forked from phenom/healer-man
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
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<DungeonRouteApproval>;
|
|
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
|
|
)));
|
|
}
|