130 lines
4.4 KiB
TypeScript
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);
|
|
});
|
|
});
|