376 lines
10 KiB
TypeScript
376 lines
10 KiB
TypeScript
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<Record<string, TextureRegion>>);
|
|
|
|
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<Record<string, number>>;
|
|
readonly cloakUrl?: string;
|
|
readonly models?: readonly Omit<EquipmentModelVisual, "itemId">[];
|
|
}
|
|
|
|
export interface ResolvedEquipmentAppearance {
|
|
readonly cacheKey: string;
|
|
readonly bodyLayers: readonly EquipmentTextureLayer[];
|
|
readonly cloakUrl: string;
|
|
readonly geosets: Readonly<Record<string, number>>;
|
|
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<Record<number, EquipmentVisualDefinition>> = 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<EquipmentAttachmentName>();
|
|
const geosets: Record<string, number> = { 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<string>();
|
|
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];
|
|
}
|