forked from phenom/healer-man
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
createLoopedPath,
|
|
hasUsableLoopedMobPath,
|
|
normalizeMobPopulationPaths,
|
|
sampleLoopedPath,
|
|
samplePackFormation,
|
|
samplePackMember,
|
|
WAILING_CAVERNS_BOSS_SPAWNS,
|
|
WAILING_CAVERNS_ENTRANCE_SPAWNS,
|
|
WAILING_CAVERNS_ENTITIES,
|
|
WAILING_CAVERNS_ROAMING_PACKS,
|
|
WAILING_CAVERNS_STATIC_SPAWNS,
|
|
} from "./mobPopulation";
|
|
import { DUNGEON_MANIFEST } from "./dungeonManifest";
|
|
import { DUNGEON_DEFINITIONS } from "./dungeonRegistry";
|
|
|
|
const square = createLoopedPath([
|
|
[0, 0, 0],
|
|
[10, 0, 0],
|
|
[10, 0, 10],
|
|
[0, 0, 10],
|
|
]);
|
|
|
|
describe("looped mob path sampling", () => {
|
|
it("precomputes a closed loop including the final-to-first segment", () => {
|
|
expect(square.segments).toHaveLength(4);
|
|
expect(square.totalLength).toBeCloseTo(40);
|
|
});
|
|
|
|
it("interpolates positions and returns a normalized forward vector", () => {
|
|
const position: [number, number, number] = [0, 0, 0];
|
|
const forward: [number, number, number] = [0, 0, 0];
|
|
|
|
expect(sampleLoopedPath(square, 15, position, forward)).toBe(1);
|
|
expect(position).toEqual([10, 0, 5]);
|
|
expect(forward).toEqual([0, 0, 1]);
|
|
});
|
|
|
|
it("wraps distances in both directions", () => {
|
|
const position: [number, number, number] = [0, 0, 0];
|
|
const forward: [number, number, number] = [0, 0, 0];
|
|
|
|
sampleLoopedPath(square, 42.5, position, forward);
|
|
expect(position).toEqual([2.5, 0, 0]);
|
|
|
|
sampleLoopedPath(square, -2.5, position, forward);
|
|
expect(position).toEqual([0, 0, 2.5]);
|
|
expect(forward).toEqual([0, 0, -1]);
|
|
});
|
|
|
|
it("keeps follower trailing distance on the path and applies lateral formation offset", () => {
|
|
const position: [number, number, number] = [0, 0, 0];
|
|
const forward: [number, number, number] = [0, 0, 0];
|
|
|
|
samplePackMember(square, 15, [2, 0.5, 3], position, forward);
|
|
|
|
expect(position).toEqual([12, 0.5, 2]);
|
|
expect(forward).toEqual([0, 0, 1]);
|
|
});
|
|
|
|
it("updates stable per-member buffers used by roaming proximity checks", () => {
|
|
const members = [
|
|
{ id: "leader", entityId: "leader", role: "leader", formationOffset: [0, 0, 0] },
|
|
{ id: "follower", entityId: "follower", role: "follower", formationOffset: [2, 0.5, 3] },
|
|
] as const;
|
|
const positions: [number, number, number][] = [[0, 0, 0], [0, 0, 0]];
|
|
const forwards: [number, number, number][] = [[0, 0, 1], [0, 0, 1]];
|
|
const leaderPosition = positions[0];
|
|
|
|
samplePackFormation(square, 15, members, positions, forwards);
|
|
expect(positions).toEqual([[10, 0, 5], [12, 0.5, 2]]);
|
|
|
|
samplePackFormation(square, 25, members, positions, forwards);
|
|
expect(positions[0]).toBe(leaderPosition);
|
|
expect(positions).toEqual([[5, 0, 10], [8, 0.5, 12]]);
|
|
});
|
|
|
|
it("rejects paths without distinct points", () => {
|
|
expect(() => createLoopedPath([[1, 2, 3]])).toThrow(/at least two waypoints/i);
|
|
expect(() => createLoopedPath([[1, 2, 3], [1, 2, 3]])).toThrow(/distinct waypoints/i);
|
|
});
|
|
|
|
it("converts malformed imported patrols into stationary spawns", () => {
|
|
const pack = {
|
|
id: "stationary-patrol",
|
|
name: "Stationary Patrol",
|
|
speed: 1,
|
|
formationSource: "database-solo",
|
|
spawnMask: 2,
|
|
waypoints: [[4, 5, 6], [4, 5, 6]],
|
|
members: [{
|
|
id: "member",
|
|
entityId: "guard",
|
|
role: "leader",
|
|
formationOffset: [1, 0.5, 2],
|
|
}],
|
|
} as const;
|
|
const normalized = normalizeMobPopulationPaths([pack], []);
|
|
|
|
expect(normalized.roamingPacks).toEqual([]);
|
|
expect(normalized.staticSpawns).toEqual([{
|
|
id: "stationary-patrol:member",
|
|
entityId: "guard",
|
|
position: [5, 5.5, 4],
|
|
spawnMask: 2,
|
|
}]);
|
|
});
|
|
|
|
it("keeps every packaged dungeon patrol on a usable loop", () => {
|
|
expect(DUNGEON_DEFINITIONS).toHaveLength(89);
|
|
for (const dungeon of DUNGEON_DEFINITIONS) {
|
|
for (const pack of dungeon.roamingPacks) {
|
|
expect(hasUsableLoopedMobPath(pack.waypoints), `${dungeon.id}:${pack.id}`).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("keeps the shipped patrol population internally consistent", () => {
|
|
expect(WAILING_CAVERNS_ROAMING_PACKS).toHaveLength(6);
|
|
|
|
const pathIds = new Set<number>();
|
|
for (const pack of WAILING_CAVERNS_ROAMING_PACKS) {
|
|
expect(pack.pathId).toBeTypeOf("number");
|
|
expect(pathIds.has(pack.pathId!)).toBe(false);
|
|
pathIds.add(pack.pathId!);
|
|
expect(() => createLoopedPath(pack.waypoints)).not.toThrow();
|
|
|
|
for (const member of pack.members) {
|
|
expect(WAILING_CAVERNS_ENTITIES[member.entityId as keyof typeof WAILING_CAVERNS_ENTITIES]).toBeDefined();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("does not duplicate roaming Kresh as a stationary boss", () => {
|
|
expect(WAILING_CAVERNS_ROAMING_PACKS.some((pack) => pack.members.some((member) => member.entityId === "kresh"))).toBe(true);
|
|
expect(WAILING_CAVERNS_BOSS_SPAWNS.some((spawn) => spawn.entityId === "kresh")).toBe(false);
|
|
});
|
|
|
|
it("keeps a renderable encounter within immediate entrance range", () => {
|
|
const entrance = DUNGEON_MANIFEST.entrance.footPosition;
|
|
expect(WAILING_CAVERNS_ENTRANCE_SPAWNS).toHaveLength(3);
|
|
|
|
for (const spawn of WAILING_CAVERNS_ENTRANCE_SPAWNS) {
|
|
const distance = Math.hypot(
|
|
spawn.position[0] - entrance[0],
|
|
spawn.position[1] - entrance[1],
|
|
spawn.position[2] - entrance[2],
|
|
);
|
|
expect(distance).toBeLessThan(22);
|
|
expect(WAILING_CAVERNS_ENTITIES[spawn.entityId as keyof typeof WAILING_CAVERNS_ENTITIES]).toBeDefined();
|
|
expect(WAILING_CAVERNS_STATIC_SPAWNS).toContain(spawn);
|
|
}
|
|
});
|
|
});
|