import { dispatchDungeonWorldObjectInteraction } from "./dungeonWorldInteraction"; import { requireDungeonDefinition } from "./dungeonRegistry"; import { useDungeonRouteStore } from "./dungeonRouteStore"; import { dungeonRouteDefinition, dungeonRouteLeg, } from "./dungeonRoutes"; import { useDungeonSimulationStore } from "./dungeonSimulationStore"; import type { DungeonRouteDefinitionV1, DungeonRouteLeg } from "./dungeonSimulationTypes"; import type { Vector3Tuple } from "./dungeonTypes"; export interface ActiveDungeonRouteTarget { readonly route: DungeonRouteDefinitionV1; readonly leg: DungeonRouteLeg; readonly checkpointIndex: number; readonly target: Vector3Tuple; readonly remaining: readonly Vector3Tuple[]; } let activeDungeonId: string | null = null; let activeObjectiveId: string | null = null; let checkpointIndex = 0; const completedActions = new Set(); function distance(left: Vector3Tuple, right: Vector3Tuple): number { return Math.hypot(right[0] - left[0], right[1] - left[1], right[2] - left[2]); } export function resetDungeonRouteRuntime(): void { activeDungeonId = null; activeObjectiveId = null; checkpointIndex = 0; completedActions.clear(); } export function routeForActiveDungeon(dungeonId: string): DungeonRouteDefinitionV1 | null { const route = dungeonRouteDefinition(dungeonId); if (!route) return null; const simulation = useDungeonSimulationStore.getState(); if (simulation.sessionMode === "spectate-live") return route; return useDungeonRouteStore.getState().isApproved(route) ? route : null; } function executeReachedActions( dungeonId: string, leg: DungeonRouteLeg, reachedIndex: number, actorId: string | undefined, now: number, ): void { const definition = requireDungeonDefinition(dungeonId); for (const action of leg.actions) { if (action.checkpointIndex > reachedIndex || completedActions.has(action.id)) continue; const object = definition.serverGameObjects.find((candidate) => candidate.id === action.objectId); if (!object?.interaction) { useDungeonSimulationStore.getState().addFinding({ id: `invalid-action:${leg.id}:${action.id}`, atMs: useDungeonSimulationStore.getState().elapsedMs, severity: "critical", code: "invalid-required-interaction", message: `Route action ${action.id} references unavailable object ${action.objectId}.`, objectiveId: leg.objectiveId, checkpointIndex: action.checkpointIndex, }); completedActions.add(action.id); continue; } dispatchDungeonWorldObjectInteraction(object, actorId, now); if (useDungeonSimulationStore.getState().sessionMode === "spectate-live") { useDungeonSimulationStore.getState().addFinding({ id: `route-action:${leg.id}:${action.id}`, atMs: useDungeonSimulationStore.getState().elapsedMs, severity: "info", code: "route-action", message: `Performed required route action ${action.id}.`, objectiveId: leg.objectiveId, checkpointIndex: action.checkpointIndex, actionId: action.id, }); } completedActions.add(action.id); } } function advanceNavigationOnlyObjective(route: DungeonRouteDefinitionV1, leg: DungeonRouteLeg): void { const simulation = useDungeonSimulationStore.getState(); if (simulation.sessionMode !== "spectate-live" || simulation.mode !== "navigation-only") return; const index = route.legs.findIndex((candidate) => candidate.id === leg.id); const next = route.legs[index + 1] ?? null; simulation.setRouteProgress(next?.objectiveId ?? null, 0); activeObjectiveId = next?.objectiveId ?? null; checkpointIndex = 0; } export function activeDungeonRouteTarget( dungeonId: string, objectiveId: string | null, position: Vector3Tuple, actorId: string | undefined, now: number, ): ActiveDungeonRouteTarget | null { const route = routeForActiveDungeon(dungeonId); const simulation = useDungeonSimulationStore.getState(); const routedObjectiveId = simulation.sessionMode === "spectate-live" && simulation.mode === "navigation-only" ? simulation.objectiveId : objectiveId; const leg = dungeonRouteLeg(route, routedObjectiveId); if (!route || !leg || !leg.checkpoints.length) return null; if (activeDungeonId !== dungeonId || activeObjectiveId !== leg.objectiveId) { activeDungeonId = dungeonId; activeObjectiveId = leg.objectiveId; checkpointIndex = 0; } while ( checkpointIndex < leg.checkpoints.length && distance(position, leg.checkpoints[checkpointIndex]) <= 1.25 ) { executeReachedActions(dungeonId, leg, checkpointIndex, actorId, now); checkpointIndex += 1; } if (checkpointIndex >= leg.checkpoints.length) { advanceNavigationOnlyObjective(route, leg); return null; } if (simulation.sessionMode === "spectate-live") { simulation.setRouteProgress(leg.objectiveId, checkpointIndex); } return { route, leg, checkpointIndex, target: leg.checkpoints[checkpointIndex], remaining: leg.checkpoints.slice(checkpointIndex), }; } export function activeDungeonRouteProgress(): { readonly dungeonId: string | null; readonly objectiveId: string | null; readonly checkpointIndex: number; } { return { dungeonId: activeDungeonId, objectiveId: activeObjectiveId, checkpointIndex }; }