forked from phenom/healer-man
155 lines
5.8 KiB
TypeScript
155 lines
5.8 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { DUNGEON_DEFINITIONS, dungeonCanEnter } from "./dungeonRegistry";
|
|
import manastormRegistry from "./generated/manastormAssetPackages.json";
|
|
import type { ManastormStageAssetPackage } from "./manastormStageAssets";
|
|
import {
|
|
MAX_SYNCHRONOUS_PARTY_NAVIGATION_TRIANGLES,
|
|
shouldLoadPartyNavigation,
|
|
} from "./partyNavigationPolicy";
|
|
|
|
interface GlbPrimitive {
|
|
readonly attributes?: Readonly<Record<string, number>>;
|
|
readonly indices?: number;
|
|
readonly mode?: number;
|
|
}
|
|
|
|
interface GlbJson {
|
|
readonly accessors?: readonly { readonly count?: number }[];
|
|
readonly meshes?: readonly { readonly primitives?: readonly GlbPrimitive[] }[];
|
|
}
|
|
|
|
const GLB_MAGIC = 0x46546c67;
|
|
const GLB_JSON_CHUNK = 0x4e4f534a;
|
|
|
|
function readGlbJson(filePath: string): GlbJson {
|
|
const glb = readFileSync(filePath);
|
|
expect(glb.readUInt32LE(0), filePath).toBe(GLB_MAGIC);
|
|
expect(glb.readUInt32LE(4), filePath).toBe(2);
|
|
expect(glb.readUInt32LE(16), filePath).toBe(GLB_JSON_CHUNK);
|
|
const jsonLength = glb.readUInt32LE(12);
|
|
return JSON.parse(glb.toString("utf8", 20, 20 + jsonLength)) as GlbJson;
|
|
}
|
|
|
|
/** Mirrors PartyPopulation's selection of the mesh with the most positions. */
|
|
function runtimeSelectedTriangleCount(filePath: string): number {
|
|
const glb = readGlbJson(filePath);
|
|
let largestVertexCount = 0;
|
|
let triangleCount = 0;
|
|
for (const mesh of glb.meshes ?? []) {
|
|
for (const primitive of mesh.primitives ?? []) {
|
|
if (primitive.mode !== undefined && primitive.mode !== 4) continue;
|
|
const positionAccessor = primitive.attributes?.POSITION;
|
|
const vertexCount = positionAccessor === undefined
|
|
? 0
|
|
: glb.accessors?.[positionAccessor]?.count ?? 0;
|
|
if (vertexCount <= largestVertexCount) continue;
|
|
largestVertexCount = vertexCount;
|
|
const elementCount = primitive.indices === undefined
|
|
? vertexCount
|
|
: glb.accessors?.[primitive.indices]?.count ?? 0;
|
|
expect(elementCount % 3, filePath).toBe(0);
|
|
triangleCount = elementCount / 3;
|
|
}
|
|
}
|
|
expect(triangleCount, filePath).toBeGreaterThan(0);
|
|
return triangleCount;
|
|
}
|
|
|
|
function regularNavigationPath(
|
|
dungeonId: string,
|
|
navigation: { readonly fileName: string; readonly url?: string },
|
|
): string {
|
|
if (navigation.url?.startsWith("/")) {
|
|
return path.join(process.cwd(), "public", ...navigation.url.split("/").filter(Boolean));
|
|
}
|
|
return path.join(
|
|
process.cwd(),
|
|
"src",
|
|
"assets",
|
|
"game",
|
|
"dungeons",
|
|
dungeonId,
|
|
navigation.fileName,
|
|
);
|
|
}
|
|
|
|
function manastormNavigationPath(url: string): string {
|
|
return path.join(process.cwd(), "public", ...url.split("/").filter(Boolean));
|
|
}
|
|
|
|
describe("party navigation fleet safety", () => {
|
|
it("covers every regular dungeon and keeps actual synchronous geometry under budget", () => {
|
|
const enabled = [];
|
|
const fallback = [];
|
|
|
|
expect(DUNGEON_DEFINITIONS).toHaveLength(84);
|
|
for (const dungeon of DUNGEON_DEFINITIONS) {
|
|
expect(dungeonCanEnter(dungeon.id), dungeon.id).toBe(true);
|
|
const navigation = dungeon.assets.navigation;
|
|
expect(navigation, dungeon.id).toBeDefined();
|
|
expect(Number.isInteger(navigation?.triangleCount), dungeon.id).toBe(true);
|
|
expect(navigation?.triangleCount, dungeon.id).toBeGreaterThan(0);
|
|
|
|
const actualTriangles = runtimeSelectedTriangleCount(regularNavigationPath(
|
|
dungeon.id,
|
|
navigation!,
|
|
));
|
|
// Metadata may conservatively count multiple primitives, but must never
|
|
// under-report the runtime-selected primitive used by the graph builder.
|
|
expect(navigation!.triangleCount, dungeon.id).toBeGreaterThanOrEqual(actualTriangles);
|
|
|
|
if (shouldLoadPartyNavigation("dungeon", dungeon, null)) {
|
|
enabled.push(dungeon.id);
|
|
expect(
|
|
dungeon.validation.status === "green" || dungeon.validation.navigation === "green",
|
|
dungeon.id,
|
|
).toBe(true);
|
|
expect(actualTriangles, dungeon.id)
|
|
.toBeLessThanOrEqual(MAX_SYNCHRONOUS_PARTY_NAVIGATION_TRIANGLES);
|
|
} else {
|
|
fallback.push(dungeon.id);
|
|
}
|
|
}
|
|
|
|
// Raksha Temple and The Origin now ship their full source-assembled
|
|
// navigation geometry, which correctly moves both above the synchronous
|
|
// graph-build ceiling and onto the breadcrumb fallback path.
|
|
// Stockades has a reviewed navigation override without claiming that its
|
|
// broader encounter/reset validation is complete.
|
|
expect(enabled).toContain("stormwind-stockade");
|
|
expect(enabled).toHaveLength(30);
|
|
expect(fallback).toHaveLength(54);
|
|
});
|
|
|
|
it("covers every Manastorm package and keeps actual synchronous geometry under budget", () => {
|
|
const packages = manastormRegistry.packages as unknown as readonly ManastormStageAssetPackage[];
|
|
const fallbackDungeon = DUNGEON_DEFINITIONS[0];
|
|
const enabled = [];
|
|
const fallback = [];
|
|
|
|
expect(packages).toHaveLength(75);
|
|
for (const assetPackage of packages) {
|
|
const declaredTriangles = assetPackage.navigation.triangleCount;
|
|
expect(Number.isInteger(declaredTriangles), assetPackage.id).toBe(true);
|
|
expect(declaredTriangles, assetPackage.id).toBeGreaterThan(0);
|
|
const actualTriangles = runtimeSelectedTriangleCount(
|
|
manastormNavigationPath(assetPackage.navigation.url),
|
|
);
|
|
expect(declaredTriangles, assetPackage.id).toBe(actualTriangles);
|
|
|
|
if (shouldLoadPartyNavigation("manastorm", fallbackDungeon, assetPackage)) {
|
|
enabled.push(assetPackage.id);
|
|
expect(actualTriangles, assetPackage.id)
|
|
.toBeLessThanOrEqual(MAX_SYNCHRONOUS_PARTY_NAVIGATION_TRIANGLES);
|
|
} else {
|
|
fallback.push(assetPackage.id);
|
|
}
|
|
}
|
|
|
|
expect(enabled).toHaveLength(41);
|
|
expect(fallback).toHaveLength(34);
|
|
});
|
|
});
|