Files
healer-man/src/game/animatedCreatureAssets.test.ts
T
2026-08-14 15:56:39 -04:00

116 lines
4.7 KiB
TypeScript

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<Record<string, number>>;
}[];
}[];
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<string>(
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<number>();
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);
});
});