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>; 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(86); 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(32); 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); }); });