Files
healer-man/server/dungeon-race-rules.mjs
T
2026-09-27 14:31:15 -04:00

75 lines
3.4 KiB
JavaScript

import catalog from "../src/game/generated/dungeonLeaderboardCatalog.generated.json" with { type: "json" };
import bots from "../src/game/dungeonRaceBotCatalog.json" with { type: "json" };
import affixes from "../src/game/dungeonRaceAffixes.json" with { type: "json" };
export const RACE_QUEUE_WAIT_MS = 5_000;
export const RACE_PREPARE_MS = 5 * 60_000;
export const RACE_COUNTDOWN_MS = 3_000;
export const RACE_DISCONNECT_MS = 30_000;
export const RACE_MAX_MS = 2 * 60 * 60_000;
export const RACE_PERIOD_MS = 30 * 24 * 60 * 60_000;
export function racePoolForDungeon(id) {
if (id.startsWith("randomized-bite-sized-")) return "randomized";
return id.startsWith("bite-sized-") ? "authored" : null;
}
export function raceDungeons(pool, policy) {
const disabled = new Set(policy?.disabled?.[pool === "randomized" ? "randomizedBiteSized" : "biteSized"] ?? []);
return catalog.dungeons.filter((dungeon) => racePoolForDungeon(dungeon.id) === pool
&& dungeon.difficulties.some((difficulty) => difficulty.finalBossId) && !disabled.has(dungeon.id));
}
export function compatibleDungeons(left, right, policy) {
if (left.pool !== right.pool) return [];
return raceDungeons(left.pool, policy).filter((dungeon) =>
(!left.dungeonId || left.dungeonId === dungeon.id) && (!right.dungeonId || right.dungeonId === dungeon.id));
}
export function hashRaceSeed(value) {
let hash = 2166136261;
for (const char of value) { hash ^= char.charCodeAt(0); hash = Math.imul(hash, 16777619); }
return hash >>> 0;
}
export function rollRaceAffixes(seed) {
return [
affixes.kill[hashRaceSeed(`${seed}:kill-affix`) % affixes.kill.length].id,
affixes.movement[hashRaceSeed(`${seed}:movement-affix`) % affixes.movement.length].id,
];
}
export function raceAffixPace(pair = []) {
return pair.reduce((factor, id) => factor * (
[...affixes.kill, ...affixes.movement].find((entry) => entry.id === id)?.botMultiplier ?? 1
), 1);
}
export function botTierForRecord(wins, losses, seed) {
const total = wins + losses;
const rate = total < 5 ? 0.5 : wins / total;
const base = rate < 0.4 ? 0 : rate < 0.6 ? 1 : rate < 0.8 ? 2 : 3;
const roll = hashRaceSeed(seed) % 4;
return bots.tiers[Math.max(0, Math.min(3, base + (roll === 0 ? -1 : roll === 3 ? 1 : 0)))].id;
}
export function botOpponent(seed, tierId, source = null, affixIds = []) {
const tier = bots.tiers.find((entry) => entry.id === tierId) ?? bots.tiers[1];
const resolvedSource = source ?? (hashRaceSeed(`${seed}:source`) % 2 ? "rom" : "wow");
const variance = 0.92 + (hashRaceSeed(`${seed}:pace`) % 161) / 1000;
const finishMs = Math.round(bots.baselineMs * tier.multiplier * variance * raceAffixPace(affixIds));
return { type: "bot", name: `${tier.label} ${resolvedSource === "rom" ? "RuneWaker" : "Azeroth"} Team`,
tier: tier.id, source: resolvedSource, roster: bots.teams[resolvedSource], finishMs,
milestones: bots.milestones.map((entry) => ({ ...entry, atMs: Math.round(finishMs * entry.at) })) };
}
export function botProgress(elapsedMs, bot) {
if (elapsedMs <= 0) return 0;
const previous = [{ atMs: 0, progress: 0 }, ...bot.milestones]
.filter((entry) => entry.atMs <= elapsedMs).at(-1);
const next = bot.milestones.find((entry) => entry.atMs > elapsedMs);
if (!next) return 100;
return Math.floor(previous.progress + (next.progress - previous.progress)
* (elapsedMs - previous.atMs) / (next.atMs - previous.atMs));
}