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)); }