import type { InventoryOptions, InventorySlotInput, SlotId } from "../types"; import { action } from "../decorators"; import { RPGComponentBase } from "./entity"; interface SlotEntry { readonly slotId: SlotId; readonly limit: number | undefined; state: { itemId: string; amount: number } | null; } interface SlotUpdateArgs { itemId: string; amount: number; slotId?: SlotId; } export class Inventory extends RPGComponentBase { private readonly slots: Map; private readonly maxAmountPerItem: Record; constructor(slotDefs: Array, options?: InventoryOptions) { super(); this.slots = new Map( slotDefs.map(def => { const slotId = typeof def === 'object' ? def.slotId : def; const limit = typeof def === 'object' ? def.limit : undefined; return [slotId, { slotId, limit, state: null }]; }) ); this.maxAmountPerItem = options?.maxAmountPerItem ?? {}; } // Max amount of itemId allowed in a single slot (min of slot.limit and maxAmountPerItem cap) private slotCapFor(slot: SlotEntry, itemId: string): number { const limitCap = slot.limit ?? Infinity; const itemCap = this.maxAmountPerItem[itemId] ?? Infinity; return Math.min(limitCap, itemCap); } // Remaining space in slot for itemId (min of slotCap and remaining amount) private slotRoomFor(slot: SlotEntry, itemId: string): number { if (slot.state !== null && slot.state.itemId !== itemId) return 0; return this.slotCapFor(slot, itemId) - (slot.state?.amount ?? 0); } @action addItem({ itemId, amount, slotId }: SlotUpdateArgs): boolean { if (amount < 0) return false; if (amount === 0) return true; if (slotId !== undefined) { const slot = this.slots.get(slotId); if (!slot) return false; if (this.slotRoomFor(slot, itemId) < amount) return false; slot.state = { itemId, amount: (slot.state?.amount ?? 0) + amount }; return true; } // Two-phase: pre-check then apply let remaining = amount; for (const slot of this.slots.values()) { if (slot.state === null || slot.state.itemId === itemId) { remaining -= Math.min(this.slotRoomFor(slot, itemId), remaining); if (remaining === 0) break; } } if (remaining > 0) return false; // Apply remaining = amount; for (const slot of this.slots.values()) { if (slot.state?.itemId === itemId) { const take = Math.min(this.slotRoomFor(slot, itemId), remaining); slot.state.amount += take; remaining -= take; if (remaining === 0) return true; } } for (const slot of this.slots.values()) { if (slot.state === null) { const take = Math.min(this.slotCapFor(slot, itemId), remaining); slot.state = { itemId, amount: take }; remaining -= take; if (remaining === 0) return true; } } return remaining === 0; } @action removeItem({ itemId, amount, slotId }: SlotUpdateArgs): boolean { if (amount < 0) return false; if (amount === 0) return true; if (slotId !== undefined) { const slot = this.slots.get(slotId); if (!slot || slot.state?.itemId !== itemId) return false; if (slot.state.amount < amount) return false; slot.state.amount -= amount; if (slot.state.amount === 0) slot.state = null; return true; } if (this.getAmount(itemId) < amount) return false; let remaining = amount; for (const slot of this.slots.values()) { if (slot.state?.itemId === itemId) { const take = Math.min(slot.state.amount, remaining); slot.state.amount -= take; if (slot.state.amount === 0) slot.state = null; remaining -= take; if (remaining === 0) return true; } } return remaining === 0; } getAmount(itemId: string, slotId?: SlotId): number { if (slotId !== undefined) { const slot = this.slots.get(slotId); return slot?.state?.itemId === itemId ? slot.state.amount : 0; } let total = 0; for (const slot of this.slots.values()) { if (slot.state?.itemId === itemId) total += slot.state.amount; } return total; } getItems(): Map { const result = new Map(); for (const slot of this.slots.values()) { if (slot.state) { result.set(slot.state.itemId, (result.get(slot.state.itemId) ?? 0) + slot.state.amount); } } return result; } override getVariables(): Record { return Object.fromEntries(this.getItems()); } }