import { describe, expect, it } from "vitest"; import type { CharacterAppearance } from "../app/characterCatalog"; import { isAvatarGeosetVisible, resolveAvatarAppearance, } from "./appearanceResolver"; import type { AvatarManifest, AvatarManifestEntry } from "./characterManifest"; const manifest: AvatarManifest = { version: 1, sourceBuild: "test", publicRoot: "/avatars", textureLayout: { width: 512, height: 512, regions: { base: [0, 0, 512, 512], faceUpper: [0, 320, 256, 64], faceLower: [0, 384, 256, 128], torsoUpper: [256, 0, 256, 128], pelvisUpper: [256, 192, 256, 128], }, }, entries: {}, }; const entry: AvatarManifestEntry = { raceId: "human", gender: "male", dbcRaceId: 1, dbcSexId: 0, sourceModel: "Character/Human/Male/HumanMale.m2", model: "/avatars/human/male/rig.glb", bytes: 1, scale: 1, groundOffset: 0, rotationY: 0, materialTokens: { body: "body", hair: "hair" }, animations: { idle: "Stand", walk: "Walk", run: "Run" }, counts: { skinColor: 2, face: 2, hairStyle: 2, hairColor: 2, feature: 2 }, dbcIndices: { skinColor: [0, 1], face: [0, 1], hairStyle: [0, 1], hairColor: [0, 1], feature: [0, 1], }, skinOptions: [ { dbcColor: 0, base: "skin-0", underwear: ["pelvis-0", "torso-0", ""] }, { dbcColor: 1, base: "skin-1", underwear: ["pelvis-1", "torso-1", ""] }, ], faceOptions: [ { dbcVariation: 0, bySkin: [["face-lower-00", "face-upper-00", ""], ["face-lower-01", "face-upper-01", ""]] }, { dbcVariation: 1, bySkin: [["face-lower-10", "face-upper-10", ""], ["face-lower-11", "face-upper-11", ""]] }, ], hairOptions: [ { dbcVariation: 0, geoset: 2, showScalp: false, byColor: [["hair-geometry-0", "scalp-lower-0", "scalp-upper-0"], ["hair-geometry-1", "scalp-lower-1", "scalp-upper-1"]] }, { dbcVariation: 1, geoset: 7, showScalp: false, byColor: [["hair2-geometry-0", "hair2-lower-0", "hair2-upper-0"], ["hair2-geometry-1", "hair2-lower-1", "hair2-upper-1"]] }, ], featureOptions: [ { dbcVariation: 0, geosets: [1, 2, 3, 4, 5], byColor: [["feature-lower-0", "feature-upper-0", ""], ["feature-lower-1", "feature-upper-1", ""]] }, { dbcVariation: 1, geosets: [6, 7, 8, 9, 2], byColor: [["feature2-lower-0", "feature2-upper-0", ""], ["feature2-lower-1", "feature2-upper-1", ""]] }, ], }; const selected: CharacterAppearance = { skinColor: 1, face: 1, hairStyle: 1, hairColor: 1, feature: 1, }; describe("resolveAvatarAppearance", () => { it("composes body layers in WoW replacement-texture order", () => { const resolved = resolveAvatarAppearance(manifest, entry, selected, "priest"); expect(resolved.bodyLayers.map((layer) => layer.url)).toEqual([ "skin-1", "pelvis-1", "torso-1", "face-lower-11", "face-upper-11", "feature2-lower-1", "feature2-upper-1", "hair2-lower-1", "hair2-upper-1", ]); expect(resolved.hairUrl).toBe("hair2-geometry-1"); }); it("clamps bad saved values to the nearest exported option", () => { const resolved = resolveAvatarAppearance(manifest, entry, { skinColor: 99, face: -4, hairStyle: 99, hairColor: -8, feature: 99, }); expect(resolved.cacheKey).toBe("human:male:1:0:1:0:1"); expect(resolved.bodyLayers[0]?.url).toBe("skin-1"); expect(resolved.hairUrl).toBe("hair2-geometry-0"); }); it("maps hair and facial choices to named rig geosets", () => { const resolved = resolveAvatarAppearance(manifest, entry, selected, "priest"); expect(resolved.geosets).toMatchObject({ Hair: 7, FacialA: 6, FacialB: 8, FacialC: 7, FacialJewelry: 9, Eyeglow: 2, Ears: 2, }); expect(isAvatarGeosetVisible("rig_Hair7", resolved)).toBe(true); expect(isAvatarGeosetVisible("rig_Hair2", resolved)).toBe(false); expect(isAvatarGeosetVisible("rig_FacialJewelry9", resolved)).toBe(true); expect(isAvatarGeosetVisible("rig_Geoset42", resolved)).toBe(true); expect(isAvatarGeosetVisible("Armature", resolved)).toBe(true); }); it("uses the Death Knight eye-glow geoset without changing other choices", () => { const normal = resolveAvatarAppearance(manifest, entry, selected, "priest"); const deathKnight = resolveAvatarAppearance(manifest, entry, selected, "death-knight"); expect(normal.geosets.Eyeglow).toBe(2); expect(deathKnight.geosets.Eyeglow).toBe(3); expect(deathKnight.geosets.Hair).toBe(normal.geosets.Hair); }); });