Guard source gear namespaces and retain recoverable legacy records (#9)

This commit is contained in:
warren committed 2026-10-10 11:36:10 -04:00
1 parent ab37006932
commit e567910c04
23 files changed
+1203 -66

No files matched your search

+19 -2
View File
@@ -1,10 +1,12 @@
import {
contentCategoryForClass,
classById,
isRomClassId,
type ClassId,
} from "../app/characterCatalog";
import {
classCanEquipItem,
classItemShapeFitsEquipmentRules,
EQUIPMENT_SLOTS,
equipmentSlotForItem,
type EquipmentAssignments,
@@ -94,6 +96,11 @@ const ARMOR_SLOTS = new Set<EquipmentSlot>([
const CATALOG_ITEMS = LOOT_CATALOG.bosses.flatMap((boss) => boss.items);
const itemCache = new Map<string, InventoryItem>();
// Training placeholders are generated internally and are never serialized as
// earned inventory. Keep their legacy simulator projection private so it can
// preserve the existing RoM training profile without making direct WoW stats
// valid on a caller-supplied RoM item.
const romTrainingItems = new WeakSet<InventoryItem>();
function effectiveLevel(context: HeirloomContext): number {
const cap = context.ownerKind === "party" ? PARTY_HEIRLOOM_LEVEL_CAP : PLAYER_HEIRLOOM_LEVEL_CAP;
@@ -181,6 +188,7 @@ function weaponStats(slot: EquipmentSlot, inventorySlot: string, weaponType: str
function syntheticCatalogItem(template: LootCatalogItem, itemLevel: number): InventoryItem {
return {
contentCategory: "wow",
instanceId: `catalog:${template.itemId}`,
itemId: template.itemId,
itemKey: template.itemKey,
@@ -203,7 +211,9 @@ function highestCatalogScore(classId: ClassId, slot: EquipmentSlot, level: numbe
let highest = 0;
for (const template of CATALOG_ITEMS) {
const candidate = syntheticCatalogItem(template, level);
if (equipmentSlotForItem(candidate) !== slot || !classCanEquipItem(classId, candidate)) continue;
// Catalog comparison is an internal shape-only specialization baseline.
// These WoW templates never enter inventory or an equipment assignment.
if (equipmentSlotForItem(candidate) !== slot || !classItemShapeFitsEquipmentRules(classId, candidate)) continue;
highest = Math.max(highest, specializationScore(candidate.stats, profile));
}
return highest;
@@ -286,6 +296,7 @@ function heirloomItem(
// internal quality so this label change cannot alter saved builds or budgets.
const romTrainingGear = isRomClassId(context.classId);
const result: InventoryItem = Object.freeze({
contentCategory: contentCategoryForClass(context.classId),
instanceId: `heirloom:${context.ownerId}:${profile.id}:${slot}`,
itemId: 9_000_000 + stableNumber % 900_000,
itemKey: `heirloom-${profile.id.replace(/:/g, "-")}-${slot}`,
@@ -302,6 +313,7 @@ function heirloomItem(
sourceBossName: romTrainingGear ? "Training Armory" : "Heirloom Armory",
acquiredAt: 0,
});
if (romTrainingGear) romTrainingItems.add(result);
itemCache.set(cacheKey, result);
return result;
}
@@ -348,7 +360,12 @@ export function equipmentStatsForItems(items: readonly InventoryItem[]): ItemSta
const nativePayloads = [] as NonNullable<ItemStats["romNative"]>[];
for (const item of items) {
for (const key of ITEM_STAT_KEYS) numeric[key] += item.stats[key];
const nativePayload = item.stats.romNative ?? romNativeGearFromSharedDirectStats(item.stats);
const nativePayload = item.stats.romNative
?? (romTrainingItems.has(item)
|| item.contentCategory === undefined
|| item.contentCategory === "wow"
? romNativeGearFromSharedDirectStats(item.stats)
: undefined);
if (nativePayload) nativePayloads.push(nativePayload);
if (item.stats.meleeWeapon) meleeWeapon = item.stats.meleeWeapon;
if (item.stats.rangedWeapon) rangedWeapon = item.stats.rangedWeapon;