import type { GenderId } from "../app/characterCatalog"; import type { CharacterWeaponStance } from "../game/combatAnimation"; import type { InventoryItem } from "../game/lootTypes"; import type { TextureRegion } from "./characterManifest"; const ASSET_ROOT = "/assets/equipment/wow335a-v1"; const TEXTURE_ROOT = `${ASSET_ROOT}/textures`; const ATLAS_REGIONS = Object.freeze({ armUpper: [0, 0, 256, 128] as const, armLower: [0, 128, 256, 64] as const, hand: [0, 192, 256, 64] as const, torsoUpper: [256, 0, 256, 128] as const, torsoLower: [256, 128, 256, 64] as const, legUpper: [256, 192, 256, 128] as const, legLower: [256, 320, 256, 64] as const, foot: [256, 384, 256, 128] as const, } satisfies Readonly>); export type EquipmentAttachmentName = | "shield" | "handRight" | "handLeft" | "shoulderRight" | "shoulderLeft"; export interface EquipmentTextureLayer { readonly url: string; readonly region: TextureRegion; } export interface EquipmentModelVisual { readonly itemId: number; readonly name: string; readonly url: string; readonly attachment: EquipmentAttachmentName; readonly fallbackKind?: "bow" | "dagger" | "mace" | "shield" | "staff" | "sword"; readonly weaponStance?: CharacterWeaponStance; } interface GenderedTextureLayer { readonly region: TextureRegion; readonly url?: string; readonly male?: string; readonly female?: string; } interface EquipmentVisualDefinition { readonly layers?: readonly GenderedTextureLayer[]; readonly layerOrder?: number; readonly geosets?: Readonly>; readonly cloakUrl?: string; readonly models?: readonly Omit[]; } export interface ResolvedEquipmentAppearance { readonly cacheKey: string; readonly bodyLayers: readonly EquipmentTextureLayer[]; readonly cloakUrl: string; readonly geosets: Readonly>; readonly models: readonly EquipmentModelVisual[]; } const texture = (path: string): string => `${TEXTURE_ROOT}/${path}`; const model = (path: string): string => `${ASSET_ROOT}/${path}/${path}.glb`; const EQUIPMENT_VISUALS: Readonly> = Object.freeze({ 5243: { models: [{ name: "Firebelcher", url: model("firebelcher-5243"), attachment: "handRight", fallbackKind: "mace", weaponStance: "one-handed", }], }, 5404: { models: [ { name: "Serpent's Shoulders (left)", url: model("serpents-shoulders-5404-left"), attachment: "shoulderLeft", }, { name: "Serpent's Shoulders (right)", url: model("serpents-shoulders-5404-right"), attachment: "shoulderRight", }, ], }, 5970: { layers: [ { url: texture("5970-serpent-gloves/arm-lower.png"), region: ATLAS_REGIONS.armLower, }, { url: texture("5970-serpent-gloves/hand.png"), region: ATLAS_REGIONS.hand, }, ], layerOrder: 20, geosets: { Gloves: 2 }, }, 6447: { models: [{ name: "Worn Turtle Shell Shield", url: model("worn-turtle-shell-shield-6447"), attachment: "shield", fallbackKind: "shield", weaponStance: "one-handed", }], }, 6448: { models: [{ name: "Tail Spike", url: model("tail-spike-6448"), attachment: "handRight", fallbackKind: "dagger", weaponStance: "one-handed", }], }, 6449: { cloakUrl: texture("6449-glowing-lizardscale-cloak/cape.png"), geosets: { Cloak: 6 }, }, 6459: { layers: [ { url: texture("6459-savage-trodders/leg-lower.png"), region: ATLAS_REGIONS.legLower, }, { url: texture("6459-savage-trodders/foot.png"), region: ATLAS_REGIONS.foot, }, ], layerOrder: 11, geosets: { Boots: 1 }, }, 6460: { layers: [{ url: texture("6460-cobrahns-grasp/leg-upper.png"), region: ATLAS_REGIONS.legUpper, }], layerOrder: 18, geosets: { Belt: 1 }, }, 6461: { models: [ { name: "Slime-Encrusted Pads (left)", url: model("slime-encrusted-pads-6461-left"), attachment: "shoulderLeft", }, { name: "Slime-Encrusted Pads (right)", url: model("slime-encrusted-pads-6461-right"), attachment: "shoulderRight", }, ], }, 6465: { layers: [ { url: texture("6465-robe-of-the-moccasin/arm-upper.png"), region: ATLAS_REGIONS.armUpper, }, { url: texture("6465-robe-of-the-moccasin/arm-lower.png"), region: ATLAS_REGIONS.armLower, }, { male: texture("6465-robe-of-the-moccasin/torso-upper-male.png"), female: texture("6465-robe-of-the-moccasin/torso-upper-female.png"), region: ATLAS_REGIONS.torsoUpper, }, { male: texture("6465-robe-of-the-moccasin/torso-lower-male.png"), female: texture("6465-robe-of-the-moccasin/torso-lower-female.png"), region: ATLAS_REGIONS.torsoLower, }, { url: texture("6465-robe-of-the-moccasin/leg-upper.png"), region: ATLAS_REGIONS.legUpper, }, { url: texture("6465-robe-of-the-moccasin/leg-lower.png"), region: ATLAS_REGIONS.legLower, }, ], layerOrder: 13, geosets: { Sleeves: 3, Chest: 1, Trousers: 2 }, }, 6469: { models: [{ name: "Venomstrike", url: model("venomstrike-6469"), attachment: "handLeft", fallbackKind: "bow", weaponStance: "bow", }], }, 6472: { models: [{ name: "Stinging Viper", url: model("stinging-viper-6472"), attachment: "handRight", fallbackKind: "mace", weaponStance: "one-handed", }], }, 6473: { layers: [ { url: texture("6473-armor-of-the-fang/torso-upper.png"), region: ATLAS_REGIONS.torsoUpper, }, { url: texture("6473-armor-of-the-fang/torso-lower.png"), region: ATLAS_REGIONS.torsoLower, }, ], layerOrder: 13, geosets: { Sleeves: 1, Chest: 1, Trousers: 1 }, }, 6627: { layers: [ { url: texture("6627-mutant-scale-breastplate/arm-upper.png"), region: ATLAS_REGIONS.armUpper, }, { url: texture("6627-mutant-scale-breastplate/torso-upper.png"), region: ATLAS_REGIONS.torsoUpper, }, { url: texture("6627-mutant-scale-breastplate/torso-lower.png"), region: ATLAS_REGIONS.torsoLower, }, ], layerOrder: 13, geosets: { Sleeves: 1, Chest: 1, Trousers: 1 }, }, 6629: { cloakUrl: texture("6629-sporid-cape/cape.png"), geosets: { Cloak: 3 }, }, 6630: { models: [{ name: "Seedcloud Buckler", url: model("seedcloud-buckler-6630"), attachment: "shield", fallbackKind: "shield", weaponStance: "one-handed", }], }, 6631: { models: [{ name: "Living Root", url: model("living-root-6631"), attachment: "handRight", fallbackKind: "staff", weaponStance: "two-handed-left", }], }, 6632: { cloakUrl: texture("6632-feyscale-cloak/cape.png"), geosets: { Cloak: 6 }, }, 10410: { layers: [ { url: texture("10410-leggings-of-the-fang/leg-upper.png"), region: ATLAS_REGIONS.legUpper, }, { url: texture("10410-leggings-of-the-fang/leg-lower.png"), region: ATLAS_REGIONS.legLower, }, ], layerOrder: 10, geosets: { Pants: 1, Kneepads: 1, Trousers: 1 }, }, 10411: { layers: [ { url: texture("10411-footpads-of-the-fang/leg-lower.png"), region: ATLAS_REGIONS.legLower, }, { url: texture("10411-footpads-of-the-fang/foot.png"), region: ATLAS_REGIONS.foot, }, ], layerOrder: 11, geosets: { Boots: 1 }, }, 10412: { layers: [{ url: texture("10412-belt-of-the-fang/leg-upper.png"), region: ATLAS_REGIONS.legUpper, }], layerOrder: 18, geosets: { Belt: 1 }, }, 13245: { models: [{ name: "Kresh's Back", url: model("kreshs-back-13245"), attachment: "shield", fallbackKind: "shield", weaponStance: "one-handed", }], }, }); export function resolveEquipmentAppearance( items: readonly InventoryItem[], gender: GenderId, ): ResolvedEquipmentAppearance { const layers: EquipmentTextureLayer[] = []; const models: EquipmentModelVisual[] = []; const occupiedAttachments = new Set(); const geosets: Record = { Cloak: 0 }; let cloakUrl = ""; const equippedVisuals = items.flatMap((item) => { const visual = EQUIPMENT_VISUALS[item.itemId]; return visual ? [{ item, visual }] : []; }); for (const { item, visual } of equippedVisuals .slice() .sort((left, right) => (left.visual.layerOrder ?? 0) - (right.visual.layerOrder ?? 0))) { for (const layer of visual.layers ?? []) { const url = layer.url ?? layer[gender]; if (url) layers.push({ url, region: layer.region }); } Object.assign(geosets, visual.geosets); if (visual.cloakUrl) cloakUrl = visual.cloakUrl; for (const itemModel of visual.models ?? []) { if (occupiedAttachments.has(itemModel.attachment)) continue; occupiedAttachments.add(itemModel.attachment); models.push({ ...itemModel, itemId: item.itemId }); } } return { cacheKey: items.map((item) => item.itemId).join(","), bodyLayers: layers, cloakUrl, geosets, models, }; } export function equipmentModelUrls(): readonly string[] { return Object.values(EQUIPMENT_VISUALS) .flatMap((visual) => visual.models?.map((itemModel) => itemModel.url) ?? []); } export function equipmentAssetUrls(): readonly string[] { const urls = new Set(); for (const visual of Object.values(EQUIPMENT_VISUALS)) { for (const itemModel of visual.models ?? []) urls.add(itemModel.url); for (const layer of visual.layers ?? []) { if (layer.url) urls.add(layer.url); if (layer.male) urls.add(layer.male); if (layer.female) urls.add(layer.female); } if (visual.cloakUrl) urls.add(visual.cloakUrl); } return [...urls]; }