Files
healer-man/src/game/lootTypes.ts
T

123 lines
4.9 KiB
TypeScript

import {
normalizeItemStats,
MAX_PERSISTED_ITEM_LEVEL,
type ItemQuality,
type ItemStats,
} from "./itemStats";
import { normalizeRomNativeGearStats } from "./romNativeStatsTypes";
import type { ContentCategoryId } from "../app/contentCategories";
export type { ItemQuality, ItemStats } from "./itemStats";
/** A concrete item owned by a character, distinct from its SQL item template. */
export interface InventoryItem {
/** Source-game namespace. Missing legacy metadata is normalized with the owning profile. */
readonly contentCategory?: ContentCategoryId | null;
readonly instanceId: string;
readonly itemId: number;
readonly itemKey: string;
readonly name: string;
readonly quality: ItemQuality;
readonly inventorySlot: string;
readonly armorType: string | null;
readonly weaponType: string | null;
/** Scaled at boss death from the killing player's level. */
readonly itemLevel: number;
/** Original 3.3.5a item level retained for provenance and future balancing. */
readonly sourceItemLevel: number;
/** Concrete scaled attributes captured when the boss drops this item. */
readonly stats: ItemStats;
readonly quantity: number;
readonly sourceBossKey: string;
readonly sourceBossName: string;
readonly acquiredAt: number;
}
const ITEM_QUALITIES = new Set<ItemQuality>(["common", "uncommon", "rare", "epic", "legendary"]);
export interface NormalizeInventoryOptions {
/** Saved character/party owner; dungeon and loot-source identities are not owner categories. */
readonly ownerCategory?: ContentCategoryId;
}
const CONTENT_CATEGORIES = new Set<ContentCategoryId>(["wow", "rom", "coa"]);
/** Returns true only for an explicit item/owner namespace match. */
export function inventoryItemMatchesCategory(
item: Pick<InventoryItem, "contentCategory">,
expectedCategory: ContentCategoryId | undefined,
): boolean {
return expectedCategory !== undefined && item.contentCategory === expectedCategory;
}
/** A RoM item is usable only when it carries nonempty validated native values. */
export function inventoryItemUsableForCategory(
item: Pick<InventoryItem, "contentCategory" | "stats">,
expectedCategory: ContentCategoryId | undefined,
): boolean {
if (!inventoryItemMatchesCategory(item, expectedCategory)) return false;
if (expectedCategory !== "rom") return true;
const native = normalizeRomNativeGearStats(item.stats?.romNative);
return Boolean(native && Object.keys(native).length > 0);
}
/** Safely migrates persisted profiles created before inventory existed. */
export function normalizeInventory(
value: unknown,
options: NormalizeInventoryOptions = {},
): InventoryItem[] {
if (!Array.isArray(value)) return [];
const normalized: InventoryItem[] = [];
for (const candidate of value) {
if (!candidate || typeof candidate !== "object") continue;
const item = candidate as Partial<InventoryItem>;
if (
typeof item.instanceId !== "string"
|| !Number.isFinite(item.itemId)
|| typeof item.itemKey !== "string"
|| typeof item.name !== "string"
|| !ITEM_QUALITIES.has(item.quality as ItemQuality)
|| typeof item.inventorySlot !== "string"
|| !Number.isFinite(item.itemLevel)
|| !Number.isFinite(item.sourceItemLevel)
|| !Number.isFinite(item.quantity)
|| typeof item.sourceBossKey !== "string"
|| typeof item.sourceBossName !== "string"
|| !Number.isFinite(item.acquiredAt)
) continue;
const itemLevel = Math.max(1, Math.min(MAX_PERSISTED_ITEM_LEVEL, Math.trunc(item.itemLevel!)));
const contentCategory = CONTENT_CATEGORIES.has(item.contentCategory as ContentCategoryId)
? item.contentCategory as ContentCategoryId
: item.contentCategory === undefined && options.ownerCategory === "wow"
? "wow"
: null;
normalized.push({
contentCategory,
instanceId: item.instanceId,
itemId: Math.max(1, Math.trunc(item.itemId!)),
itemKey: item.itemKey,
name: item.name,
quality: item.quality as ItemQuality,
inventorySlot: item.inventorySlot,
armorType: typeof item.armorType === "string" ? item.armorType : null,
weaponType: typeof item.weaponType === "string" ? item.weaponType : null,
itemLevel,
sourceItemLevel: Math.max(1, Math.trunc(item.sourceItemLevel!)),
stats: normalizeItemStats(item.stats, {
itemId: Math.max(1, Math.trunc(item.itemId!)),
quality: item.quality as ItemQuality,
inventorySlot: item.inventorySlot,
armorType: typeof item.armorType === "string" ? item.armorType : null,
weaponType: typeof item.weaponType === "string" ? item.weaponType : null,
itemLevel,
sourceItemLevel: Math.max(1, Math.trunc(item.sourceItemLevel!)),
}),
quantity: Math.max(1, Math.trunc(item.quantity!)),
sourceBossKey: item.sourceBossKey,
sourceBossName: item.sourceBossName,
acquiredAt: Math.max(0, Math.trunc(item.acquiredAt!)),
});
}
return normalized;
}