Files
healer-man/src/scene/wowTerrainMaterial.test.ts
T
2026-10-07 13:05:04 -04:00

101 lines
5.7 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { Group, Mesh, MeshStandardMaterial, NoColorSpace, PlaneGeometry, RepeatWrapping, SRGBColorSpace, Texture } from "three";
import type { GLTF, GLTFParser } from "three-stdlib";
import { bindWowTerrainMaterial, createWowTerrainPlugin } from "./wowTerrainMaterial";
import { prepareDungeonMaterial } from "./productionDungeonMaterials";
import { applyDungeonMeshGraphics } from "./graphicsMaterialQuality";
import { disposeObject3DResources, materialTextures } from "./threeResourceDisposal";
const roles = { cutoutPatterns: [], cutoutNames: [], additivePatterns: [], additiveNames: [], blendPatterns: [], blendNames: [] };
const metadata = {
schemaVersion: 1, sourceAdt: { path: "world/maps/tanarisinstance/tanarisinstance_30_29.adt", sha256: "a".repeat(64) },
layerVariants: ["dirt", "rock"], weightVariants: ["chunk-alpha"],
uv: { attribute: "TEXCOORD_0", basis: "mcnk-local-normalized", diffuseScale: [8, 8], weightScale: [1, 1], vSign: 1 },
composition: { rule: "wow-overlay-residual", normalize: false },
};
function fixture() {
const layers = [new Texture(), new Texture()];
layers.forEach(texture => {
texture.colorSpace = SRGBColorSpace;
texture.wrapS = RepeatWrapping;
texture.wrapT = RepeatWrapping;
});
const weights = new Texture();
const material = new MeshStandardMaterial({ map: layers[0] });
material.userData.wowTerrain = structuredClone(metadata);
const scene = new Group();
scene.add(new Mesh(new PlaneGeometry(), material));
const carriers = [new MeshStandardMaterial({ map: layers[0] }), new MeshStandardMaterial({ map: layers[1] }),
new MeshStandardMaterial({ aoMap: weights })];
const parser: Pick<GLTFParser, "json" | "getDependency"> = {
json: { extensions: { KHR_materials_variants: { variants: ["dirt", "rock", "chunk-alpha"].map(name => ({ name })) } },
meshes: [{ primitives: [{ extensions: { KHR_materials_variants: {
mappings: carriers.map((_, index) => ({ material: index, variants: [index] })),
} } }] }] },
getDependency: vi.fn(async (_kind, index) => carriers[index]),
};
return { layers, weights, material, scene, carriers, parser };
}
describe("source-backed WoW terrain materials", () => {
it("keeps native tile detail and linear blend data through production cloning, filtering and disposal", async () => {
const { layers, weights, material, scene, parser } = fixture();
await createWowTerrainPlugin(parser).afterRoot!({ scene } as GLTF);
const clone = prepareDungeonMaterial(material, roles, "adt-hybrid");
const allTextures = [...layers, weights];
expect(new Set(materialTextures(clone))).toEqual(new Set(allTextures));
expect(layers.every(texture => texture.colorSpace === SRGBColorSpace)).toBe(true);
expect(weights.colorSpace).toBe(NoColorSpace);
expect(clone.customProgramCacheKey()).toBe(material.customProgramCacheKey());
const root = new Group();
const geometry = new PlaneGeometry();
root.add(new Mesh(geometry, clone), new Mesh(geometry, clone));
applyDungeonMeshGraphics(root, "environment", "high", 16, 8);
expect(allTextures.every(texture => texture.anisotropy === 8)).toBe(true);
const disposals = allTextures.map(texture => vi.spyOn(texture, "dispose"));
expect(disposeObject3DResources(root)).toEqual({ geometries: 1, materials: 1, textures: 3, skeletons: 0 });
disposals.forEach(dispose => expect(dispose).toHaveBeenCalledOnce());
});
it("rejects unverified sources and unsupported normalized/reversed blends before loading carriers", async () => {
for (const change of [
(data: typeof metadata) => { data.sourceAdt.sha256 = "unknown"; },
(data: typeof metadata) => { data.composition.normalize = true; },
(data: typeof metadata) => { data.uv.vSign = -1; },
]) {
const { material, scene, parser } = fixture();
change(material.userData.wowTerrain);
await expect(createWowTerrainPlugin(parser).afterRoot!({ scene } as GLTF)).rejects.toThrow("Unsupported or unverified");
expect(parser.getDependency).not.toHaveBeenCalled();
}
});
it("requires the linear AO carrier for native weights and refuses missing or ambiguous variants", async () => {
const first = fixture();
first.carriers[2].aoMap = null;
first.carriers[2].map = first.weights;
await expect(createWowTerrainPlugin(first.parser).afterRoot!({ scene: first.scene } as GLTF)).rejects.toThrow("Missing native WoW terrain image");
const second = fixture();
second.parser.json.meshes[0].primitives[0].extensions.KHR_materials_variants.mappings.pop();
await expect(createWowTerrainPlugin(second.parser).afterRoot!({ scene: second.scene } as GLTF)).rejects.toThrow("Missing native WoW terrain carrier");
const third = fixture();
third.parser.json.meshes[0].primitives[0].extensions.KHR_materials_variants.mappings.push({ material: 1, variants: [0] });
await expect(createWowTerrainPlugin(third.parser).afterRoot!({ scene: third.scene } as GLTF)).rejects.toThrow("ambiguous native WoW");
});
it("supports base-only native chunks and refuses an incomplete multilayer binding", () => {
const material = new MeshStandardMaterial();
const base = new Texture();
base.colorSpace = SRGBColorSpace;
base.wrapS = RepeatWrapping;
base.wrapT = RepeatWrapping;
bindWowTerrainMaterial(material, { layers: [base] });
expect(materialTextures(material)).toEqual([base]);
expect(() => bindWowTerrainMaterial(material, { layers: [base, new Texture()] })).toThrow("layer layout");
const invalidWeights = new Texture();
invalidWeights.colorSpace = SRGBColorSpace;
expect(() => bindWowTerrainMaterial(material, { layers: [base, base], weights: invalidWeights })).toThrow("clamped linear blend");
});
});