import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { verifiedRuneWakerParticleModels } from "./rom-particle-readiness.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const publicRoot = path.join(root, "public"); const index = JSON.parse(fs.readFileSync(path.join(publicRoot, "assets/game/combat-effects/original/runewaker/manifest.json"), "utf8")); function assetFile(url) { assert.match(url, /^\/assets\/game\/combat-effects\/original\/runewaker\/[\w/.-]+\.(?:json|png|hlsl)$/); const file = path.resolve(publicRoot, url.slice(1)); assert.ok(file.startsWith(`${publicRoot}${path.sep}`)); return file; } test("primary-five RuneWaker particle archive resolves its decoded graphs and textures", () => { assert.equal(index.requestedModels, 170); assert.equal(index.processedModels, index.requestedModels); assert.equal(index.failedModels, 0); assert.equal(index.results.length, index.requestedModels); let spriteControllers = 0; let textureReferences = 0; for (const result of index.results) { assert.ok(result.graphUrl, result.model); const graph = JSON.parse(fs.readFileSync(assetFile(result.graphUrl), "utf8")); assert.equal(graph.kind, "runewaker-ros-particle-archive"); assert.equal(graph.sourceModel, result.model); assert.equal(graph.rendererReady, result.rendererReady); assert.equal(Boolean(graph.partialRendererReady), result.partialRendererReady); if (result.rendererReady) { assert.equal(result.partialRendererReady, false); assert.equal(result.errors.length, 0); assert.ok(result.spriteControllers > 0); assert.ok(result.unsupported.every((type) => type === "CRuController_EventTrigger")); } if (verifiedRuneWakerParticleModels.has(result.model) && !result.partialRendererReady) { assert.equal(result.rendererReady, true, result.model); } assert.equal(graph.shader.name, "ruSHADER_PARTICLE_SPRITEEX"); for (const [stage, signature] of [["vertex", "VS_OUTPUT main"], ["pixel", "PS_OUTPUT main"]]) { const shader = fs.readFileSync(assetFile(graph.shader[`${stage}SourceUrl`]), "utf8"); assert.ok(shader.includes(signature)); assert.ok(shader.endsWith("\n")); assert.ok(!shader.trimEnd().endsWith("\\n\\")); } assert.equal(graph.textures.length, result.textures); spriteControllers += result.spriteControllers; for (const texture of graph.textures) { const png = fs.readFileSync(assetFile(texture.url)); assert.equal(png.subarray(0, 8).toString("hex"), "89504e470d0a1a0a"); textureReferences++; } for (const motion of graph.motions) for (const controller of motion.controllers) { if (controller.type === "sprite" && controller.textureAssetUrl) { assert.ok(graph.textures.some((texture) => texture.url === controller.textureAssetUrl)); assert.equal(controller.shader, "ruSHADER_PARTICLE_SPRITEEX"); assert.ok(Array.isArray(controller.tileSizePixels)); } if (controller.type === "emitter") assert.equal(controller.tracks.length, 27); } } assert.equal(spriteControllers, 668); assert.ok(textureReferences >= 139); }); test("Rising Tide ships three original renderable phases and Urgent Heal declares its partial target layers", () => { for (const model of [ "model/fx/skill/ice/act_icecone01_c_hand.ros", "model/fx/skill/ice/act_icecone01_f.ros", "model/fx/skill/ice/act_riptide_t_freeze_hit.ros", ]) { const result = index.results.find((entry) => entry.model === model); assert.equal(result?.rendererReady, true, model); assert.equal(result?.partialRendererReady, false, model); } const urgent = index.results.find((entry) => entry.model === "model/fx/skill/holy/act_benedictory_heal_t_down.ros"); assert.equal(urgent?.rendererReady, false); assert.equal(urgent?.partialRendererReady, true); assert.equal(urgent?.spriteControllers, 2); const graph = JSON.parse(fs.readFileSync(assetFile(urgent.graphUrl), "utf8")); assert.equal(graph.visualFidelity, "partial-original"); assert.deepEqual(graph.omittedVisuals, ["CRuEntity_Decal:terrain-projection"]); assert.equal(graph.runeMesh.sourceModel, "model/fx/skill/holy/_polygon/rune_cylinder.ros"); assert.equal(graph.runeMesh.positions.length, 72 * 9); assert.equal(graph.runeMesh.uvs.length, 72 * 6); assert.ok(graph.textures.some((texture) => texture.url === graph.runeMesh.textureAssetUrl)); const impactControllers = graph.motions.find((motion) => motion.name === "explode").controllers; assert.ok(impactControllers.some((controller) => controller.type === "entity-visualizer")); const decal = impactControllers.find((controller) => controller.type === "decal"); assert.ok(decal); assert.ok(graph.textures.some((texture) => texture.url === decal.textureAssetUrl)); assert.ok(impactControllers.some((controller) => controller.type === "global-color")); assert.ok(impactControllers.some((controller) => controller.type === "global-alpha")); });