146 lines
5.0 KiB
TypeScript
146 lines
5.0 KiB
TypeScript
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<SlotId, SlotEntry>;
|
|
private readonly maxAmountPerItem: Record<string, number>;
|
|
|
|
constructor(slotDefs: Array<InventorySlotInput>, 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<string, number> {
|
|
const result = new Map<string, number>();
|
|
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<string, number> {
|
|
return Object.fromEntries(this.getItems());
|
|
}
|
|
}
|