import { useEffect, useMemo, useState } from "react"; import { selectedCharacter, useShellStore } from "../app/shellStore"; import { useDungeonRaceStore, type RacePool } from "../app/dungeonRaceStore"; import { loadDungeonQueuePolicy, useDungeonQueuePolicyStore } from "../app/dungeonQueuePolicyStore"; import { requestOnlineJson } from "../app/onlineAccountClient"; import { type RaceBotSource, type RaceBotTier } from "../app/dungeonRaceBot"; import { dungeonsByCategory, randomizedBiteSizedDungeons } from "../game/dungeonCatalog"; import { dungeonEnabledForQueue } from "../game/dungeonQueuePolicy"; import { formatDungeonDuration } from "../game/dungeonRunTypes"; import { dungeonRaceAffix } from "../game/dungeonRaceAffixes"; import { dungeonLeaderboardCatalog as leaderboardCatalog } from "../game/dungeonLeaderboardCatalog"; import { partyRolesForClass, PARTY_ROLE_LABELS, type PartyRole } from "../game/partyStore"; import { specializationById } from "../game/specializationCatalog"; import { BrandMark, FrontSurface } from "./FrontSurface"; import "./dungeonRace.css"; import { BiteSizedBoardFilters, useBiteSizedBoardSelection } from "./BiteSizedBoardFilters"; interface BoardEntry { readonly accountId: string; readonly username: string; readonly rank: number; readonly wins?: number; readonly losses?: number; readonly durationMs?: number; readonly affixIds?: readonly string[] } interface BoardResponse { readonly entries: readonly BoardEntry[]; readonly total: number } export function DungeonRaceScreen({ onClose }: { readonly onClose: () => void }) { const character = useShellStore((state) => state.activeCharacter) ?? useShellStore(selectedCharacter); const session = useShellStore((state) => state.session); const race = useDungeonRaceStore(); const policy = useDungeonQueuePolicyStore((state) => state.config); const refreshPolicy = useDungeonQueuePolicyStore((state) => state.refresh); const [pool, setPool] = useState("authored"); const [dungeonId, setDungeonId] = useState(""); const [practice, setPractice] = useState(!session?.accessToken); const [tier, setTier] = useState("regular"); const [source, setSource] = useState("wow"); const [role, setRole] = useState(null); const [period, setPeriod] = useState<"30d" | "all">("30d"); const [boardScope, setBoardScope] = useState<"overall" | "dungeon">("overall"); const [boardView, setBoardView] = useState<"wins" | "times">("wins"); const [boardBossId, setBoardBossId] = useState(""); const [board, setBoard] = useState(null); const [boardError, setBoardError] = useState(""); useEffect(() => { void refreshPolicy(session); }, [refreshPolicy, session]); const allEntries = useMemo(() => (pool === "randomized" ? randomizedBiteSizedDungeons() : dungeonsByCategory("bite-sized")).filter((entry) => entry.available && dungeonEnabledForQueue(policy, entry)), [pool, policy]); const boardDungeonId = dungeonId || allEntries[0]?.id || ""; const boardDungeon = leaderboardCatalog.dungeons.find((entry) => entry.id === boardDungeonId); const bossOptions = boardDungeon?.difficulties.find((entry) => entry.id === boardDungeon.defaultDifficultyId) ?.finalBossIds?.map((id) => boardDungeon.bosses.find((boss) => boss.id === id)!) ?? []; const selectedBoardBossId = bossOptions.find((boss) => boss.id === boardBossId)?.id ?? bossOptions[0]?.id; const spotBoard = useBiteSizedBoardSelection(boardDungeonId); useEffect(() => { const controller = new AbortController(); const query = new URLSearchParams({ view: boardScope === "overall" ? "wins" : boardView, period, ...(boardScope === "dungeon" && boardDungeonId ? { dungeon: boardDungeonId, ...(selectedBoardBossId ? { boss: selectedBoardBossId } : {}), ...(boardView === "times" && spotBoard.spotId ? { spot: spotBoard.spotId, spotRevision: String(spotBoard.spotRevision) } : {}) } : {}) }); if (boardScope === "dungeon" && !boardDungeonId) { setBoard(null); return; } void requestOnlineJson(`/api/dungeon-races/leaderboard?${query}`, { signal: controller.signal }) .then((value) => { setBoard(value); setBoardError(""); }) .catch((error: unknown) => { if (!controller.signal.aborted) { setBoard(null); setBoardError(error instanceof Error ? error.message : "Leaderboard unavailable."); } }); return () => controller.abort(); }, [boardDungeonId, boardScope, boardView, period, selectedBoardBossId, spotBoard.spotId, spotBoard.spotRevision]); const specialization = specializationById(character?.specializationId); const roles = character?.categoryId === "rom" ? partyRolesForClass(character.classId) : specialization ? [specialization.role] : []; const eligibleCharacter = character && ["wow", "rom"].includes(character.categoryId ?? "wow"); const selectedRole = role && roles.includes(role) ? role : roles[0]; const busy = ["queued", "preparing", "countdown", "active"].includes(race.status) || race.preparingContent; const start = async () => { if (!character || !eligibleCharacter || !selectedRole || !allEntries.length) return; const latest = await loadDungeonQueuePolicy(); const candidates = allEntries.filter((entry) => dungeonEnabledForQueue(latest, entry)); if (!candidates.length) { useDungeonRaceStore.setState({ error: "No dungeon is enabled for this race queue." }); return; } if (practice) await race.startPractice(character, selectedRole, dungeonId || candidates[Math.floor(Math.random() * candidates.length)].id, tier, source); else if (session?.accessToken) await race.startOnline(session, character, selectedRole, pool, dungeonId || undefined); }; return
Main menu / Dungeon Race

Bite-Sized Dungeon Race

Race another player or a full NPC party through matching short dungeons. First final boss kill wins. You can still finish and earn its card after a loss. Each race rolls one kill mechanic and one movement mechanic for both sides.

{roles.map((candidate) => )}
{practice &&
Practice awards local boss cards and never enters a leaderboard.
} {!eligibleCharacter &&

Select a WoW or RoM character to race. CoA characters will be added later.

} {!session?.accessToken && !practice &&

Sign in online for ranked races.

} {!allEntries.length &&

No Bite-Sized dungeon is enabled for this queue.

} {race.status === "queued" &&

Searching for a player. An NPC team joins after five seconds.

} {race.status === "preparing" &&

{race.preparingContent ? "Installing race content…" : "Preparing both parties…"}

} {race.error &&

{race.error}

}
{busy && }

Leaderboards

{boardScope === "dungeon" && boardView === "times" && }
{boardScope === "dungeon" &&
} {boardScope === "dungeon" && bossOptions.length > 1 && }
{boardError &&

{boardError}

} {!boardError && board?.entries.length === 0 &&

No results yet.

} {board?.entries.length ? {boardScope === "dungeon" && boardView === "times" && } {!(boardScope === "dungeon" && boardView === "times") && } {board.entries.map((entry) => {boardScope === "dungeon" && boardView === "times" && } {entry.durationMs === undefined && })}
RankPlayer {boardScope === "dungeon" && boardView === "times" ? "Best time" : "Wins"}MechanicsLosses
#{entry.rank}{entry.username} {entry.durationMs !== undefined ? formatDungeonDuration(entry.durationMs) : entry.wins}{entry.affixIds?.length ? entry.affixIds.map((id) => dungeonRaceAffix(id)?.name ?? id).join(" + ") : "Legacy"}{entry.losses}
: null}
; }