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 = { 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"); }); });