Files
healer-man/src/avatar/appearanceResolver.test.ts
T
2026-08-14 15:56:39 -04:00

130 lines
4.4 KiB
TypeScript

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);
});
});