Files
healer-man/src/game/dungeonRouteRuntime.ts
T
2026-09-16 14:30:58 -04:00

144 lines
5.2 KiB
TypeScript

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<string>();
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 };
}