forked from phenom/healer-man
Guard source gear namespaces and retain recoverable legacy records (#9)
This commit is contained in:
1 parent
ab37006932
commit
e567910c04
23 files changed
+1203
-66
No files matched your search
+42
-4
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
baseClassFor,
|
||||
classById,
|
||||
contentCategoryForClass,
|
||||
type BaseClassId,
|
||||
type ClassId,
|
||||
} from "../app/characterCatalog";
|
||||
import type { ContentCategoryId } from "../app/contentCategories";
|
||||
import {
|
||||
EMPTY_ITEM_STATS,
|
||||
ITEM_STAT_KEYS,
|
||||
@@ -11,7 +13,7 @@ import {
|
||||
type NumericItemStatKey,
|
||||
type WeaponStats,
|
||||
} from "./itemStats";
|
||||
import type { InventoryItem } from "./lootTypes";
|
||||
import { inventoryItemUsableForCategory, type InventoryItem } from "./lootTypes";
|
||||
import { romNativeGearFromSharedDirectStats, sumRomNativeGearStats } from "./romNativeStatsTypes";
|
||||
|
||||
export const PLAYER_EQUIPMENT_OWNER_ID = "player";
|
||||
@@ -48,6 +50,8 @@ export interface EquipmentChange {
|
||||
export interface EquipmentClassContext {
|
||||
readonly secondaryClassId?: ClassId | null;
|
||||
readonly level?: number;
|
||||
/** Saved owner category; falls back to the owner's class category when absent. */
|
||||
readonly ownerCategory?: ContentCategoryId;
|
||||
}
|
||||
|
||||
const equipmentSlotSet = new Set<string>(EQUIPMENT_SLOTS);
|
||||
@@ -82,7 +86,15 @@ export function equipmentSlotForItem(item: InventoryItem): EquipmentSlot | null
|
||||
return equipmentSlotSet.has(slot) ? slot as EquipmentSlot : null;
|
||||
}
|
||||
|
||||
export function classCanEquipItem(classId: ClassId, item: InventoryItem, context: EquipmentClassContext = {}): boolean {
|
||||
/**
|
||||
* Internal shape-only predicate for synthetic catalog scoring. Callers must
|
||||
* still use classCanEquipItem before assigning or applying an item.
|
||||
*/
|
||||
export function classItemShapeFitsEquipmentRules(
|
||||
classId: ClassId,
|
||||
item: InventoryItem,
|
||||
context: EquipmentClassContext = {},
|
||||
): boolean {
|
||||
const slot = equipmentSlotForItem(item);
|
||||
if (!slot) return false;
|
||||
const level = context.level ?? Number.POSITIVE_INFINITY;
|
||||
@@ -111,6 +123,13 @@ export function classCanEquipItem(classId: ClassId, item: InventoryItem, context
|
||||
return true;
|
||||
}
|
||||
|
||||
export function classCanEquipItem(classId: ClassId, item: InventoryItem, context: EquipmentClassContext = {}): boolean {
|
||||
const classCategory = contentCategoryForClass(classId);
|
||||
if (context.ownerCategory !== undefined && context.ownerCategory !== classCategory) return false;
|
||||
return inventoryItemUsableForCategory(item, classCategory)
|
||||
&& classItemShapeFitsEquipmentRules(classId, item, context);
|
||||
}
|
||||
|
||||
function mutableAssignments(assignments: EquipmentAssignments): Record<string, EquipmentLoadout> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(assignments).map(([ownerId, loadout]) => [ownerId, { ...loadout }]),
|
||||
@@ -141,6 +160,14 @@ export function equipInventoryItem(
|
||||
): EquipmentChange {
|
||||
const item = inventory.find((candidate) => candidate.instanceId === instanceId);
|
||||
if (!item) return { ok: false, assignments, reason: "That item is no longer in your inventory." };
|
||||
const ownerCategory = contentCategoryForClass(ownerClassId);
|
||||
if ((classContext.ownerCategory !== undefined && classContext.ownerCategory !== ownerCategory)
|
||||
|| !inventoryItemUsableForCategory(item, ownerCategory)) {
|
||||
const reason = ownerCategory === "rom" && item.contentCategory === "rom"
|
||||
? "That item has no validated RuneWaker-native gear values."
|
||||
: "That item belongs to a different game category.";
|
||||
return { ok: false, assignments, reason };
|
||||
}
|
||||
const slot = equipmentSlotForItem(item);
|
||||
if (!slot) return { ok: false, assignments, reason: `${item.name} does not use an equippable slot.` };
|
||||
if (!classCanEquipItem(ownerClassId, item, classContext)) {
|
||||
@@ -206,6 +233,7 @@ export function equipmentItemsForOwner(
|
||||
export function normalizeEquipment(
|
||||
value: unknown,
|
||||
inventory: readonly InventoryItem[],
|
||||
ownerCategory?: ContentCategoryId,
|
||||
): EquipmentAssignments {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
||||
const itemById = new Map(inventory.map((item) => [item.instanceId, item]));
|
||||
@@ -219,6 +247,7 @@ export function normalizeEquipment(
|
||||
if (!equipmentSlotSet.has(rawSlot) || typeof rawInstanceId !== "string" || assigned.has(rawInstanceId)) continue;
|
||||
const item = itemById.get(rawInstanceId);
|
||||
if (!item || equipmentSlotForItem(item) !== rawSlot) continue;
|
||||
if (ownerCategory !== undefined && !inventoryItemUsableForCategory(item, ownerCategory)) continue;
|
||||
loadout[rawSlot as EquipmentSlot] = rawInstanceId;
|
||||
assigned.add(rawInstanceId);
|
||||
}
|
||||
@@ -260,7 +289,11 @@ export function equipmentStats(
|
||||
const stats = instanceId ? itemById.get(instanceId)?.stats : null;
|
||||
if (!stats) continue;
|
||||
for (const key of ITEM_STAT_KEYS) numeric[key] += stats[key];
|
||||
const nativePayload = stats.romNative ?? romNativeGearFromSharedDirectStats(stats);
|
||||
const contentCategory = itemById.get(instanceId)?.contentCategory;
|
||||
const nativePayload = stats.romNative
|
||||
?? (contentCategory === "rom" || contentCategory === "coa"
|
||||
? undefined
|
||||
: romNativeGearFromSharedDirectStats(stats));
|
||||
if (nativePayload) nativePayloads.push(nativePayload);
|
||||
if (stats.meleeWeapon) meleeWeapon = stats.meleeWeapon;
|
||||
if (stats.rangedWeapon) rangedWeapon = stats.rangedWeapon;
|
||||
@@ -275,14 +308,19 @@ export function activeEquipmentStatsForClass(
|
||||
inventory: readonly InventoryItem[],
|
||||
ownerId: string,
|
||||
classId: ClassId,
|
||||
ownerCategory?: ContentCategoryId,
|
||||
): ItemStats {
|
||||
const classCategory = contentCategoryForClass(classId);
|
||||
if (ownerCategory !== undefined && ownerCategory !== classCategory) return EMPTY_ITEM_STATS;
|
||||
const loadout = assignments[ownerId];
|
||||
if (!loadout) return EMPTY_ITEM_STATS;
|
||||
const itemById = new Map(inventory.map((item) => [item.instanceId, item]));
|
||||
const activeAssignments: EquipmentAssignments = {
|
||||
[ownerId]: Object.fromEntries(Object.entries(loadout).filter(([, instanceId]) => {
|
||||
const item = instanceId ? itemById.get(instanceId) : null;
|
||||
return Boolean(item && classCanEquipItem(classId, item));
|
||||
return Boolean(item
|
||||
&& inventoryItemUsableForCategory(item, classCategory)
|
||||
&& classCanEquipItem(classId, item));
|
||||
})),
|
||||
};
|
||||
return equipmentStats(activeAssignments, inventory, ownerId);
|
||||
|
||||
Reference in new issue
Block a user