Update Healer Man server
This commit is contained in:
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);
|
||||
});
|
||||
Reference in new issue
Block a user