Files
healer-man/src/game/biteSizedSpotNavigation.ts
T
2026-09-27 21:48:00 -04:00

47 lines
3.1 KiB
TypeScript

import { findNearestPartyNavigationSurface, findPartyNavigationPath, type PartyNavigationGraph } from "./partyNavigation";
import { derivePartySpawnSlots } from "./partySpawnSlots";
import type { BiteSizedLayoutBlocker, BiteSizedSpot, LayoutVector } from "../../shared/bite-sized-layouts.mjs";
export function biteSizedRouteSegmentClear(blockers: readonly BiteSizedLayoutBlocker[], from: LayoutVector, to: LayoutVector): boolean {
return blockers.every((blocker) => {
const sin = Math.sin(blocker.yaw), cos = Math.cos(blocker.yaw);
const local = (point: LayoutVector) => {
const x = point[0] - blocker.position[0], z = point[2] - blocker.position[2];
return [x * cos - z * sin, point[1] - blocker.position[1], x * sin + z * cos];
};
const start = local(from), end = local(to);
let enter = 0, leave = 1;
for (let axis = 0; axis < 3; axis++) {
const extent = blocker.halfExtents[axis] + (axis === 1 ? 1 : 0.34);
const delta = end[axis] - start[axis];
if (Math.abs(delta) < 1e-9) { if (Math.abs(start[axis]) > extent) return true; continue; }
const first = (-extent - start[axis]) / delta, second = (extent - start[axis]) / delta;
enter = Math.max(enter, Math.min(first, second));
leave = Math.min(leave, Math.max(first, second));
if (enter > leave) return true;
}
return false;
});
}
export function validateBiteSizedSpotNavigation(spot: BiteSizedSpot, graph: PartyNavigationGraph, regions?: readonly number[], blockers: readonly BiteSizedLayoutBlocker[] = []): void {
const start = findNearestPartyNavigationSurface(graph, spot.partySpawn.footPosition, 0.75);
if (!start) throw new Error(`${spot.name}: Party anchor is off the walkable floor. Move its spawn point.`);
const points: { label: string; position: readonly [number, number, number]; snapDistance?: number }[] = [
...derivePartySpawnSlots(spot.partySpawn).map((s, i) => ({ label: `Party member ${i + 1}`, position: s.footPosition, snapDistance: i === 0 ? 0.75 : 6 })),
...spot.mobs.map((m, i) => ({ label: `Mob ${i + 1}`, position: m.position })),
{ label: "Boss", position: spot.bossSpawn.footPosition },
];
for (const point of points) {
// Companions project their formation seats onto the floor within six metres,
// exactly as PartyPopulation does. Authored mob/boss points never auto-snap.
const match = findNearestPartyNavigationSurface(graph, point.position, point.snapDistance ?? 0.75);
if (!match) throw new Error(`${spot.name}: ${point.label} is off the walkable floor. Move its spawn point.`);
if (!biteSizedRouteSegmentClear(blockers, match.position, match.position)) throw new Error(`${spot.name}: ${point.label} is inside a route blocker.`);
const reachable = regions ? regions[start.triangleId] === regions[match.triangleId]
: findPartyNavigationPath(graph, start.position, match.position, { maxSnapDistance: 0.75,
routeSegmentAllowed: (from, to) => biteSizedRouteSegmentClear(blockers, from, to) });
if (!reachable) throw new Error(`${spot.name}: ${point.label} cannot be reached from the party start.`);
}
}