Update Healer Man server

This commit is contained in:
phenom committed 2026-09-26 20:27:07 -04:00
1 parent 4957b69e86
commit 8a98797fcf
30 files changed
+469 -39

No files matched your search

@@ -0,0 +1,91 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { NodeIO } from "@gltf-transform/core";
import { BufferGeometry, Float32BufferAttribute, Matrix4, Vector3 } from "three";
import { describe, expect, it } from "vitest";
import { dungeonContentPackId, dungeonContentSourceId } from "../dungeonContentSource";
import { DUNGEON_REGISTRY } from "../dungeonRegistry";
import { createDungeonEncounterSnapshot, reduceDungeonEncounter } from "../dungeonEncounterRuntime";
import { buildPartyNavigationGraph, findNearestPartyNavigationSurface, findPartyNavigationPath } from "../partyNavigation";
const sliceIds = [
"bite-sized-forsaken-abbey-ancalon",
"bite-sized-paspers-shrine-hafiz",
"bite-sized-mystic-altar-golem",
] as const;
describe("RoM bite-sized finales", () => {
it.each(sliceIds)("gates %s behind its retained mobs and completes on its boss", (id) => {
const slice = DUNGEON_REGISTRY[id];
const source = DUNGEON_REGISTRY[dungeonContentSourceId(id)];
expect(slice).toBeDefined();
expect(source).toBeDefined();
expect(dungeonContentPackId(id)).toBe(`dungeon:${source.id}`);
expect(slice.assets).toBe(source.assets);
expect(slice.staticSpawns).toHaveLength(6);
expect(slice.staticSpawns.every((spawn) => source.staticSpawns.includes(spawn))).toBe(true);
expect(slice.bosses).toHaveLength(1);
expect(slice.bosses[0].eventOnly).toBe(true);
expect(slice.staticSpawns).not.toContainEqual(expect.objectContaining({ id: slice.bosses[0].id }));
expect(source.staticSpawns).toContainEqual(expect.objectContaining({ id: slice.bosses[0].id }));
const encounter = slice.encounters![0];
const mobIds = slice.staticSpawns.map((spawn) => spawn.id);
const initial = createDungeonEncounterSnapshot(encounter);
const beforeClear = reduceDungeonEncounter(encounter, initial, { kind: "tick", now: 100,
facts: { registeredEntityIds: mobIds, deadEntityIds: mobIds.slice(0, -1) } }).snapshot;
expect(beforeClear.activeSpawnGroupIds).toEqual([]);
const afterClear = reduceDungeonEncounter(encounter, beforeClear, { kind: "tick", now: 200,
facts: { registeredEntityIds: mobIds, deadEntityIds: mobIds } }).snapshot;
expect(afterClear.activeSpawnGroupIds).toEqual([slice.encounterSpawnGroups![0].id]);
const completed = reduceDungeonEncounter(encounter, afterClear, { kind: "tick", now: 300,
facts: { registeredEntityIds: [slice.bosses[0].id], deadEntityIds: [slice.bosses[0].id] } }).snapshot;
expect(completed.completedObjectiveIds).toContain(slice.bosses[0].id);
expect(completed.flags["dungeon-complete"]).toBe(true);
expect(completed.phase).toBe("complete");
// The complete source remains available with its original population and objectives.
expect(source.bosses.length).toBeGreaterThan(1);
});
it.each(sliceIds)("keeps the %s start and boss reachable on shipped navigation", async (id) => {
const slice = DUNGEON_REGISTRY[id];
const sourceId = dungeonContentSourceId(id);
const navigation = slice.assets.navigation!;
const file = path.resolve("src/assets/game/dungeons", sourceId, navigation.fileName);
expect(readFileSync(file).length).toBeGreaterThan(0);
const document = await new NodeIO().read(file);
const positions: number[] = [];
const vertex = new Vector3();
for (const node of document.getRoot().listNodes()) {
const mesh = node.getMesh();
if (!mesh) continue;
const world = new Matrix4().fromArray(node.getWorldMatrix());
for (const primitive of mesh.listPrimitives()) {
const source = primitive.getAttribute("POSITION")?.getArray();
if (!source) continue;
const indices = primitive.getIndices()?.getArray() ?? Array.from({ length: source.length / 3 }, (_, i) => i);
for (const index of indices) {
vertex.fromArray(source, Number(index) * 3).applyMatrix4(world);
positions.push(vertex.x, vertex.y, vertex.z);
}
}
}
const geometry = new BufferGeometry();
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
const graph = buildPartyNavigationGraph(geometry);
const start = findNearestPartyNavigationSurface(graph, slice.entrance.footPosition, 3);
const boss = findNearestPartyNavigationSurface(graph, slice.bosses[0].position, 6);
expect(start, `${id} start`).not.toBeNull();
expect(boss, `${id} boss`).not.toBeNull();
expect(findPartyNavigationPath(graph, start!.position, boss!.position, {
maxStartSnapDistance: 3, maxEndSnapDistance: 6,
}), `${id} start to boss`).not.toBeNull();
for (const spawn of slice.staticSpawns) {
const mob = findNearestPartyNavigationSurface(graph, spawn.position, 3);
expect(mob, `${id} ${spawn.id}`).not.toBeNull();
expect(findPartyNavigationPath(graph, start!.position, mob!.position, {
maxStartSnapDistance: 3, maxEndSnapDistance: 3,
}), `${id} route to ${spawn.id}`).not.toBeNull();
}
}, 30_000);
});