import { useEffect, useMemo, useRef, useState } from "react"; import { selectedCharacter, useShellStore } from "../app/shellStore"; import { enterBossTesting, prepareBossTesting } from "../app/bossTestingRuntime"; import { BOSS_TESTING_STATUS_LABELS, useBossTestingReviews } from "../app/bossTestingReviews"; import { BOSS_TESTING_CATALOG, randomTestingBoss, type TestableBoss } from "../game/bossTesting"; import { defaultPartyRoleForClass, partyRolesForClass, PARTY_ROLE_LABELS, type PartyRole } from "../game/partyRoles"; import { specializationById } from "../game/specializationCatalog"; import { useContentManagerSnapshot } from "../content/useContentManager"; import { DualDisplayFrame } from "../components/DualDisplayFrame"; import { useMenuController, type MenuAction } from "../input/useMenuController"; import { FrontSurface, BrandMark, ControllerLegend } from "./FrontSurface"; import { ControllerButton } from "./ControllerButton"; import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel"; import "./bossTesting.css"; export function BossTestingScreen({ onClose }: { readonly onClose: () => void }) { const character = useShellStore(selectedCharacter); const session = useShellStore((state) => state.session); const reviews = useBossTestingReviews((state) => state.reviews); const refresh = useBossTestingReviews((state) => state.refresh); const content = useContentManagerSnapshot(); const [query, setQuery] = useState(""); const [category, setCategory] = useState("all"); const [status, setStatus] = useState("all"); const [selectedKey, setSelectedKey] = useState(""); const [preparing, setPreparing] = useState(false); const [prepared, setPrepared] = useState(false); const [error, setError] = useState(""); const [role, setRole] = useState(character ? defaultPartyRoleForClass(character.classId) : "damage"); const busy = useRef(false); const alive = useRef(true); const bosses = useMemo(() => BOSS_TESTING_CATALOG.filter((boss) => (category === "all" || boss.category === category) && (status === "all" || (reviews[boss.key]?.status ?? "needs-testing") === status) && `${boss.name} ${boss.dungeonTitle}`.toLowerCase().includes(query.trim().toLowerCase()) ), [category, query, reviews, status]); const selected = BOSS_TESTING_CATALOG.find((boss) => boss.key === selectedKey); const specialization = specializationById(character?.specializationId); const roles = character ? specialization ? [specialization.role] : partyRolesForClass(character.classId) : []; const effectiveRole = specialization?.role ?? role; useEffect(() => { alive.current = true; void refresh(session); const reconnect = () => void refresh(session); window.addEventListener("online", reconnect); return () => { alive.current = false; window.removeEventListener("online", reconnect); }; }, [refresh, session]); const choose = async (boss: TestableBoss, start = false) => { if (busy.current || !character) return; busy.current = true; setSelectedKey(boss.key); setPreparing(true); setPrepared(false); setError(""); try { await prepareBossTesting(boss); if (!alive.current) return; setPrepared(true); if (start) enterBossTesting(boss, effectiveRole); } catch (failure) { if (alive.current) setError(failure instanceof Error ? failure.message : "The boss could not be prepared. Please retry."); } finally { busy.current = false; if (alive.current) setPreparing(false); } }; const testSelected = () => { if (!selected || preparing) return; if (!prepared) { void choose(selected, true); return; } try { enterBossTesting(selected, effectiveRole); } catch (failure) { setError(failure instanceof Error ? failure.message : "The test could not be started."); } }; const random = () => { const boss = randomTestingBoss(bosses); if (boss) void choose(boss, true); }; const actions: MenuAction[] = [ { id: "boss-testing-back", run: onClose }, { id: "boss-testing-category", run: () => setCategory(category === "all" ? "wow" : category === "wow" ? "rom" : "all"), enabled: !preparing }, { id: "boss-testing-status", run: () => { const values = ["all", "ready", "needs-testing", "not-ready"]; setStatus(values[(values.indexOf(status) + 1) % values.length]); }, enabled: !preparing }, { id: "boss-testing-picker", run: () => { const next = bosses[(bosses.findIndex((boss) => boss.key === selectedKey) + 1) % bosses.length]; if (next) void choose(next); }, enabled: !preparing && bosses.length > 0 }, ...roles.map((value) => ({ id: `boss-testing-role-${value}`, run: () => setRole(value), enabled: !preparing })), { id: "boss-testing-start", run: testSelected, enabled: Boolean(selected) && !preparing }, { id: "boss-testing-random", run: random, enabled: bosses.length > 0 && !preparing }, ]; const controller = useMenuController(actions, { initialId: "boss-testing-picker", onBack: onClose }); const selectProps = (controlId: string) => ({ className: controller.selectedId === controlId ? "is-controller-selected" : "", onPointerEnter: () => controller.select(controlId), disabled: preparing }); const button = (controlId: string, label: string, run: () => void, disabled = false) => {label}; return
Main menu / Mechanics laboratory

Boss Testing

{button("boss-testing-back", "Back to main menu", onClose)}

Choose a boss to test with your party in an empty room. Missing content downloads automatically when you select a boss.

{selected &&

Selected encounter

{selected.name}

{selected.dungeonTitle}

{BOSS_TESTING_STATUS_LABELS[reviews[selected.key]?.status ?? "needs-testing"]} {reviews[selected.key]?.pending && Readiness saved on this device; waiting to sync.}
} {roles.length > 1 &&
{roles.map((value) => setRole(value)} disabled={preparing}>{PARTY_ROLE_LABELS[value]} )}
} {preparing &&

Preparing {selected?.name}… {content.installingPackId ? "Downloading content" : "Checking assets"}

{content.installingPackId && <>{Math.round(content.downloadedBytes / 1024 / 1024)} / {Math.round(content.totalBytes / 1024 / 1024)} MB}
} {error &&

{error}

} {!bosses.length &&

No bosses match these filters.

}
{button("boss-testing-start", error ? "Retry and test selected boss" : "Test selected boss", testSelected, !selected || preparing)} {button("boss-testing-random", "Fight a random boss", random, !bosses.length || preparing)}

Mark readiness and submit a boss report from pause or after victory. Test fights do not award loot, XP, or boss cards.

} bottom={} />; }