import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { buildCollections, MAX_HUNTER_NAME_LENGTH, MODE_COPY, normalizeHunterName } from "../frontend/data"; import { formatPlayTime, formatSaveTimestamp } from "../frontend/saveRepository"; import { resolveSaveContinuation, saveVersionsMatch } from "../frontend/saveContinuation"; import { useActiveHunter, useFrontendStore } from "../frontend/store"; import type { GameModeId, ProfileStatId, SaveSlotId, SaveSlotState } from "../frontend/types"; import { PROFILE_SECTIONS, alphabeticalBosses, defaultStatForSection, isBossProfileStat, profileSectionForStat, } from "../frontend/profileSections"; import { useMenuController, type MenuAction } from "../input/useMenuController"; import { HEALER_CLASSES, HEALER_CLASS_ORDER, isHealerClassId } from "../game/healers"; import { HEALER_GUIDES } from "../game/healerGuides"; import { ABILITY_ORDER } from "../game/data"; import { BOSS_DEFINITIONS, BOSS_GROUP_BY_ID, BOSS_GROUPS } from "../game/bossCatalog"; import { bossMechanicIsPassive, bossMechanicName } from "../game/bosses/mechanicPool"; import { RUN_BUFFS, formatRunBuffEffect, selectRandomBossPair } from "../game/roguelike"; import type { BossId } from "../game/types"; import { GEAR_OWNER_LABELS, GEAR_OWNER_ORDER, GEAR_RECIPES, GEAR_SLOT_LABELS, GEAR_SLOT_ORDER, GEAR_STAT_LABELS, MAX_GEAR_LEVEL, canAffordGearUpgrade, canUpgradeGearSlot, gearBonusText, gearUpgradeCosts, type GearOwnerId, } from "../game/progression/gear"; import { DIFFICULTIES, DIFFICULTY_BY_SLUG, bossGroupDrop } from "../game/progression/loot"; import { ACTIVE_INFUSION_MIN_GEAR_LEVEL, PASSIVE_INFUSIONS, PASSIVE_INFUSION_MIN_GEAR_LEVEL, activeInfusionUnlocked, infusionCosts, infusionsForOwner, passiveInfusionUnlocked, } from "../game/progression/infusions"; import { requestDisplaySurface } from "../platform/displayRouting"; import { onlineRepository, type LeaderboardResult } from "../frontend/onlineRepository"; import { leaderboardCache } from "../frontend/leaderboardCache"; import { hasPendingSaveSync, networkAppearsOnline } from "../frontend/saveSync"; import { DualDisplayFrame } from "./DualDisplayFrame"; import { DEFAULT_CONTROLLER_GLYPHS } from "../input/controllerGlyphs"; import { HOCKEY_PVP_GOAL_DAMAGE, HOCKEY_PVP_QUEUE_TIMEOUT_MS, hockeyPvpBossAt, type HockeyPvpMatchConfig, } from "../game/hockeyHealingPvp"; import { startHockeyPvpMatchmaking, type HockeyPvpMatchOperation } from "../frontend/hockeyPvpMatchmaking"; import { BLOCKBREAKER_BREACH_DAMAGE } from "../game/blockbreaker"; import { APPEARANCE_SLOT_DEFINITIONS, appearanceSlotEnabled, appearanceSlotLabel, appearancesMatch, cycleAppearanceSlot, type AppearanceSlotId, } from "../game/appearanceLab"; import { CHARACTER_MODEL_MODE, type CharacterAppearanceV1 } from "../game/characterAppearance"; import { createDefaultHealerAppearance } from "../game/healerVisuals"; const HealerAppearancePreview = lazy(() => import("./GameScene").then((module) => ({ default: module.HealerAppearancePreview }))); function ControllerButton({ id, selectedId, select, className = "", onClick, onPointerEnter, onFocus: _onFocus, trackPointer = true, ...props }: React.ButtonHTMLAttributes & { id: string; selectedId: string; select: (id: string) => void; trackPointer?: boolean }) { return ( ); } function SaveScreen() { const slots = useFrontendStore((state) => state.slots); const accountId = useFrontendStore((state) => state.accountId); const selectedSlotId = useFrontendStore((state) => state.selectedSlotId); const notice = useFrontendStore((state) => state.notice); const selectSlot = useFrontendStore((state) => state.selectSlot); const createSlot = useFrontendStore((state) => state.createSlot); const playSlot = useFrontendStore((state) => state.playSlot); const uploadSlot = useFrontendStore((state) => state.uploadSlot); const downloadSlot = useFrontendStore((state) => state.downloadSlot); const copySlot = useFrontendStore((state) => state.copySlot); const deleteSlot = useFrontendStore((state) => state.deleteSlot); const navigate = useFrontendStore((state) => state.navigate); const [dialog, setDialog] = useState<"create" | "copy" | "delete" | "version" | null>(null); const [hunterName, setHunterName] = useState(""); const hunterNameRef = useRef(null); const [resolvingOnline, setResolvingOnline] = useState(false); const [versionError, setVersionError] = useState(""); const selected = slots.find((slot) => slot.id === selectedSlotId)!; const hasLocal = Boolean(selected.local); const hasOnline = Boolean(selected.online); const continuation = resolveSaveContinuation(selected); const primaryActionId = continuation === "create" ? "create" : "play"; const finishCreation = () => { if (createSlot(selectedSlotId, hunterName)) { setHunterName(""); setDialog(null); } }; const openCreation = () => { setHunterName(""); setDialog("create"); requestDisplaySurface("top"); }; const openSaveDialog = (nextDialog: "copy" | "delete") => { setDialog(nextDialog); requestDisplaySurface("top"); }; const continueWithOnline = async () => { if (resolvingOnline) return; setResolvingOnline(true); setVersionError(""); await downloadSlot(selectedSlotId); const refreshed = useFrontendStore.getState().slots.find((slot) => slot.id === selectedSlotId); if (refreshed && saveVersionsMatch(refreshed.local, refreshed.online)) { setDialog(null); setResolvingOnline(false); playSlot(selectedSlotId); return; } setVersionError(useFrontendStore.getState().notice || "Online save could not be loaded."); setResolvingOnline(false); }; const continueSelected = () => { if (continuation === "create") return openCreation(); if (continuation === "local") return playSlot(selectedSlotId); if (continuation === "online") { void continueWithOnline(); return; } setVersionError(""); setDialog("version"); requestDisplaySurface("top"); }; const actions = useMemo(() => dialog === "version" ? [ { id: "version-online", run: () => { void continueWithOnline(); }, enabled: !resolvingOnline }, { id: "version-local", run: () => { setDialog(null); playSlot(selectedSlotId); }, enabled: !resolvingOnline }, { id: "cancel-version", run: () => setDialog(null), enabled: !resolvingOnline }, ] : dialog === "create" ? [ { id: "hunter-name", run: () => hunterNameRef.current?.focus() }, { id: "confirm-create", run: finishCreation, enabled: Boolean(normalizeHunterName(hunterName)) }, { id: "cancel-create", run: () => setDialog(null) }, ] : dialog === "copy" ? slots.filter((slot) => slot.id !== selectedSlotId).map((slot) => ({ id: `copy-${slot.id}`, run: () => { copySlot(selectedSlotId, slot.id); setDialog(null); } })) : dialog === "delete" ? [ { id: "confirm-delete", run: () => { deleteSlot(selectedSlotId); setDialog(null); } }, { id: "cancel-delete", run: () => setDialog(null) }, ] : [ ...slots.map((slot, index) => ({ id: `slot-${slot.id}`, run: () => selectSlot(slot.id), neighbors: { left: `slot-${slots[Math.max(0, index - 1)].id}`, right: `slot-${slots[Math.min(slots.length - 1, index + 1)].id}`, down: primaryActionId, }, })), { id: primaryActionId, run: continueSelected, enabled: !resolvingOnline, neighbors: { up: `slot-${selectedSlotId}`, right: "upload" } }, { id: "upload", run: () => uploadSlot(selectedSlotId), enabled: hasLocal && Boolean(accountId), neighbors: { left: primaryActionId, right: "download", up: "slot-1" } }, { id: "download", run: () => downloadSlot(selectedSlotId), enabled: hasOnline && Boolean(accountId), neighbors: { left: "upload", right: "copy", up: "slot-2" } }, { id: "copy", run: () => openSaveDialog("copy"), enabled: hasLocal, neighbors: { left: "download", right: "delete", up: "slot-2" } }, { id: "delete", run: () => openSaveDialog("delete"), enabled: hasLocal, neighbors: { left: "copy", right: "back", up: "slot-3" } }, { id: "back", run: () => navigate("login"), neighbors: { left: "delete", up: "slot-3" } }, ], [accountId, continuation, copySlot, createSlot, deleteSlot, dialog, downloadSlot, hasLocal, hasOnline, hunterName, navigate, playSlot, primaryActionId, resolvingOnline, selectSlot, selectedSlotId, slots, uploadSlot]); const controller = useMenuController(actions, { onBack: () => dialog ? resolvingOnline ? undefined : setDialog(null) : navigate("login") }); const cloudStatus = continuation === "choose" ? "Newer online save" : !accountId ? "Offline mode" : selected.online ? "Online version available" : "No online version"; return (
Hunter records

Choose a save

{accountId ? `● ${accountId}` : "○ OFFLINE"}
{slots.map((slot) => ( { controller.select(`slot-${slot.id}`); selectSlot(slot.id); }} onHover={() => controller.select(`slot-${slot.id}`)} /> ))}
{continuation === "create" ? "Create hunter" : resolvingOnline ? "Loading online save…" : "Continue"} {continuation === "create" ? `Use slot ${selectedSlotId}` : continuation === "online" ? "Download online copy" : continuation === "choose" ? "Choose online or device copy" : `Slot ${selectedSlotId} · ${selected.local?.hunterName}`} uploadSlot(selectedSlotId)}>UploadDevice → server downloadSlot(selectedSlotId)}>DownloadServer → device openSaveDialog("copy")}>CopyDuplicate save openSaveDialog("delete")}>DeleteErase device copy navigate("login")}>BackLogin screen
Autosave OFFLINE FIRST
{notice || "Lower display shows selected save details."}
{dialog && (
{dialog === "version" && selected.local && selected.online ? (
Newer online save found

Which save do you want?

Online choice replaces this device copy. Device choice keeps online copy unchanged.

Online copyNEWER
{selected.online.hunterName} {formatPlayTime(selected.online.playSeconds)} · Level {selected.online.healers[selected.online.activeClassId].level}
Device copyOFFLINE
{selected.local.hunterName} {formatPlayTime(selected.local.playSeconds)} · Level {selected.local.healers[selected.local.activeClassId].level}
{ void continueWithOnline(); }}>{resolvingOnline ? "Loading…" : "Continue online copy"}{formatSaveTimestamp(selected.online.updatedAt)} { setDialog(null); playSlot(selectedSlotId); }}>Continue device copy{formatSaveTimestamp(selected.local.updatedAt)} setDialog(null)}>Cancel
{versionError &&
{versionError}
}
) : dialog === "create" ? (
{ event.preventDefault(); finishCreation(); }}> New offline save

