47 lines
3.1 KiB
TypeScript
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.`);
|
|
}
|
|
}
|