import rawCatalog from "./generated/dungeonRouteCatalog.generated.json"; import type { DungeonRouteApproval, DungeonRouteCatalogV1, DungeonRouteDefinitionV1, DungeonRouteLeg, } from "./dungeonSimulationTypes"; import type { Vector3Tuple } from "./dungeonTypes"; export const DUNGEON_ROUTE_CATALOG = rawCatalog as unknown as DungeonRouteCatalogV1; export const DUNGEON_ROUTE_APPROVAL_CACHE_KEY = "healer-man.dungeon-route-approvals.v1"; const routesByDungeon = new Map( DUNGEON_ROUTE_CATALOG.routes.map((route) => [route.dungeonId, route] as const), ); export function dungeonRouteDefinition(dungeonId: string, routeId = "primary"): DungeonRouteDefinitionV1 | null { const route = routesByDungeon.get(dungeonId) ?? null; return route?.id === routeId ? route : null; } export function dungeonRouteLeg( route: DungeonRouteDefinitionV1 | null, objectiveId: string | null, ): DungeonRouteLeg | null { if (!route || !objectiveId) return null; return route.legs.find((leg) => leg.objectiveId === objectiveId) ?? null; } export function routeDistance(points: readonly Vector3Tuple[]): number { let total = 0; for (let index = 1; index < points.length; index += 1) { total += Math.hypot( points[index][0] - points[index - 1][0], points[index][1] - points[index - 1][1], points[index][2] - points[index - 1][2], ); } return total; } export function routePointDistanceSquared(point: Vector3Tuple, start: Vector3Tuple, end: Vector3Tuple): { readonly planarSquared: number; readonly vertical: number; readonly progress: number; } { const dx = end[0] - start[0]; const dz = end[2] - start[2]; const lengthSquared = dx * dx + dz * dz; const progress = lengthSquared > 1e-9 ? Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[2] - start[2]) * dz) / lengthSquared)) : 0; const projectedX = start[0] + dx * progress; const projectedY = start[1] + (end[1] - start[1]) * progress; const projectedZ = start[2] + dz * progress; return { planarSquared: (point[0] - projectedX) ** 2 + (point[2] - projectedZ) ** 2, vertical: Math.abs(point[1] - projectedY), progress, }; } export function positionAlongRouteCorridor( point: Vector3Tuple, checkpoints: readonly Vector3Tuple[], planarLimit = 10, verticalLimit = 4, ): boolean { for (let index = 1; index < checkpoints.length; index += 1) { const distance = routePointDistanceSquared(point, checkpoints[index - 1], checkpoints[index]); if (distance.planarSquared <= planarLimit ** 2 && distance.vertical <= verticalLimit) return true; } return checkpoints.length === 1 && Math.hypot(point[0] - checkpoints[0][0], point[2] - checkpoints[0][2]) <= planarLimit && Math.abs(point[1] - checkpoints[0][1]) <= verticalLimit; } export function readCachedDungeonRouteApprovals(): readonly DungeonRouteApproval[] { if (typeof localStorage === "undefined") return []; try { const parsed = JSON.parse(localStorage.getItem(DUNGEON_ROUTE_APPROVAL_CACHE_KEY) ?? "null"); if (!Array.isArray(parsed?.approvals)) return []; return parsed.approvals.filter((approval: unknown): approval is DungeonRouteApproval => { if (!approval || typeof approval !== "object") return false; const value = approval as Partial; return typeof value.dungeonId === "string" && typeof value.routeId === "string" && /^[a-f0-9]{64}$/.test(value.routeHash ?? "") && Number.isFinite(value.reviewedAt); }); } catch { return []; } } export function cacheDungeonRouteApprovals(approvals: readonly DungeonRouteApproval[]): void { if (typeof localStorage === "undefined") return; localStorage.setItem(DUNGEON_ROUTE_APPROVAL_CACHE_KEY, JSON.stringify({ approvals })); } export function routeHasExactApproval( route: DungeonRouteDefinitionV1 | null, approvals: readonly DungeonRouteApproval[], ): boolean { return Boolean(route?.generationStatus === "Candidate" && approvals.some((approval) => ( approval.dungeonId === route.dungeonId && approval.routeId === route.id && approval.routeHash === route.hash ))); }