Name your hunter

This name identifies the character in local and online save lists.

controller.select("hunter-name")} onChange={(event) => setHunterName(event.target.value)} onKeyDown={(event) => { if (event.key === "Escape") setDialog(null); }} placeholder="Enter name" /> {normalizeHunterName(hunterName).length}/{MAX_HUNTER_NAME_LENGTH}
Create hunter setDialog(null)}>Cancel
) : dialog === "copy" ? ( <> Copy local save

Choose destination

Destination local save will be overwritten. Online copies stay unchanged.

{slots.filter((slot) => slot.id !== selectedSlotId).map((slot) => ( { copySlot(selectedSlotId, slot.id); setDialog(null); }}> Slot {slot.id}{slot.local ? "Overwrite" : "Empty"} ))}
) : ( <> Delete local save

Erase slot {selectedSlotId}?

Device copy will be removed. Existing online version remains available for download.

{ deleteSlot(selectedSlotId); setDialog(null); }}>Delete local setDialog(null)}>Cancel
)}
)} } bottom={
Slot {selectedSlotId}{cloudStatus}
{selected.local ?? selected.online ? ( <>
{(selected.local ?? selected.online)!.hunterName[0]}
{selected.local ? "Device save" : "Online copy only"}

{(selected.local ?? selected.online)!.hunterName}

{(selected.local ?? selected.online)!.location}

) : ( <>
Local record

Empty slot

Create a hunter or download an online version.

)}
{(selected.local ?? selected.online) && (() => { const save = (selected.local ?? selected.online)!; const healer = HEALER_CLASSES[save.activeClassId]; return ( <>
Active healerLv {save.healers[save.activeClassId].level}{healer.name} Play time{formatPlayTime(save.playSeconds)}Local activity Boss kills{save.stats.totalBossKills}{save.stats.flawlessClears} flawless
Roguelike bestRound {save.stats.highestRoguelikeRound} Endless best{save.stats.highestRogueTrialsEndlessKills} kills
); })()}
Device copy{selected.local ? formatSaveTimestamp(selected.local.updatedAt) : "Not present"} Online copy{selected.online ? formatSaveTimestamp(selected.online.updatedAt) : accountId ? "Not uploaded" : "Sign-in required"}
{notice || "Use upper display for every save action. Details here follow selected slot."}
} /> ); } const HOME_MODES: { id: GameModeId; category: "pve" | "pvp"; icon: string; label: string; copy: string }[] = [ { id: "roguelike-pve", category: "pve", icon: "✦", label: "RPG Roguelike", copy: "Draft party, spells, gear, and route" }, { id: "rogue-trials", category: "pve", icon: "Ⅲ", label: "Rogue Trials", copy: "Four rounds, then a boss trio" }, { id: "dungeons", category: "pve", icon: "♜", label: "Dungeons", copy: "Choose your boss encounter" }, { id: "hockey-healing", category: "pve", icon: "◌", label: "Hockey Healing", copy: "Defend goal under boss pressure" }, { id: "hockey-healing-pvp", category: "pvp", icon: "◇", label: "Healing Hockey PVP", copy: "Online mirrored healer duel" }, { id: "blockbreaker", category: "pve", icon: "▦", label: "Blockbreaker", copy: "Break color walls while healing" }, { id: "aether-assault", category: "pve", icon: "⌁", label: "Aether Assault", copy: "Auto-fire through arcane formations" }, { id: "roguelike-pvp", category: "pvp", icon: "⚔", label: "Roguelike PvP", copy: "Draft, race, sabotage" }, { id: "stadium-pvp", category: "pvp", icon: "◉", label: "Stadium PvP", copy: "Prepared 5v5 rounds" }, ]; const HOME_MODE_SECTIONS = [ { id: "pve", title: "PVE Modes", copy: "Cooperative expeditions", modes: HOME_MODES.filter((mode) => mode.category === "pve") }, { id: "pvp", title: "PVP Modes", copy: "Competitive arenas", modes: HOME_MODES.filter((mode) => mode.category === "pvp") }, ] as const; function HomeScreen() { const hunter = useActiveHunter(); const accountId = useFrontendStore((state) => state.accountId); const selectMode = useFrontendStore((state) => state.selectMode); const selectHealerClass = useFrontendStore((state) => state.selectHealerClass); const openAppearanceLab = useFrontendStore((state) => state.openAppearanceLab); const openClassHelp = useFrontendStore((state) => state.openClassHelp); const navigate = useFrontendStore((state) => state.navigate); const actions = useMemo(() => [ { id: "roguelike-pve", run: () => selectMode("roguelike-pve"), neighbors: { left: "roguelike-pve", right: "rogue-trials", down: "dungeons" } }, { id: "rogue-trials", run: () => selectMode("rogue-trials"), neighbors: { left: "roguelike-pve", right: "hockey-healing-pvp", down: "hockey-healing" } }, { id: "dungeons", run: () => selectMode("dungeons"), neighbors: { left: "dungeons", right: "hockey-healing", up: "roguelike-pve", down: "blockbreaker" } }, { id: "hockey-healing", run: () => selectMode("hockey-healing"), neighbors: { left: "dungeons", right: "roguelike-pvp", up: "rogue-trials", down: "aether-assault" } }, { id: "blockbreaker", run: () => selectMode("blockbreaker"), neighbors: { left: "blockbreaker", right: "aether-assault", up: "dungeons", down: "profile" } }, { id: "aether-assault", run: () => selectMode("aether-assault"), neighbors: { left: "blockbreaker", right: "stadium-pvp", up: "hockey-healing", down: "appearance" } }, { id: "hockey-healing-pvp", run: () => selectMode("hockey-healing-pvp"), neighbors: { left: "rogue-trials", right: "hockey-healing-pvp", down: "roguelike-pvp" } }, { id: "roguelike-pvp", run: () => selectMode("roguelike-pvp"), neighbors: { left: "hockey-healing", right: "roguelike-pvp", up: "hockey-healing-pvp", down: "stadium-pvp" } }, { id: "stadium-pvp", run: () => selectMode("stadium-pvp"), neighbors: { left: "aether-assault", right: "stadium-pvp", up: "roguelike-pvp", down: "class-help" } }, { id: "profile", run: () => navigate("profile"), neighbors: { up: "blockbreaker", left: "class-help", right: "gear", down: "class-priest" } }, { id: "gear", run: () => navigate("gear"), neighbors: { up: "aether-assault", left: "profile", right: "appearance", down: "class-druid" } }, { id: "appearance", run: openAppearanceLab, neighbors: { up: "aether-assault", left: "gear", right: "settings", down: "class-priest" } }, { id: "settings", run: () => navigate("settings"), neighbors: { up: "stadium-pvp", left: "appearance", right: "class-help", down: "class-druid" } }, { id: "class-help", run: openClassHelp, neighbors: { up: "stadium-pvp", left: "settings", right: "profile", down: "class-chronomancer" } }, ...HEALER_CLASS_ORDER.map((classId, index) => ({ id: `class-${classId}`, run: () => selectHealerClass(classId), neighbors: { left: `class-${HEALER_CLASS_ORDER[index % 3 === 0 ? Math.min(index + 2, HEALER_CLASS_ORDER.length - 1) : index - 1]}`, right: `class-${HEALER_CLASS_ORDER[index % 3 === 2 || index === HEALER_CLASS_ORDER.length - 1 ? index - (index % 3) : index + 1]}`, up: index < 3 ? (["appearance", "settings", "settings"] as const)[index] : `class-${HEALER_CLASS_ORDER[index - 3]}`, down: index + 3 < HEALER_CLASS_ORDER.length ? `class-${HEALER_CLASS_ORDER[index + 3]}` : "change-save", }, })), { id: "change-save", run: () => navigate("saves"), neighbors: { up: "class-chronomancer" } }, ], [navigate, openAppearanceLab, openClassHelp, selectHealerClass, selectMode]); const controller = useMenuController(actions, { columns: 2, onBack: () => navigate("saves") }); if (!hunter) return null; const activeHealer = HEALER_CLASSES[hunter.activeClassId]; const activeProgress = hunter.healers[hunter.activeClassId]; return (
Welcome back, {hunter.hunterName}{accountId ? "● SYNC READY" : "○ OFFLINE"}
{HOME_MODE_SECTIONS.map((section) => (
{section.copy}{section.title}{section.modes.length} ACTIVITIES
{section.modes.map((mode) => ( selectMode(mode.id)}> {mode.icon}{mode.copy}{mode.label} ))}
))}
navigate("profile")}>Hunter ProfileStats & collection log navigate("gear")}>Gear UpgradeSpend group drops Appearance LabBuild and preview your healer navigate("settings")}>SettingsAudio, display, controls ?Class HelpAbilities, rotations, synergies
} bottom={
Active hunterLOCAL AUTOSAVE
{hunter.hunterName[0]}{activeHealer.icon}
Level {activeProgress.level} · {activeHealer.specialization}

{hunter.hunterName}

{hunter.location}

Boss kills{hunter.stats.totalBossKills} Flawless{hunter.stats.flawlessClears} Allies saved{hunter.stats.alliesSaved}
Choose healer
{HEALER_CLASS_ORDER.map((classId) => { const healer = HEALER_CLASSES[classId]; const progress = hunter.healers[classId]; return selectHealerClass(classId)}> {healer.icon}{healer.name}Level {progress.level} · {progress.inventory.length} items{classId === hunter.activeClassId ? "✓" : ""} ; })}
navigate("saves")}>Change save slotLast saved {formatSaveTimestamp(hunter.updatedAt)}
} /> ); } function ClassHelpScreen() { const navigate = useFrontendStore((state) => state.navigate); const classId = useFrontendStore((state) => state.guideClassId); const selectedAbilityId = useFrontendStore((state) => state.guideAbilityId); const selectClass = useFrontendStore((state) => state.selectGuideClass); const selectAbility = useFrontendStore((state) => state.selectGuideAbility); const healer = HEALER_CLASSES[classId]; const guide = HEALER_GUIDES[classId]; const selectedAbility = healer.abilities[selectedAbilityId]; const selectedGuide = guide.abilityGuides[selectedAbilityId]; const classIndex = HEALER_CLASS_ORDER.indexOf(classId); const actions = useMemo(() => [ { id: "guide-back", run: () => navigate("home"), neighbors: { right: "guide-class-priest", down: "guide-class-priest" } }, ...HEALER_CLASS_ORDER.map((candidate, index) => ({ id: `guide-class-${candidate}`, run: () => selectClass(candidate), neighbors: { left: `guide-class-${HEALER_CLASS_ORDER[index === 0 ? HEALER_CLASS_ORDER.length - 1 : index - 1]}`, right: `guide-class-${HEALER_CLASS_ORDER[(index + 1) % HEALER_CLASS_ORDER.length]}`, up: "guide-back", down: `guide-${ABILITY_ORDER[Math.min(index, ABILITY_ORDER.length - 1)]}`, }, })), ...ABILITY_ORDER.map((abilityId, index) => ({ id: `guide-${abilityId}`, run: () => selectAbility(abilityId), neighbors: { left: `guide-${ABILITY_ORDER[index % 3 === 0 ? index + 2 : index - 1]}`, right: `guide-${ABILITY_ORDER[index % 3 === 2 ? index - 2 : index + 1]}`, up: index < 3 ? `guide-class-${classId}` : `guide-${ABILITY_ORDER[index - 3]}`, down: index < 3 ? `guide-${ABILITY_ORDER[index + 3]}` : "guide-back", }, })), ], [classId, navigate, selectAbility, selectClass]); const controller = useMenuController(actions, { initialId: `guide-class-${classId}`, onBack: () => navigate("home"), }); return (
Field manual

Class Help

{classIndex + 1} / {HEALER_CLASS_ORDER.length} navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
{healer.icon} {healer.specialization} · {guide.learningCurve}

{guide.role}

{healer.description}

{healer.secondaryResourceName ? `${healer.resourceName} + ${healer.secondaryResourceName}` : healer.resourceName}
{ABILITY_ORDER.map((abilityId) => { const ability = healer.abilities[abilityId]; return ( selectAbility(abilityId)} > {ability.icon}{ability.gamepad} {ability.name}{ability.description} {ability.cooldown ? `${ability.cooldown}s CD` : "No cooldown"}{ability.mana} {healer.resourceName}{ability.targeting} ); })}
} bottom={
{healer.name} field guide{guide.learningCurve.toUpperCase()}
{healer.icon}{healer.specialization}

{guide.role}

{guide.resourceGuide}

Core loop01—03
{guide.coreLoop.map((step, index) =>

0{index + 1}{step}

)}
{selectedAbility.icon}Selected ability · {selectedAbility.gamepad}

{selectedAbility.name}

{selectedGuide.useWhen}

Synergies{selectedGuide.synergies.map((synergy) => { const pairedAbility = healer.abilities[synergy.with]; return
{pairedAbility.icon}{pairedAbility.name}{synergy.summary}{synergy.kind === "mechanic" ? "DIRECT" : "COMBO"}
; })}
} /> ); } function ProfileScreen() { const hunter = useActiveHunter(); const accountId = useFrontendStore((state) => state.accountId); const navigate = useFrontendStore((state) => state.navigate); const uploadSlot = useFrontendStore((state) => state.uploadSlot); const collectionView = useFrontendStore((state) => state.profileCollectionView); const groupId = useFrontendStore((state) => state.selectedProfileGroupId); const selectedStat = useFrontendStore((state) => state.selectedProfileStatId); const setCollectionView = useFrontendStore((state) => state.selectProfileCollectionView); const setGroupId = useFrontendStore((state) => state.selectProfileGroup); const setSelectedStat = useFrontendStore((state) => state.selectProfileStat); const collections = useMemo(() => hunter ? buildCollections(hunter.collectionLog, hunter.stats.bossKills) : [], [hunter]); const bosses = useMemo(() => alphabeticalBosses(collections), [collections]); const collection = collections.find((group) => group.groupId === groupId) ?? collections[0]; const profileView = collectionView === "loot" ? "loot" : "stats"; const activeSection = profileSectionForStat(selectedStat); const activeSectionDefinition = PROFILE_SECTIONS.find((section) => section.id === activeSection) ?? PROFILE_SECTIONS[0]; const selectedBoss = isBossProfileStat(selectedStat) ? bosses.find((boss) => boss.bossId === selectedStat) : undefined; const [leaderboard, setLeaderboard] = useState(null); const [leaderboardStatus, setLeaderboardStatus] = useState(""); const [leaderboardUpdatedAt, setLeaderboardUpdatedAt] = useState(null); const [leaderboardOwner, setLeaderboardOwner] = useState(null); const [refreshingLeaderboard, setRefreshingLeaderboard] = useState(false); const leaderboardRequestId = useRef(0); const leaderboardRefreshActive = useRef(false); const displaySurface = document.documentElement.dataset.displaySurface; const rendersTopSurface = displaySurface !== "bottom"; const rendersBottomSurface = displaySurface !== "top"; useEffect(() => { if (!isBossProfileStat(selectedStat) || bosses.some((boss) => boss.bossId === selectedStat)) return; setSelectedStat(bosses[0]?.bossId ?? "roguelike"); }, [bosses, selectedStat, setSelectedStat]); useEffect(() => { if (!rendersTopSurface || !hunter || profileView !== "stats" || activeSection === "bosses") return; const requestId = ++leaderboardRequestId.current; leaderboardRefreshActive.current = false; setRefreshingLeaderboard(false); const cached = leaderboardCache.read(hunter.slotId, selectedStat, hunter.hunterName, accountId); setLeaderboard(cached?.result ?? null); setLeaderboardUpdatedAt(cached?.updatedAt ?? null); setLeaderboardOwner(cached?.accountId ?? accountId); setLeaderboardStatus(cached ? "" : accountId ? "No cached rankings. Refresh when online." : "Sign in once, then refresh rankings for offline viewing."); return () => { if (leaderboardRequestId.current === requestId) leaderboardRequestId.current += 1; }; }, [accountId, activeSection, hunter, profileView, rendersTopSurface, selectedStat]); const refreshLeaderboard = useCallback(async () => { if (!rendersTopSurface || !hunter || leaderboardRefreshActive.current) return; if (!accountId) { setLeaderboardStatus("Sign in to refresh online rankings."); return; } if (!networkAppearsOnline()) { setLeaderboardStatus("Offline. Cached rankings remain available."); return; } const requestId = ++leaderboardRequestId.current; leaderboardRefreshActive.current = true; setRefreshingLeaderboard(true); if (hasPendingSaveSync(hunter.slotId)) { setLeaderboardStatus("Publishing local records…"); const uploaded = await uploadSlot(hunter.slotId); if (leaderboardRequestId.current !== requestId) return; if (!uploaded) { setLeaderboardStatus("Local records queued. Refresh when connection returns."); leaderboardRefreshActive.current = false; setRefreshingLeaderboard(false); return; } } setLeaderboardStatus("Refreshing online rankings…"); const request = selectedStat === "roguelike" ? onlineRepository.roguelikeLeaderboard(hunter.slotId) : selectedStat === "rogue-trials-endless" ? onlineRepository.rogueTrialsEndlessLeaderboard(hunter.slotId) : selectedStat === "hockey-healing" ? onlineRepository.hockeyHealingLeaderboard(hunter.slotId) : selectedStat === "hockey-pvp-wins" ? onlineRepository.hockeyPvpWinsLeaderboard(hunter.slotId) : selectedStat === "hockey-pvp-boss-kills" ? onlineRepository.hockeyPvpBossKillsLeaderboard(hunter.slotId) : selectedStat === "blockbreaker-bricks" ? onlineRepository.blockbreakerBricksLeaderboard(hunter.slotId) : selectedStat === "blockbreaker-time" ? onlineRepository.blockbreakerTimeLeaderboard(hunter.slotId) : selectedStat === "blockbreaker-score" ? onlineRepository.blockbreakerScoreLeaderboard(hunter.slotId) : selectedStat === "aether-assault" ? onlineRepository.aetherAssaultLeaderboard(hunter.slotId) : onlineRepository.bossLeaderboard(selectedStat, hunter.slotId); try { const result = await request; const cached = leaderboardCache.write(accountId, hunter.hunterName, hunter.slotId, selectedStat, result); if (leaderboardRequestId.current !== requestId) return; setLeaderboard(result); setLeaderboardUpdatedAt(cached.updatedAt); setLeaderboardOwner(accountId); setLeaderboardStatus(""); } catch (error) { if (leaderboardRequestId.current !== requestId) return; setLeaderboardStatus(error instanceof Error ? error.message : "Leaderboard unavailable."); } finally { if (leaderboardRequestId.current === requestId) { leaderboardRefreshActive.current = false; setRefreshingLeaderboard(false); } } }, [accountId, hunter, rendersTopSurface, selectedStat, uploadSlot]); const sectionMetricIds = activeSectionDefinition.statIds; const bossGridColumns = 7; const actions = useMemo(() => [ ...(rendersTopSurface ? [ { id: "view-stats", run: () => setCollectionView("stats"), neighbors: { right: "view-loot", down: profileView === "stats" ? "section-roguelike" : undefined } }, { id: "view-loot", run: () => setCollectionView("loot"), neighbors: { left: "view-stats" } }, ...(profileView === "stats" ? [ ...PROFILE_SECTIONS.map((section, index) => ({ id: `section-${section.id}`, run: () => setSelectedStat(defaultStatForSection(section.id, bosses)), neighbors: { up: index === 0 ? "view-stats" : `section-${PROFILE_SECTIONS[index - 1].id}`, down: index === PROFILE_SECTIONS.length - 1 ? "section-roguelike" : `section-${PROFILE_SECTIONS[index + 1].id}`, right: section.id === "bosses" ? `boss-card-${bosses[0]?.bossId}` : `metric-${section.statIds[0]}`, }, })), ...(activeSection === "bosses" ? bosses.map((boss, index) => { const rowStart = index % bossGridColumns === 0; const rowEnd = index % bossGridColumns === bossGridColumns - 1 || index === bosses.length - 1; return { id: `boss-card-${boss.bossId}`, run: () => setSelectedStat(boss.bossId), neighbors: { left: rowStart ? "section-bosses" : `boss-card-${bosses[index - 1].bossId}`, right: rowEnd ? `boss-card-${boss.bossId}` : `boss-card-${bosses[index + 1].bossId}`, up: index < bossGridColumns ? "view-stats" : `boss-card-${bosses[index - bossGridColumns].bossId}`, down: bosses[index + bossGridColumns] ? `boss-card-${bosses[index + bossGridColumns].bossId}` : `boss-card-${boss.bossId}`, }, }; }) : [ ...sectionMetricIds.map((statId, index) => ({ id: `metric-${statId}`, run: () => setSelectedStat(statId), neighbors: { left: index === 0 ? `section-${activeSection}` : `metric-${sectionMetricIds[index - 1]}`, right: index === sectionMetricIds.length - 1 ? "refresh-leaderboard" : `metric-${sectionMetricIds[index + 1]}`, up: "view-stats", down: "refresh-leaderboard", }, })), { id: "refresh-leaderboard", run: () => { void refreshLeaderboard(); }, enabled: Boolean(accountId), neighbors: { left: `metric-${selectedStat}`, up: `metric-${selectedStat}` } }, ]), ] : []), { id: "back", run: () => navigate("home") }, ] : []), ...(rendersBottomSurface && profileView === "loot" ? collections.map((group) => ({ id: `group-${group.groupId}`, run: () => setGroupId(group.groupId) })) : []), ], [accountId, activeSection, bosses, collections, navigate, profileView, refreshLeaderboard, rendersBottomSurface, rendersTopSurface, sectionMetricIds, selectedStat, setCollectionView, setGroupId, setSelectedStat]); const controller = useMenuController(actions, { onBack: () => navigate("home") }); useEffect(() => { const hoveredSection = PROFILE_SECTIONS.find((section) => `section-${section.id}` === controller.selectedId); if (hoveredSection) { const nextStat = defaultStatForSection(hoveredSection.id, bosses); if (nextStat !== selectedStat) setSelectedStat(nextStat); return; } const hoveredMetric = sectionMetricIds.find((statId) => `metric-${statId}` === controller.selectedId); if (hoveredMetric && hoveredMetric !== selectedStat) { setSelectedStat(hoveredMetric); return; } const hoveredBoss = bosses.find((boss) => `boss-card-${boss.bossId}` === controller.selectedId); if (hoveredBoss && hoveredBoss.bossId !== selectedStat) setSelectedStat(hoveredBoss.bossId); }, [bosses, controller.selectedId, sectionMetricIds, selectedStat, setSelectedStat]); if (!hunter || !collection) return null; const activeHealer = HEALER_CLASSES[hunter.activeClassId]; const activeProgress = hunter.healers[hunter.activeClassId]; const earned = collection.drops.filter((drop) => drop.count > 0).length; const petsOwned = bosses.filter((boss) => boss.pet.count > 0).length; const hockeyDuration = `${Math.floor(hunter.stats.longestHockeyHealingSecondsAtBest / 60)}:${String(Math.floor(hunter.stats.longestHockeyHealingSecondsAtBest % 60)).padStart(2, "0")}`; const blockbreakerDuration = `${Math.floor(hunter.stats.longestBlockbreakerSeconds / 60)}:${String(Math.floor(hunter.stats.longestBlockbreakerSeconds % 60)).padStart(2, "0")}`; const aetherDuration = `${Math.floor(hunter.stats.longestAetherAssaultSecondsAtBest / 60)}:${String(Math.floor(hunter.stats.longestAetherAssaultSecondsAtBest % 60)).padStart(2, "0")}`; const metrics: { id: ProfileStatId; label: string; value: string; copy: string }[] = activeSection === "roguelike" ? [{ id: "roguelike", label: "Highest round", value: hunter.stats.highestRoguelikeRound.toLocaleString(), copy: "Best run before defeat" }] : activeSection === "rogue-trials" ? [{ id: "rogue-trials-endless", label: "Endless best", value: hunter.stats.highestRogueTrialsEndlessKills.toLocaleString(), copy: "Bosses defeated in one run" }] : activeSection === "hockey" ? [{ id: "hockey-healing", label: "Hockey record", value: `${hunter.stats.highestHockeyHealingReturns} returns`, copy: `${hockeyDuration} longest survival` }] : activeSection === "hockey-pvp" ? [ { id: "hockey-pvp-wins", label: "Match record", value: `${hunter.stats.hockeyHealingPvpWins}W · ${hunter.stats.hockeyHealingPvpLosses}L`, copy: "Lifetime PVP results" }, { id: "hockey-pvp-boss-kills", label: "Boss race kills", value: hunter.stats.hockeyHealingPvpBossKills.toLocaleString(), copy: "Lifetime PVP bosses" }, ] : activeSection === "aether-assault" ? [{ id: "aether-assault", label: "Overall score", value: hunter.stats.highestAetherAssaultScore.toLocaleString(), copy: `Wave ${hunter.stats.highestAetherAssaultWaveAtBest} · ${aetherDuration}` }] : [ { id: "blockbreaker-score", label: "Overall score", value: hunter.stats.highestBlockbreakerScore.toLocaleString(), copy: "Highest single-run score" }, { id: "blockbreaker-bricks", label: "Bricks broken", value: hunter.stats.highestBlockbreakerBricks.toLocaleString(), copy: "Most in one run" }, { id: "blockbreaker-time", label: "Time survived", value: blockbreakerDuration, copy: "Longest run" }, ]; const selectedStatValue = selectedStat === "roguelike" ? hunter.stats.highestRoguelikeRound : selectedStat === "rogue-trials-endless" ? hunter.stats.highestRogueTrialsEndlessKills : selectedStat === "hockey-healing" ? hunter.stats.highestHockeyHealingReturns : selectedStat === "hockey-pvp-wins" ? hunter.stats.hockeyHealingPvpWins : selectedStat === "hockey-pvp-boss-kills" ? hunter.stats.hockeyHealingPvpBossKills : selectedStat === "blockbreaker-bricks" ? hunter.stats.highestBlockbreakerBricks : selectedStat === "blockbreaker-time" ? hunter.stats.longestBlockbreakerSeconds : selectedStat === "blockbreaker-score" ? hunter.stats.highestBlockbreakerScore : selectedStat === "aether-assault" ? hunter.stats.highestAetherAssaultScore : isBossProfileStat(selectedStat) ? hunter.stats.bossKills[selectedStat] ?? 0 : 0; const selectedStatLabel = selectedStat === "roguelike" ? "Roguelike rounds" : selectedStat === "rogue-trials-endless" ? "Rogue Trials endless kills" : selectedStat === "hockey-healing" ? "Hockey Healing returns" : selectedStat === "hockey-pvp-wins" ? "Healing Hockey PVP wins" : selectedStat === "hockey-pvp-boss-kills" ? "Healing Hockey PVP boss kills" : selectedStat === "blockbreaker-bricks" ? "Blockbreaker bricks broken" : selectedStat === "blockbreaker-time" ? "Blockbreaker survival time" : selectedStat === "blockbreaker-score" ? "Blockbreaker overall score" : selectedStat === "aether-assault" ? "Aether Assault score" : isBossProfileStat(selectedStat) ? BOSS_DEFINITIONS[selectedStat].name : "Hunter record"; const leaderboardValue = (value: number, secondaryValue?: number) => selectedStat === "hockey-healing" ? `${value} · ${Math.floor((secondaryValue ?? 0) / 60)}:${String(Math.floor((secondaryValue ?? 0) % 60)).padStart(2, "0")}` : selectedStat === "hockey-pvp-wins" ? `${value}W · ${secondaryValue ?? 0}L` : selectedStat === "blockbreaker-time" ? `${Math.floor(value / 60)}:${String(Math.floor(value % 60)).padStart(2, "0")}` : selectedStat === "aether-assault" ? `${value.toLocaleString()} · Wave ${secondaryValue ?? 0}` : value.toLocaleString(); const selectedStatSummary = selectedStat === "hockey-healing" ? `${selectedStatValue} returns · ${hockeyDuration}` : selectedStat === "hockey-pvp-wins" ? `${hunter.stats.hockeyHealingPvpWins}W · ${hunter.stats.hockeyHealingPvpLosses}L` : selectedStat === "blockbreaker-time" ? `${Math.floor(selectedStatValue / 60)}:${String(Math.floor(selectedStatValue % 60)).padStart(2, "0")} survived` : selectedStat === "blockbreaker-score" ? `${selectedStatValue.toLocaleString()} points` : selectedStat === "aether-assault" ? `${selectedStatValue.toLocaleString()} points · Wave ${hunter.stats.highestAetherAssaultWaveAtBest}` : selectedStat === "blockbreaker-bricks" ? `${selectedStatValue.toLocaleString()} bricks` : `${selectedStatValue.toLocaleString()} ${selectedStat === "roguelike" ? "round" : "kills"}`; const selectedSecondaryValue = selectedStat === "hockey-healing" ? hunter.stats.longestHockeyHealingSecondsAtBest : selectedStat === "hockey-pvp-wins" ? hunter.stats.hockeyHealingPvpLosses : selectedStat === "aether-assault" ? hunter.stats.highestAetherAssaultWaveAtBest : undefined; const leaderboardMeta = leaderboardStatus || (leaderboardUpdatedAt ? `Cached ${formatSaveTimestamp(leaderboardUpdatedAt)}` : "Offline cache empty"); return (
Hunter profile

Records

setCollectionView("stats")}>Records setCollectionView("loot")}>Group Loot
navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
{profileView === "loot" ? <>
Shared group drops · Core: {collection.coreMechanic}

Group {collection.groupLetter} · {collection.groupName}

{earned} / {collection.drops.length} discovered
{collection.drops.map((drop) => (
{drop.icon}{drop.count} {drop.rarity}{drop.name}

{drop.count ? `${drop.count} earned` : collection.defeated ? "Not yet earned" : `Defeat a Group ${collection.groupLetter} boss`}

{drop.chance}{drop.itemLevel ? ` · iLvl ${drop.itemLevel}` : ""}
))}
Boss pets live in the Bosses record grid.Every boss, kill count, and pet status stays together.
: <>
Lifetime records · Controller-first collection log

Hunter Records

{petsOwned} / {bosses.length} boss pets found
{PROFILE_SECTIONS.map((section) => setSelectedStat(defaultStatForSection(section.id, bosses))}>{section.icon}{section.label}{section.copy})}
{activeSection === "bosses" ?
Boss pets · A–ZEvery guardian{hunter.stats.totalBossKills.toLocaleString()} total kills
{bosses.map((boss) => { const owned = boss.pet.count > 0; return setSelectedStat(boss.bossId)}> Kills{boss.kills.toLocaleString()} {boss.bossName} {owned ? `Pet owned${boss.pet.count > 1 ? ` ×${boss.pet.count}` : ""}` : "Pet not found"} ; })}
:
All {activeSectionDefinition.label} stats{activeSectionDefinition.copy}{metrics.length} record{metrics.length === 1 ? "" : "s"}
{metrics.map((metric) => setSelectedStat(metric.id)}>{metric.label}{metric.value}{metric.copy})}
{leaderboardMeta}{selectedStatLabel}
{selectedStatSummary} { void refreshLeaderboard(); }}>{refreshingLeaderboard ? "Refreshing…" : "Refresh online"}
{leaderboard ?
{leaderboard.top.length ? leaderboard.top.slice(0, 4).map((entry) =>
#{entry.rank}{entry.hunterName}{entry.username}{leaderboardValue(entry.value, entry.secondaryValue)}
) :
No ranked hunters yet.
}
:
{leaderboardStatus || "No cached rankings."}
}
{leaderboard?.current ? `#${leaderboard.current.rank}` : "—"}Your rank · {hunter.hunterName}{leaderboardOwner ?? "Offline hunter"}{leaderboardValue(selectedStatValue, selectedSecondaryValue)}
}
{activeSection === "bosses" ? "♛" : "◆"}{activeSection === "bosses" ? "Move through the grid to inspect a boss below." : "Every stat in this section stays visible together."}{activeSection === "bosses" ? "The lower screen shows kills, pet ownership, and collection details." : "Select a record card to change the online leaderboard."}
} } bottom={ {profileView === "stats" && activeSection === "bosses" && selectedBoss ? <>
Boss record · {selectedBoss.bossName}{selectedBoss.pet.count > 0 ? "PET OWNED" : "PET NOT FOUND"}
0 ? "is-owned" : "is-unowned"}`}>{selectedBoss.pet.icon}Boss pet icon
{BOSS_DEFINITIONS[selectedBoss.bossId].title}

{selectedBoss.bossName}

{BOSS_DEFINITIONS[selectedBoss.bossId].summary}

Times killed{selectedBoss.kills.toLocaleString()} Boss pet{selectedBoss.pet.count > 0 ? "Owned" : "Not found"} Pet copies{selectedBoss.pet.count.toLocaleString()} Drop chance{selectedBoss.pet.chance}
Encounter read{BOSS_DEFINITIONS[selectedBoss.bossId].briefing}Move on upper-screen grid to inspect another boss.
: <>
{hunter.hunterName} · {activeHealer.name} statsLEVEL {activeProgress.level}
Total boss kills{hunter.stats.totalBossKills} Flawless clears{hunter.stats.flawlessClears} Allies saved{hunter.stats.alliesSaved} Healing done{hunter.stats.healingDone.toLocaleString()} Highest roguelike round{hunter.stats.highestRoguelikeRound} Endless best{hunter.stats.highestRogueTrialsEndlessKills} Hockey PVP record{hunter.stats.hockeyHealingPvpWins}W · {hunter.stats.hockeyHealingPvpLosses}L Boss pets found{petsOwned}/{bosses.length}
{profileView === "loot" &&
Mechanic groups{collections.map((group) => ( setGroupId(group.groupId)}> {group.defeated ? group.groupLetter : "?"}Group {group.groupLetter} · {group.groupName}{group.bosses.reduce((sum, boss) => sum + boss.kills, 0)} kills · {group.coreMechanic}{group.drops.filter((drop) => drop.count > 0).length}/{group.drops.length} ))}
} }
} /> ); } function GearScreen() { const hunter = useActiveHunter(); const navigate = useFrontendStore((state) => state.navigate); const notice = useFrontendStore((state) => state.notice); const selectedOwnerId = useFrontendStore((state) => state.selectedGearOwnerId); const selectedSlotId = useFrontendStore((state) => state.selectedGearSlotId); const workshopMode = useFrontendStore((state) => state.gearWorkshopMode); const selectedInfusionId = useFrontendStore((state) => state.selectedInfusionId); const selectedPassiveAbilityId = useFrontendStore((state) => state.selectedPassiveAbilityId); const selectedPassiveInfusionId = useFrontendStore((state) => state.selectedPassiveInfusionId); const selectOwner = useFrontendStore((state) => state.selectGearOwner); const selectSlot = useFrontendStore((state) => state.selectGearSlot); const selectWorkshopMode = useFrontendStore((state) => state.selectGearWorkshopMode); const selectInfusion = useFrontendStore((state) => state.selectInfusion); const selectPassiveAbility = useFrontendStore((state) => state.selectPassiveAbility); const selectPassiveInfusion = useFrontendStore((state) => state.selectPassiveInfusion); const upgrade = useFrontendStore((state) => state.upgradeSelectedGear); const installInfusion = useFrontendStore((state) => state.equipSelectedInfusion); const installPassive = useFrontendStore((state) => state.equipPassiveInfusion); const slot = hunter?.gearProgress[selectedOwnerId].slots[selectedSlotId]; const upgradeReadiness = useMemo(() => { const owners = new Set(); const slots = new Set(); if (!hunter) return { owners, slots }; for (const ownerId of GEAR_OWNER_ORDER) { for (const slotId of GEAR_SLOT_ORDER) { if (!canUpgradeGearSlot(hunter.gearProgress, hunter.materials, ownerId, slotId)) continue; owners.add(ownerId); slots.add(`${ownerId}:${slotId}`); } } return { owners, slots }; }, [hunter]); const recipe = GEAR_RECIPES[selectedOwnerId][selectedSlotId]; const costs = hunter && slot ? gearUpgradeCosts(selectedOwnerId, selectedSlotId, slot.level) : []; const canUpgrade = upgradeReadiness.slots.has(`${selectedOwnerId}:${selectedSlotId}`); const infusionChoices = infusionsForOwner(selectedOwnerId); const selectedInfusion = infusionChoices.find((choice) => choice.id === selectedInfusionId) ?? infusionChoices[0]; const selectedInfusionCosts = hunter ? infusionCosts(selectedOwnerId, selectedSlotId, selectedInfusion.id) : []; const activeUnlocked = Boolean(hunter && activeInfusionUnlocked(hunter.gearProgress[selectedOwnerId])); const anchorUnlocked = Boolean(slot && slot.level >= ACTIVE_INFUSION_MIN_GEAR_LEVEL); const infusionEquipped = hunter?.gearProgress[selectedOwnerId].infusionAbilityId === selectedInfusion.id; const canInstallInfusion = Boolean(hunter && activeUnlocked && anchorUnlocked && !infusionEquipped && canAffordGearUpgrade(hunter.materials, selectedInfusionCosts)); const passiveUnlocked = Boolean(hunter && passiveInfusionUnlocked(hunter.gearProgress)); const healerOwner = isHealerClassId(selectedOwnerId); const passiveHealerClassId = isHealerClassId(selectedOwnerId) ? selectedOwnerId : "priest"; const passiveChoices = PASSIVE_INFUSIONS.filter((passive) => passive.abilitySlotId === selectedPassiveAbilityId); const selectedPassive = RUN_BUFFS[selectedPassiveInfusionId]; const healerAbilities = HEALER_CLASSES[passiveHealerClassId].abilities; const previewEntryId = workshopMode === "upgrade" ? "upgrade" : `infusion-${infusionChoices[0].id}`; const actions = useMemo(() => [ ...GEAR_OWNER_ORDER.map((ownerId, index) => ({ id: `owner-${ownerId}`, run: () => selectOwner(ownerId), neighbors: { up: index === 0 ? "back" : `owner-${GEAR_OWNER_ORDER[index - 1]}`, down: index === GEAR_OWNER_ORDER.length - 1 ? previewEntryId : `owner-${GEAR_OWNER_ORDER[index + 1]}`, right: `slot-${selectedSlotId}`, }, })), ...GEAR_SLOT_ORDER.map((slotId, index) => ({ id: `slot-${slotId}`, run: () => selectSlot(slotId), neighbors: { up: index === 0 ? "workshop-upgrade" : `slot-${GEAR_SLOT_ORDER[index - 1]}`, down: index === GEAR_SLOT_ORDER.length - 1 ? previewEntryId : `slot-${GEAR_SLOT_ORDER[index + 1]}`, left: `owner-${selectedOwnerId}`, right: previewEntryId, }, })), { id: "workshop-upgrade", run: () => selectWorkshopMode("upgrade"), neighbors: { right: "workshop-infusion", down: "slot-weapon" } }, { id: "workshop-infusion", run: () => selectWorkshopMode("infusion"), neighbors: { left: "workshop-upgrade", down: `infusion-${infusionChoices[0].id}` } }, ...infusionChoices.map((infusion, index) => ({ id: `infusion-${infusion.id}`, run: () => selectInfusion(infusion.id), neighbors: { up: index === 0 ? "workshop-infusion" : `infusion-${infusionChoices[index - 1].id}`, down: index === infusionChoices.length - 1 ? (healerOwner ? `passive-ability-${selectedPassiveAbilityId}` : "install-infusion") : `infusion-${infusionChoices[index + 1].id}`, left: `slot-${selectedSlotId}`, right: index === infusionChoices.length - 1 ? "install-infusion" : undefined, }, })), ...(healerOwner ? ABILITY_ORDER.map((abilityId, index) => ({ id: `passive-ability-${abilityId}`, run: () => selectPassiveAbility(abilityId), neighbors: { up: index < 3 ? `infusion-${infusionChoices[infusionChoices.length - 1].id}` : `passive-ability-${ABILITY_ORDER[index - 3]}`, down: index < 3 ? `passive-ability-${ABILITY_ORDER[index + 3]}` : `passive-${passiveChoices[Math.min(index - 3, passiveChoices.length - 1)].id}`, left: index % 3 > 0 ? `passive-ability-${ABILITY_ORDER[index - 1]}` : `slot-${selectedSlotId}`, right: index % 3 < 2 ? `passive-ability-${ABILITY_ORDER[index + 1]}` : undefined, }, })) : []), ...(healerOwner ? passiveChoices.map((passive, index) => ({ id: `passive-${passive.id}`, run: () => { selectPassiveInfusion(passive.id); installPassive(passive.id); }, enabled: passiveUnlocked, neighbors: { up: index === 0 ? `passive-ability-${selectedPassiveAbilityId}` : `passive-${passiveChoices[index - 1].id}`, down: index === passiveChoices.length - 1 ? `passive-ability-${selectedPassiveAbilityId}` : `passive-${passiveChoices[index + 1].id}`, left: `slot-${selectedSlotId}`, }, })) : []), { id: "upgrade", run: upgrade, enabled: canUpgrade, neighbors: { left: `slot-${selectedSlotId}`, up: `slot-${selectedSlotId}` } }, { id: "install-infusion", run: installInfusion, enabled: canInstallInfusion, neighbors: { left: `slot-${selectedSlotId}`, up: `infusion-${infusionChoices[infusionChoices.length - 1].id}`, down: healerOwner ? `passive-ability-${selectedPassiveAbilityId}` : undefined } }, { id: "back", run: () => navigate("home"), neighbors: { left: "workshop-infusion", down: `owner-${GEAR_OWNER_ORDER[0]}` } }, ], [canInstallInfusion, canUpgrade, healerOwner, infusionChoices, installInfusion, installPassive, navigate, passiveChoices, passiveUnlocked, previewEntryId, selectInfusion, selectOwner, selectPassiveAbility, selectPassiveInfusion, selectSlot, selectWorkshopMode, selectedOwnerId, selectedPassiveAbilityId, selectedSlotId, upgrade]); const controller = useMenuController(actions, { onBack: () => navigate("home") }); const passiveContext = workshopMode === "infusion" && healerOwner && controller.selectedId.startsWith("passive-"); if (!hunter || !slot) return null; const currentBonus = gearBonusText(recipe.statId, slot.level); const nextBonus = gearBonusText(recipe.statId, Math.min(MAX_GEAR_LEVEL, slot.level + 1)); return (
Group drop workshop

Gear & Infusions

selectWorkshopMode("upgrade")}>Upgrade selectWorkshopMode("infusion")}>Infusion
navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
{GEAR_OWNER_ORDER.map((ownerId) => { const highest = Math.max(...GEAR_SLOT_ORDER.map((slotId) => hunter.gearProgress[ownerId].slots[slotId].level)); const upgradeReady = upgradeReadiness.owners.has(ownerId); return selectOwner(ownerId)}>{GEAR_OWNER_LABELS[ownerId]}Highest slot +{highest}{ownerId === selectedOwnerId ? "✓" : ""}; })}
{GEAR_SLOT_ORDER.map((slotId) => { const progress = hunter.gearProgress[selectedOwnerId].slots[slotId]; const slotRecipe = GEAR_RECIPES[selectedOwnerId][slotId]; const upgradeReady = upgradeReadiness.slots.has(`${selectedOwnerId}:${slotId}`); return selectSlot(slotId)}>{slotId === "weapon" ? "⚔" : slotId === "helmet" ? "♙" : slotId === "chest" ? "▧" : slotId === "legs" ? "Ⅱ" : "⌁"}{GEAR_SLOT_LABELS[slotId]}{GEAR_STAT_LABELS[slotRecipe.statId]}+{progress.level}; })}
{workshopMode === "upgrade" ?
Selected upgrade

{GEAR_OWNER_LABELS[selectedOwnerId]} · {GEAR_SLOT_LABELS[selectedSlotId]} +{slot.level}

{GEAR_STAT_LABELS[recipe.statId]} from Group {BOSS_GROUP_BY_ID[recipe.primaryGroupId].letter} and Group {BOSS_GROUP_BY_ID[recipe.secondaryGroupId].letter} drops.

Current{currentBonus}{slot.level >= MAX_GEAR_LEVEL ? "Maximum" : `Rank +${slot.level + 1}`}{nextBonus}
:
Active infusion · unlock +{ACTIVE_INFUSION_MIN_GEAR_LEVEL}

{selectedInfusion.icon} {selectedInfusion.name}

{selectedInfusion.description} Anchor purchase to a +{ACTIVE_INFUSION_MIN_GEAR_LEVEL} slot.

{infusionChoices.map((infusion) => selectInfusion(infusion.id)}>{infusion.icon}{infusion.name}{infusion.description}{hunter.gearProgress[selectedOwnerId].infusionAbilityId === infusion.id ? "✓" : ""})}
{healerOwner &&
Passive blessing · global +{PASSIVE_INFUSION_MIN_GEAR_LEVEL}
{ABILITY_ORDER.map((abilityId) => selectPassiveAbility(abilityId)}>{healerAbilities[abilityId].shortName})}
{passiveChoices.map((passive) => selectPassiveInfusion(passive.id)} onClick={() => { selectPassiveInfusion(passive.id); installPassive(passive.id); }} >{passive.icon}{healerAbilities[passive.abilitySlotId].shortName}: {passive.name}{formatRunBuffEffect(passive.id, 1, healerAbilities[passive.abilitySlotId].shortName)}{hunter.gearProgress[selectedOwnerId].passiveInfusionId === passive.id ? "✓" : ""})}
}
}
} bottom={
{workshopMode === "upgrade" ? `${GEAR_SLOT_LABELS[selectedSlotId]} recipe` : passiveContext ? `${healerAbilities[selectedPassive.abilitySlotId].name}: ${selectedPassive.name}` : `${selectedInfusion.name} infusion`}{hunter.materials.reduce((sum, item) => sum + item.quantity, 0)} DROPS
{workshopMode === "upgrade" ? "Upgrade requirements" : passiveContext ? "Passive blessing · Rank 1" : `Infusion requirements · ${GEAR_SLOT_LABELS[selectedSlotId]} +${slot.level} anchor`} {passiveContext ?
{passiveUnlocked ? "✓" : "×"}{formatRunBuffEffect(selectedPassive.id, 1, healerAbilities[selectedPassive.abilitySlotId].shortName)}{selectedPassive.detail}{hunter.gearProgress[selectedOwnerId].passiveInfusionId === selectedPassive.id ? "EQUIPPED" : "RANK 1"}
: (workshopMode === "upgrade" ? costs : selectedInfusionCosts).length ? (workshopMode === "upgrade" ? costs : selectedInfusionCosts).map((cost) => { const owned = hunter.materials.find((item) => item.id === cost.itemId)?.quantity ?? 0; return
= cost.quantity ? "is-met" : "is-missing"} key={cost.itemId}>{owned >= cost.quantity ? "✓" : "×"}{cost.itemName}{owned} owned · {cost.quantity} needed{owned}/{cost.quantity}
; }) :
Maximum rank reachedNo more materials required.+{MAX_GEAR_LEVEL}
}
{workshopMode === "upgrade" ? {slot.level >= MAX_GEAR_LEVEL ? "Maximum rank" : `Upgrade to +${slot.level + 1}`}{canUpgrade ? "Spend group drops · autosave" : "Collect required group drops"} : passiveContext ?
{hunter.gearProgress[selectedOwnerId].passiveInfusionId === selectedPassive.id ? "Passive equipped" : `${DEFAULT_CONTROLLER_GLYPHS.confirm} · Equip selected passive`}Applies at rank 1 next encounter.
: {infusionEquipped ? "Infusion equipped" : `Install ${selectedInfusion.name}`}{!activeUnlocked ? `Raise any ${GEAR_OWNER_LABELS[selectedOwnerId]} slot to +${ACTIVE_INFUSION_MIN_GEAR_LEVEL}` : !anchorUnlocked ? `Select a +${ACTIVE_INFUSION_MIN_GEAR_LEVEL} anchor slot` : canInstallInfusion ? "Spend group drops · autosave" : infusionEquipped ? "Applies next encounter" : "Collect required group drops"}}
{notice || "Gear changes save locally and apply when next encounter starts."}
} /> ); } const APPEARANCE_PREVIEW_ANIMATIONS = [ { id: "idle", label: "Idle" }, { id: "walk", label: "Walk" }, { id: "cast", label: "Cast" }, ] as const; function appearanceControlId(slotId: AppearanceSlotId, direction: "previous" | "next") { return `appearance-${slotId}-${direction}`; } function AppearanceScreen() { const hunter = useActiveHunter(); const classId = useFrontendStore((state) => state.appearanceClassId); const drafts = useFrontendStore((state) => state.appearanceDrafts); const previewMode = useFrontendStore((state) => state.previewMode); const previewAnimation = useFrontendStore((state) => state.previewAnimation); const notice = useFrontendStore((state) => state.notice); const selectAppearanceClass = useFrontendStore((state) => state.selectAppearanceClass); const updateAppearanceDraft = useFrontendStore((state) => state.updateAppearanceDraft); const resetAppearanceDraft = useFrontendStore((state) => state.resetAppearanceDraft); const saveAppearanceDraft = useFrontendStore((state) => state.saveAppearanceDraft); const closeAppearanceLab = useFrontendStore((state) => state.closeAppearanceLab); const setAppearancePreviewMode = useFrontendStore((state) => state.setAppearancePreviewMode); const setAppearancePreviewAnimation = useFrontendStore((state) => state.setAppearancePreviewAnimation); const draft = drafts[classId] ?? createDefaultHealerAppearance(classId); const saved = hunter?.healers[classId].appearance ?? createDefaultHealerAppearance(classId); const dirtyClassIds = HEALER_CLASS_ORDER.filter((candidate) => !appearancesMatch( drafts[candidate] ?? createDefaultHealerAppearance(candidate), hunter?.healers[candidate].appearance ?? createDefaultHealerAppearance(candidate), )); const currentDirty = !appearancesMatch(draft, saved); const dirtyCount = dirtyClassIds.length; const [discardArmed, setDiscardArmed] = useState(false); const effectivePreviewMode = CHARACTER_MODEL_MODE === "legacy" ? "legacy" : previewMode; const changeSlot = useCallback((slotId: AppearanceSlotId, direction: -1 | 1) => { updateAppearanceDraft(cycleAppearanceSlot(draft, slotId, direction)); }, [draft, updateAppearanceDraft]); const requestClose = useCallback(() => { if (dirtyCount > 0 && !discardArmed) { setDiscardArmed(true); return; } closeAppearanceLab(); }, [closeAppearanceLab, dirtyCount, discardArmed]); useEffect(() => setDiscardArmed(false), [classId, drafts, previewAnimation, previewMode]); const actions = useMemo(() => { const classActions = HEALER_CLASS_ORDER.map((candidate, index) => ({ id: `appearance-class-${candidate}`, run: () => selectAppearanceClass(candidate), neighbors: { left: `appearance-class-${HEALER_CLASS_ORDER[(index - 1 + HEALER_CLASS_ORDER.length) % HEALER_CLASS_ORDER.length]}`, right: `appearance-class-${HEALER_CLASS_ORDER[(index + 1) % HEALER_CLASS_ORDER.length]}`, down: "appearance-animation-idle", }, })); const animationActions = APPEARANCE_PREVIEW_ANIMATIONS.map((animation, index) => ({ id: `appearance-animation-${animation.id}`, run: () => setAppearancePreviewAnimation(animation.id), neighbors: { left: `appearance-animation-${APPEARANCE_PREVIEW_ANIMATIONS[(index - 1 + APPEARANCE_PREVIEW_ANIMATIONS.length) % APPEARANCE_PREVIEW_ANIMATIONS.length].id}`, right: `appearance-animation-${APPEARANCE_PREVIEW_ANIMATIONS[(index + 1) % APPEARANCE_PREVIEW_ANIMATIONS.length].id}`, up: `appearance-class-${classId}`, down: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[0].id, index === 0 ? "previous" : "next"), }, })); const slotActions = APPEARANCE_SLOT_DEFINITIONS.flatMap((slot, index) => { const previousRow = index === 0 ? "appearance-animation-idle" : appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[index - 1].id, "previous"); const nextRow = index === APPEARANCE_SLOT_DEFINITIONS.length - 1 ? "appearance-compare" : appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[index + 1].id, "previous"); const previous = appearanceControlId(slot.id, "previous"); const next = appearanceControlId(slot.id, "next"); const enabled = appearanceSlotEnabled(draft, slot.id); return [ { id: previous, run: () => changeSlot(slot.id, -1), enabled, neighbors: { left: next, right: next, up: previousRow, down: nextRow } }, { id: next, run: () => changeSlot(slot.id, 1), enabled, neighbors: { left: previous, right: previous, up: index === 0 ? "appearance-animation-cast" : appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[index - 1].id, "next"), down: index === APPEARANCE_SLOT_DEFINITIONS.length - 1 ? "appearance-save" : appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[index + 1].id, "next") } }, ]; }); return [ ...classActions, ...animationActions, ...slotActions, { id: "appearance-compare", run: () => setAppearancePreviewMode(previewMode === "modular" ? "legacy" : "modular"), enabled: CHARACTER_MODEL_MODE !== "legacy", neighbors: { left: "appearance-close", right: "appearance-reset", up: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS.at(-1)!.id, "previous") } }, { id: "appearance-reset", run: resetAppearanceDraft, neighbors: { left: "appearance-compare", right: "appearance-save", up: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS.at(-1)!.id, "previous") } }, { id: "appearance-save", run: () => { saveAppearanceDraft(); }, neighbors: { left: "appearance-reset", right: "appearance-close", up: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS.at(-1)!.id, "next") } }, { id: "appearance-close", run: requestClose, neighbors: { left: "appearance-save", right: "appearance-compare", up: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS.at(-1)!.id, "next") } }, ]; }, [changeSlot, classId, draft, previewMode, requestClose, resetAppearanceDraft, saveAppearanceDraft, selectAppearanceClass, setAppearancePreviewAnimation, setAppearancePreviewMode]); const controller = useMenuController(actions, { initialId: `appearance-class-${classId}`, onBack: requestClose }); if (!hunter) return null; const healer = HEALER_CLASSES[classId]; const legacyActive = effectivePreviewMode === "legacy"; return (
Character workshop

Appearance Lab

{DEFAULT_CONTROLLER_GLYPHS.back} · {discardArmed ? "Confirm discard" : dirtyCount > 0 ? `Cancel (${dirtyCount} unsaved)` : "Close"}
Assembling shared rig
}>
{legacyActive ? "LEGACY WHOLE MODEL" : "MODULAR LIVE PREVIEW"} {legacyActive ? "Saved parts preserved but intentionally ignored" : `${previewAnimation} animation · shared Rig_Medium`}
{healer.specialization} {healer.name} {currentDirty ? "UNSAVED CHANGES" : "SAVED LOOK"}
{DEFAULT_CONTROLLER_GLYPHS.select} / TAB Open lower-screen controls Every choice previews on same animation skeleton
} bottom={
Appearance controls{dirtyCount > 0 ? `${dirtyCount} UNSAVED` : "SAVED LOCALLY"}
{HEALER_CLASS_ORDER.map((candidate) => { const candidateHealer = HEALER_CLASSES[candidate]; return selectAppearanceClass(candidate)} >{candidateHealer.icon}{candidateHealer.name}; })}
{APPEARANCE_PREVIEW_ANIMATIONS.map((animation) => setAppearancePreviewAnimation(animation.id)} >{animation.label})}
{APPEARANCE_SLOT_DEFINITIONS.map((slot) => { const active = controller.selectedId.startsWith(`appearance-${slot.id}-`); const enabled = appearanceSlotEnabled(draft, slot.id); return
{slot.label}{slot.assetNote} changeSlot(slot.id, -1)}>‹ {appearanceSlotLabel(draft, slot.id)} changeSlot(slot.id, 1)}>›
; })}
setAppearancePreviewMode(previewMode === "modular" ? "legacy" : "modular")}>{legacyActive ? "Show custom" : "Compare legacy"}{CHARACTER_MODEL_MODE === "legacy" ? "Rollout locked" : "No save change"} ResetClass default { saveAppearanceDraft(); }}>Save lookCurrent healer {discardArmed ? "Confirm" : "Cancel"}{discardArmed ? "Press again" : dirtyCount > 0 ? `Discard ${dirtyCount}` : "Close lab"}
0 ? "is-dirty" : ""}`}>{discardArmed ? `Discard ${dirtyCount} unsaved healer ${dirtyCount === 1 ? "look" : "looks"}? Press Cancel or Back again.` : CHARACTER_MODEL_MODE === "legacy" ? "Legacy rollout active. Modular choices remain saved for later." : currentDirty ? "Preview changed. Save look to use it in gameplay." : dirtyCount > 0 ? `${dirtyCount} other healer ${dirtyCount === 1 ? "look is" : "looks are"} still unsaved. Switch classes to save them.` : notice || "Saved look will load in the next encounter."}
} /> ); } function SettingToggle({ id, label, copy, value, selectedId, select, onClick }: { id: string; label: string; copy: string; value: boolean; selectedId: string; select: (id: string) => void; onClick: () => void }) { return {label}{copy}{value ? "ON" : "OFF"}; } function SettingsScreen() { const settings = useFrontendStore((state) => state.settings); const updateSetting = useFrontendStore((state) => state.updateSetting); const navigate = useFrontendStore((state) => state.navigate); const notice = useFrontendStore((state) => state.notice); const actions = useMemo(() => [ { id: "volume-down", run: () => updateSetting("masterVolume", Math.max(0, settings.masterVolume - 10)) }, { id: "volume-up", run: () => updateSetting("masterVolume", Math.min(100, settings.masterVolume + 10)) }, { id: "motion", run: () => updateSetting("reducedMotion", !settings.reducedMotion) }, { id: "numbers", run: () => updateSetting("damageNumbers", !settings.damageNumbers) }, { id: "text", run: () => updateSetting("largeText", !settings.largeText) }, { id: "back", run: () => navigate("home") }, ], [navigate, settings, updateSetting]); const controller = useMenuController(actions, { onBack: () => navigate("home") }); return (
Field configuration

Settings

navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
Audio
Master volumeAll music, effects, and voice
updateSetting("masterVolume", Math.max(0, settings.masterVolume - 10))}>−{settings.masterVolume}% updateSetting("masterVolume", Math.min(100, settings.masterVolume + 10))}>+
Display & accessibility updateSetting("reducedMotion", !settings.reducedMotion)} /> updateSetting("damageNumbers", !settings.damageNumbers)} /> updateSetting("largeText", !settings.largeText)} />
{notice || "Settings write to offline storage immediately."}
} bottom={
ControllerBUILT-IN THOR PAD
{DEFAULT_CONTROLLER_GLYPHS.faceTop}{DEFAULT_CONTROLLER_GLYPHS.faceLeft}{DEFAULT_CONTROLLER_GLYPHS.faceRight}{DEFAULT_CONTROLLER_GLYPHS.faceBottom}
{DEFAULT_CONTROLLER_GLYPHS.confirm} Confirm / cast Purify{DEFAULT_CONTROLLER_GLYPHS.back} Back / cast ShieldD-Pad Navigate / target partyRight stick Rotate camera{DEFAULT_CONTROLLER_GLYPHS.start} Pause / menu
No app focus requiredNative controller input routes through app-level actions.
} /> ); } function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], difficultySlug?: (typeof DIFFICULTIES)[number]["slug"], hockeyPvpMatch?: HockeyPvpMatchConfig) => void }) { const hunter = useActiveHunter(); const accountId = useFrontendStore((state) => state.accountId); const modeId = useFrontendStore((state) => state.selectedMode); const selectedBossId = useFrontendStore((state) => state.selectedBossId); const selectedDifficultySlug = useFrontendStore((state) => state.selectedDifficultySlug); const selectBoss = useFrontendStore((state) => state.selectBoss); const selectDifficulty = useFrontendStore((state) => state.selectDifficulty); const navigate = useFrontendStore((state) => state.navigate); const [message, setMessage] = useState(""); const [queueing, setQueueing] = useState(false); const [queueElapsed, setQueueElapsed] = useState(0); const queueActive = useRef(false); const queueOperation = useRef(null); const mode = MODE_COPY[modeId]; const healer = hunter ? HEALER_CLASSES[hunter.activeClassId] : HEALER_CLASSES.priest; const progress = hunter?.healers[hunter.activeClassId]; const selectedBoss = BOSS_DEFINITIONS[selectedBossId]; const selectedDifficulty = DIFFICULTY_BY_SLUG[selectedDifficultySlug]; const isPve = modeId === "roguelike-pve"; const isRogueTrials = modeId === "rogue-trials"; const isPveRun = isPve || isRogueTrials; const isDungeon = modeId === "dungeons"; const isHockey = modeId === "hockey-healing"; const isHockeyPvp = modeId === "hockey-healing-pvp"; const isBlockbreaker = modeId === "blockbreaker"; const isAetherAssault = modeId === "aether-assault"; const selectedBossGroup = BOSS_GROUP_BY_ID[selectedBoss.groupId]; const visibleBossIds = selectedBossGroup.bossIds; const bossGridColumns = Math.min(2, visibleBossIds.length); const selectBossGroup = (groupId: (typeof BOSS_GROUPS)[number]["id"]) => { selectBoss(BOSS_GROUP_BY_ID[groupId].bossIds[0]); }; const completePvpQueue = (match: HockeyPvpMatchConfig) => { if (!queueActive.current) return; queueActive.current = false; queueOperation.current = null; setQueueing(false); setMessage(match.role === "cpu" ? `CPU rival found: ${match.opponentName}.` : `Matched with ${match.opponentName}.`); onLaunch([hockeyPvpBossAt(match.seed, 0)], "initiate", match); }; const cancelPvpQueue = () => { if (!queueActive.current) return; queueActive.current = false; queueOperation.current?.cancel(); queueOperation.current = null; setQueueing(false); setQueueElapsed(0); setMessage("Matchmaking cancelled."); }; const startPvpQueue = async () => { if (!hunter || queueActive.current) return; queueActive.current = true; setQueueing(true); setQueueElapsed(0); setMessage(accountId ? "Searching online queue…" : "Offline queue: searching before CPU fallback…"); const operation = startHockeyPvpMatchmaking({ slotId: hunter.slotId, hunterName: hunter.hunterName, online: Boolean(accountId && networkAppearsOnline()), onElapsed: setQueueElapsed, onOnlineUnavailable: () => setMessage("Online queue unavailable. CPU fallback still searching…"), }); queueOperation.current = operation; const match = await operation.result; if (!match || queueOperation.current !== operation) return; completePvpQueue(match); }; useEffect(() => () => { queueActive.current = false; queueOperation.current?.cancel(); queueOperation.current = null; }, []); const leaveMode = () => { cancelPvpQueue(); navigate("home"); }; const launch = () => { if (isPveRun) return onLaunch(selectRandomBossPair(), "initiate"); if (isHockey) return onLaunch(selectRandomBossPair(), "initiate"); if (isBlockbreaker) return onLaunch(selectRandomBossPair(), "initiate"); if (isAetherAssault) return onLaunch(selectRandomBossPair(), "initiate"); if (isHockeyPvp) return queueing ? cancelPvpQueue() : void startPvpQueue(); if (isDungeon) return onLaunch([selectedBossId], selectedDifficultySlug); setMessage("Online matchmaking is not available for this mode yet."); }; const actions = useMemo(() => [ ...(isDungeon ? BOSS_GROUPS.map((group, index) => { const groupColumns = 5; const row = Math.floor(index / groupColumns); const column = index % groupColumns; const groupAt = (targetRow: number, targetColumn: number) => BOSS_GROUPS[targetRow * groupColumns + targetColumn]; return { id: `boss-group-${group.id}`, run: () => selectBossGroup(group.id), neighbors: { up: row > 0 ? `boss-group-${groupAt(row - 1, column)?.id}` : "back", down: groupAt(row + 1, column) ? `boss-group-${groupAt(row + 1, column)?.id}` : group.id === selectedBossGroup.id ? `boss-${selectedBossGroup.bossIds[0]}` : undefined, left: column > 0 ? `boss-group-${groupAt(row, column - 1)?.id}` : undefined, right: groupAt(row, column + 1) ? `boss-group-${groupAt(row, column + 1)?.id}` : undefined, }, }; }) : []), ...(isDungeon ? visibleBossIds.map((bossId, index) => { const row = Math.floor(index / bossGridColumns); const column = index % bossGridColumns; const bossAt = (targetRow: number, targetColumn: number) => visibleBossIds[targetRow * bossGridColumns + targetColumn]; return { id: `boss-${bossId}`, run: () => selectBoss(bossId), neighbors: { up: row > 0 ? `boss-${bossAt(row - 1, column)}` : `boss-group-${selectedBossGroup.id}`, down: bossAt(row + 1, column) ? `boss-${bossAt(row + 1, column)}` : `difficulty-${DIFFICULTIES[0].slug}`, left: column > 0 ? `boss-${bossAt(row, column - 1)}` : undefined, right: bossAt(row, column + 1) ? `boss-${bossAt(row, column + 1)}` : undefined, }, }; }) : []), ...(isDungeon ? DIFFICULTIES.map((difficulty, index) => ({ id: `difficulty-${difficulty.slug}`, run: () => selectDifficulty(difficulty.slug), neighbors: { left: index > 0 ? `difficulty-${DIFFICULTIES[index - 1].slug}` : `boss-${selectedBossId}`, right: index < DIFFICULTIES.length - 1 ? `difficulty-${DIFFICULTIES[index + 1].slug}` : "launch", up: `boss-${selectedBossId}`, down: "launch", }, })) : []), { id: "launch", run: launch, neighbors: isDungeon ? { up: `difficulty-${DIFFICULTIES[DIFFICULTIES.length - 1].slug}` } : { up: "back" } }, { id: "back", run: leaveMode, neighbors: isDungeon ? { down: `boss-group-${selectedBossGroup.id}` } : { down: "launch" } }, ], [bossGridColumns, isAetherAssault, isBlockbreaker, isDungeon, isHockey, isHockeyPvp, isPveRun, modeId, navigate, onLaunch, queueing, selectBoss, selectDifficulty, selectedBossGroup, selectedBossId, selectedDifficultySlug, visibleBossIds]); const controller = useMenuController(actions, { onBack: leaveMode }); const launchLabel = isRogueTrials ? "Begin Rogue Trials" : isPve ? "Begin RPG Roguelike" : isHockey ? "Begin Hockey Healing" : isBlockbreaker ? "Begin Blockbreaker" : isAetherAssault ? "Begin Aether Assault" : isHockeyPvp ? queueing ? "Cancel matchmaking" : "Enter online queue" : isDungeon ? `Challenge ${selectedBoss.name}` : "Enter matchmaking"; const contextRules = isDungeon ? [ [selectedBoss.name, selectedBoss.summary], [bossMechanicName(selectedBoss.mechanicIds[0]), selectedBoss.briefing], [bossMechanicName(selectedBoss.mechanicIds[1]), "Controller-ready party behavior and full lower-display support."], ] : isAetherAssault ? [ ["Movement is the only arcade input", "Spellfire launches automatically down the five runway lanes while every healing and targeting control stays unchanged."], ["Formation pressure", "Eight arcane ships enter the first wave. Later formations grow to twenty, add armor, fire faster, and peel into diving attacks."], ["Heal through every hit", "Ship bolts and dive collisions damage only the healer. Endless bosses keep attacking the full party until the formation falls."], ] : isBlockbreaker ? [ ["Break linked colors", "Aim the puck into five-column rows. A hit removes its full orthogonally connected color cluster."], ["Accelerating wall", "Rows begin every 10 seconds, accelerate 10% each minute, and push survivors toward the danger line."], ["Heal under pressure", `Two bosses attack without pause. Each brick-wall breach deals ${BLOCKBREAKER_BREACH_DAMAGE} damage to every party member. The run ends when all four allies fall.`], ] : isHockeyPvp ? [ ["Normalized rivals", "Both parties use default base gear with all upgrade and infusion bonuses disabled. Boss order remains identical."], ["Escalating pressure", `Every boss kill by either party adds 5% global healing Dampening. Every net breach still deals ${HOCKEY_PVP_GOAL_DAMAGE} partywide damage.`], ["Online or CPU", "Queue searches online for five seconds. If no rival answers, a randomly named CPU healer takes far goal."], ] : isHockey ? [ ["Wide goal defense", "Healer owns near half. Intercept every incoming puck before it reaches the wide blue goal."], ["Pong rally", "Held left-stick direction controls return angle. Moving enemy paddle tracks the puck and strikes it back."], ["Unbroken boss fight", "Party fights two bosses on enemy half. Every kill awards loot and pet chance before replacement arrives."], ] : isRogueTrials ? [ ["Four dual rounds", "Clear four randomized pairs while drafting one stacking buff after each win."], ["Unseen trio finale", "Round 5 selects three bosses that have not appeared earlier in that run."], ["Endless choice", "After the trio falls, quit with the clear or continue while every dead boss is replaced."], ] : isPve ? [ ["Draft every run", "Choose four companions from three five-card waves, then build a six-slot spellbook from every enabled healer class."], ["Challenge hallways", "Brickbreaker, Hockey, and Aether Assault objectives connect boss rooms. Repeats raise their targets; failure still advances."], ["Run-only growth", "Boss chests upgrade owned spells, companions, or +0–+5 gear. Shop after each three-boss act, then face a finale."], ] : [ ["Draft a healing path", "Choose rites after every completed room."], ["Protect the formation", "Boss pressure changes around your build."], ["Bank collection drops", "Earned boss loot writes to active offline save."], ]; return (
Game mode

{mode.title}

{DEFAULT_CONTROLLER_GLYPHS.back} · Back
{!isDungeon &&
{mode.eyebrow}

{mode.title}

{mode.description}

{mode.detail}
} {isDungeon && (
Choose a mechanic group
{BOSS_GROUPS.map((group) => ( selectBossGroup(group.id)} > {group.letter}Group {group.letter}{group.name} ))}
Group {selectedBossGroup.letter} · {selectedBossGroup.name}{selectedBossGroup.coreMechanic} mechanics · {visibleBossIds.length} guardians
{visibleBossIds.map((bossId) => { const boss = BOSS_DEFINITIONS[bossId]; return ( selectBoss(bossId)} > {boss.icon}{boss.name}{boss.mechanicIds.filter((id) => !bossMechanicIsPassive(id)).map(bossMechanicName).join(" · ")}{selectedBossId === bossId ? "✓" : ""} ); })}
)} {isDungeon && (
Difficulty {DIFFICULTIES.map((difficulty) => selectDifficulty(difficulty.slug)}>{difficulty.name}iLvl {difficulty.itemLevel})}
)} {launchLabel}{queueing ? `CPU fallback in ${Math.max(0, ((HOCKEY_PVP_QUEUE_TIMEOUT_MS - queueElapsed) / 1000)).toFixed(1)}s` : `${mode.status} · ${DEFAULT_CONTROLLER_GLYPHS.confirm}`} {message &&
{message}
} } bottom={
Run preparation{isDungeon ? selectedDifficulty.name.toUpperCase() : mode.status.toUpperCase()}
{contextRules.map(([title, copy], index) =>
0{index + 1}{title}{copy}
)}
Equipped role{healer.specialization} · Level {progress?.level ?? 1}{isHockeyPvp ? "6 abilities · Base gear normalized · Controller ready" : `6 abilities · ${progress?.inventory.length ?? 0} class items · Controller ready`}
{isDungeon &&
Guaranteed reward{bossGroupDrop(selectedBossId, selectedDifficultySlug).name}1–3 group drops · {selectedDifficulty.rarity} · Pet chance 1 in 500
} {isHockey &&
Every boss killNormal boss loot awardedGuaranteed 1–3 group drops · Independent pet chance 1 in 500
} {isBlockbreaker &&
Ranked recordsOverall score · Bricks · Survival10 points per brick ladder · +0.1× every 30 seconds
} {isAetherAssault &&
Ranked recordOverall score · Wave at bestKill streak raises multiplier · Ship damage resets it
} {isHockeyPvp &&
Ranked recordsWins / losses · Lifetime PVP boss killsOnline leaderboards publish through active hunter save
}
} /> ); } export function FrontEnd({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], difficultySlug?: (typeof DIFFICULTIES)[number]["slug"], hockeyPvpMatch?: HockeyPvpMatchConfig) => void }) { const screen = useFrontendStore((state) => state.screen); if (screen === "login") return ; if (screen === "saves") return ; if (screen === "home") return ; if (screen === "class-help") return ; if (screen === "profile") return ; if (screen === "gear") return ; if (screen === "appearance") return ; if (screen === "settings") return ; if (screen === "mode") return ; return null; }