import assert from "node:assert/strict"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { Document, NodeIO } from "@gltf-transform/core"; import { Matrix4, Vector3 } from "three"; import { assembleInstancedGlb, buildWdbPlacementPlan, normalizeWdbResourcePath, placementMatrixInPrimaryThree, resourceCacheKey, transformSourcePointToPrimaryThree, } from "./lib/environment-assembly.mjs"; const identityPlacement = (overrides = {}) => ({ guid: "PRIMARY", resource: "model\\dungeon\\fixture\\room.ros", scale: [1, 1, 1], rotation: [0, 0, 0, 1], translation: [10, 20, 30], localBounds: { center: [0, 0, 0], radius: 1 }, worldBounds: { center: [10, 20, 30], radius: 1 }, ...overrides, }); const fixtureRecipe = (source = {}) => ({ slug: "fixture", coordinateSystem: { source: "left-handed Y-up", nativeBridge: "three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)", metersPerSourceUnit: 0.1, }, source: { model: "model/dungeon/fixture/room.ros", primaryPlacementTranslation: [10, 20, 30], ...source, }, }); test("normalizes resource paths and makes case-insensitive stable cache keys", () => { assert.equal( normalizeWdbResourcePath("/Model/Dungeon/Fixture/Room.ROS"), "model\\dungeon\\fixture\\room.ros", ); assert.equal( resourceCacheKey("Model/Dungeon/Fixture/Room.ROS"), resourceCacheKey("model\\dungeon\\fixture\\room.ros"), ); }); test("converts full placement translation, quaternion, and nonuniform scale", () => { const primary = identityPlacement(); const placed = identityPlacement({ guid: "PLACED", resource: "model\\dungeon\\fixture\\bridge.ros", translation: [20, 22, 40], rotation: [0, Math.SQRT1_2, 0, Math.SQRT1_2], scale: [2, 3, 4], }); const matrix = placementMatrixInPrimaryThree(placed, primary, 0.1); const sourcePoint = [3, 4, 5]; const viaMatrix = new Vector3(sourcePoint[0] * 0.1, sourcePoint[1] * 0.1, -sourcePoint[2] * 0.1) .applyMatrix4(new Matrix4().fromArray(matrix.toArray())) .toArray(); const direct = transformSourcePointToPrimaryThree(sourcePoint, placed, primary, 0.1); assert.deepEqual(viaMatrix.map((value) => Number(value.toFixed(9))), direct.map((value) => Number(value.toFixed(9)))); assert.notDeepEqual(matrix.toArray(), new Matrix4().identity().toArray()); }); test("builds deterministic static, dynamic, review, and unsupported placement groups", () => { const primary = identityPlacement(); const door = identityPlacement({ guid: "DOOR", resource: "model\\dungeon\\fixture\\boss_door.ros", translation: [15, 20, 30], }); const hintedBridge = identityPlacement({ guid: "BRIDGE", resource: "model\\dungeon\\fixture\\drawbridge.ros", translation: [20, 20, 30], }); const unsupported = identityPlacement({ guid: "FX", resource: "effect\\fixture\\mist.eft", }); const recipe = fixtureRecipe({ wdbAssembly: { mode: "all-ros", dynamicPlacements: [{ id: "boss-door", guid: "DOOR", kind: "door", initialState: "closed", collision: "stateful", evidence: ["fixture.lua:10"], }], }, }); const first = buildWdbPlacementPlan({ sourceWdb: "fixture.wdb", descriptors: [unsupported, hintedBridge, door, primary], }, recipe); const second = buildWdbPlacementPlan({ sourceWdb: "fixture.wdb", descriptors: [unsupported, hintedBridge, door, primary], }, recipe); assert.deepEqual(first, second); assert.equal(first.staticPlacements.length, 2); assert.equal(first.dynamicPlacements.length, 1); assert.equal(first.dynamicPlacements[0].dynamic.kind, "door"); assert.equal(first.reviewRequiredPlacements.length, 1); assert.equal(first.reviewRequiredPlacements[0].guid, "BRIDGE"); assert.equal(first.unsupportedPlacements.length, 1); }); test("assembles repeated cached GLBs as transformed resource instances", async () => { const temporary = await mkdtemp(path.join(os.tmpdir(), "healer-man-wdb-assembly-")); try { const sourceFile = path.join(temporary, "source.glb"); const outputFile = path.join(temporary, "assembled.glb"); const source = new Document(); const buffer = source.createBuffer("fixture-buffer"); const position = source.createAccessor("fixture-position", buffer) .setType("VEC3") .setArray(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0])); const primitive = source.createPrimitive().setAttribute("POSITION", position); const mesh = source.createMesh("fixture-mesh").addPrimitive(primitive); source.createScene("fixture-scene").addChild(source.createNode("fixture-node").setMesh(mesh)); const io = new NodeIO(); await io.write(sourceFile, source); const makePlacement = (id, x) => ({ id, guid: id.toUpperCase(), resource: "model/dungeon/fixture/room.ros", resourceCacheKey: resourceCacheKey("model/dungeon/fixture/room.ros"), transform: { primaryLocalThreeMatrix: new Matrix4().makeTranslation(x, 0, 0).toArray(), }, }); const result = await assembleInstancedGlb([ { assetFile: sourceFile, placement: makePlacement("first", 0) }, { assetFile: sourceFile, placement: makePlacement("second", 5) }, ], outputFile); const assembled = await io.read(outputFile); const scene = assembled.getRoot().getDefaultScene() ?? assembled.getRoot().listScenes()[0]; assert.deepEqual(scene.listChildren().map((node) => node.getName()), ["first", "second"]); assert.deepEqual(result.bounds, { min: [0, 0, 0], max: [6, 1, 0] }); assert.equal(assembled.getRoot().listMeshes().length, 1, "repeated placements share cached mesh data"); } finally { await rm(temporary, { recursive: true, force: true }); } }); test("rejects dynamic rules that would remove the primary coordinate anchor", () => { const primary = identityPlacement(); assert.throws(() => buildWdbPlacementPlan({ descriptors: [primary] }, fixtureRecipe({ wdbAssembly: { dynamicPlacements: [{ id: "invalid-primary-door", guid: "PRIMARY", kind: "door", evidence: ["fixture.lua:1"], }], }, })), /primary coordinate-anchor placement cannot be dynamic/); }); test("current modular WDB reports account for every descriptor without guessed dynamics", async () => { for (const slug of ["raksha-temple", "the-origin", "treasure-trove"]) { const [recipe, report] = await Promise.all([ readFile(new URL("./recipes/" + slug + ".json", import.meta.url), "utf8").then(JSON.parse), readFile(new URL("../../src/assets/game/dungeons/" + slug + "/wdb-report.json", import.meta.url), "utf8").then(JSON.parse), ]); const plan = buildWdbPlacementPlan(report, recipe); assert.equal( plan.staticPlacements.length + plan.dynamicPlacements.length + plan.unsupportedPlacements.length, report.descriptorCount, slug, ); assert.equal(plan.dynamicPlacements.length, 0, slug + " must not infer stateful behavior from names"); assert.ok(plan.resources.length > 1, slug + " should expose arbitrary resource assembly"); assert.equal(plan.primaryPlacement.resource.toLowerCase(), recipe.source.model.toLowerCase()); } });