import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { WAILING_CAVERNS_ENTITIES } from "./mobPopulation"; interface GlbDocument { readonly accessors?: readonly { readonly bufferView?: number; readonly byteOffset?: number; readonly componentType?: number; readonly count?: number; readonly type?: string; }[]; readonly animations?: readonly { readonly name?: string }[]; readonly bufferViews?: readonly { readonly byteOffset?: number; readonly byteStride?: number; }[]; readonly materials?: readonly { readonly name?: string }[]; readonly meshes?: readonly { readonly primitives?: readonly { readonly attributes?: Readonly>; }[]; }[]; readonly nodes?: readonly { readonly skin?: number }[]; readonly skins?: readonly unknown[]; } const projectDirectory = fileURLToPath(new URL("../..", import.meta.url)); const modelUrls: string[] = Array.from(new Set( Object.values(WAILING_CAVERNS_ENTITIES).flatMap((entity): string[] => ( "model" in entity.visual ? [entity.visual.model.url] : [] )), )); function readGlb(url: string): { document: GlbDocument; binary: Buffer } { const filePath = path.join(projectDirectory, "public", url.replace(/^\//, "")); const glb = readFileSync(filePath); expect(glb.readUInt32LE(0)).toBe(0x46546c67); expect(glb.readUInt32LE(4)).toBe(2); expect(glb.readUInt32LE(16)).toBe(0x4e4f534a); const jsonLength = glb.readUInt32LE(12); const binaryChunkOffset = 20 + jsonLength; expect(glb.readUInt32LE(binaryChunkOffset + 4)).toBe(0x004e4942); const binaryLength = glb.readUInt32LE(binaryChunkOffset); return { document: JSON.parse(glb.subarray(20, 20 + jsonLength).toString("utf8").trimEnd()) as GlbDocument, binary: glb.subarray(binaryChunkOffset + 8, binaryChunkOffset + 8 + binaryLength), }; } function expectUnitNormals(document: GlbDocument, binary: Buffer): void { const normalAccessors = new Set(); for (const mesh of document.meshes ?? []) { for (const primitive of mesh.primitives ?? []) { const accessorIndex = primitive.attributes?.NORMAL; if (accessorIndex !== undefined) normalAccessors.add(accessorIndex); } } const invalid: Array<{ accessor: number; vertex: number; length: number }> = []; for (const accessorIndex of normalAccessors) { const accessor = document.accessors?.[accessorIndex]; const bufferView = accessor?.bufferView === undefined ? undefined : document.bufferViews?.[accessor.bufferView]; expect(accessor?.componentType).toBe(5126); expect(accessor?.type).toBe("VEC3"); expect(bufferView).toBeDefined(); const count = accessor?.count ?? 0; const stride = bufferView?.byteStride ?? 12; const start = (bufferView?.byteOffset ?? 0) + (accessor?.byteOffset ?? 0); for (let vertex = 0; vertex < count; vertex += 1) { const offset = start + vertex * stride; const length = Math.hypot( binary.readFloatLE(offset), binary.readFloatLE(offset + 4), binary.readFloatLE(offset + 8), ); if (!Number.isFinite(length) || Math.abs(length - 1) > 1e-4) { invalid.push({ accessor: accessorIndex, vertex, length }); } } } expect(invalid).toEqual([]); } describe("browser-facing animated creature assets", () => { it("routes every imported creature model to an animated GLB", () => { expect(modelUrls).toHaveLength(16); expect(modelUrls.every((url) => url.endsWith("-animated.glb"))).toBe(true); }); it.each(modelUrls)("ships its complete native locomotion and combat library: %s", (url) => { const { document, binary } = readGlb(url); const clipNames = document.animations?.map((animation) => animation.name) ?? []; expect(document.skins).toHaveLength(1); expect(document.nodes?.some((node) => node.skin === 0)).toBe(true); expect(clipNames).toContain("Stand (ID 0 variation 0)"); expect(clipNames).toContain("Walk (ID 4 variation 0)"); expect(clipNames).toContain("Run (ID 5 variation 0)"); expect(clipNames.some((name) => /\(ID (?:16|17|18|19|57|58|85|87|88|95|118) variation \d+\)$/i.test(name ?? ""))).toBe(true); expect(clipNames.some((name) => /\(ID 1 variation \d+\)$/i.test(name ?? ""))).toBe(true); expect(clipNames.some((name) => /\(ID (?:8|9|10) variation \d+\)$/i.test(name ?? ""))).toBe(true); expect(clipNames.length).toBeGreaterThanOrEqual(8); expect(new Set(clipNames).size).toBe(clipNames.length); expect(document.materials?.every((material) => /_B[0124]$/i.test(material.name ?? ""))).toBe(true); expectUnitNormals(document, binary); }); });