import { useEffect } from "react"; import { useShellStore } from "../app/shellStore"; import { useOnlineSessionStore } from "../app/onlineSessionStore"; import { useOnlineGroupStore } from "../app/onlineGroupStore"; import { activeDungeonCompletedObjectiveIds, subscribeDungeonEncounterSnapshots } from "./dungeonEncounterRuntime"; import { useCombatStore } from "./combatStore"; import { usePartyStore } from "./partyStore"; import { useGameStore } from "./store"; import { useDungeonRunStore } from "./dungeonRunStore"; import type { DungeonRunMember } from "./dungeonRunTypes"; function currentRunParty(): readonly DungeonRunMember[] { const shell = useShellStore.getState(); const character = shell.activeCharacter; if (!character) return []; const members: DungeonRunMember[] = [{ name: character.name, classId: character.classId, level: useCombatStore.getState().level, role: shell.activeDungeonRole, npc: false, ...(character.secondaryClassId ? { secondaryClassId: character.secondaryClassId } : {}) }]; const online = useOnlineSessionStore.getState().snapshot; for (const player of online?.players ?? []) { if (player.accountId === shell.session?.ownerId || !player.character) continue; members.push({ name: player.character.name, classId: player.character.classId, level: player.character.level, role: player.role ?? "damage", npc: false }); } for (const member of usePartyStore.getState().members) { members.push({ name: member.name, classId: member.classId, level: member.level, role: member.role, npc: true }); } return members.slice(0, 5); } /** Wall-clock run timing survives wipes, pauses, checkpoints, and renderer retries. */ export function DungeonRunBridge() { useEffect(() => { let announcedId: string | null = null; const advance = () => { const game = useGameStore.getState(); const shell = useShellStore.getState(); if (shell.phase !== "game" || game.gameMode !== "dungeon" || game.gmTestMode || !["ready", "proxy"].includes(game.assetStatus)) return; const run = useDungeonRunStore.getState(); if (run.progress?.dungeonId !== game.activeDungeonId) return; const party = currentRunParty(); const activity = useOnlineGroupStore.getState().activeActivity; if (activity?.type === "dungeon" && activity.contentId === game.activeDungeonId && run.progress.finishedAt === null && run.progress.id !== activity.id) { useDungeonRunStore.setState({ progress: { ...run.progress, id: activity.id, startedAt: activity.startedAt, party } }); } run.start(Date.now(), party); run.advance(activeDungeonCompletedObjectiveIds(game.activeDungeonId), Object.values(useCombatStore.getState().mobs).filter((mob) => mob.dead && !mob.despawned).map((mob) => mob.id), Date.now(), party); const progress = useDungeonRunStore.getState().progress; if (progress?.finishedAt !== null && progress && announcedId !== progress.id) { announcedId = progress.id; useGameStore.getState().openOverlay("dungeon-results"); } }; const unsubscribeEncounters = subscribeDungeonEncounterSnapshots(advance); const unsubscribeGame = useGameStore.subscribe(advance); const interval = setInterval(advance, 250); advance(); return () => { unsubscribeEncounters(); unsubscribeGame(); clearInterval(interval); }; }, []); return null; }