144 lines
5.2 KiB
TypeScript
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 };
|
|
}
|