Files
healer-man/src/game/mobPopulation.test.ts
T
2026-09-26 19:55:08 -04:00

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);
}
});
});