1
0
Fork 0

Inventory system

This commit is contained in:
Pabloader 2026-04-27 13:33:10 +00:00
parent 00fc43a564
commit 8c7969771d
2 changed files with 150 additions and 0 deletions

139
src/common/rpg/inventory.ts Normal file
View File

@ -0,0 +1,139 @@
import type { InventoryOptions, InventorySlotInput } from "./types";
interface SlotEntry {
readonly slotId: string;
readonly limit: number | undefined;
state: { itemId: string; amount: number } | null;
}
export class Inventory {
private readonly slots: Map<string, SlotEntry>;
private readonly maxAmountPerItem: Record<string, number>;
constructor(slotDefs: Array<InventorySlotInput>, options?: InventoryOptions) {
this.slots = new Map(
slotDefs.map(def => {
const slotId = typeof def === 'string' ? def : def.slotId;
const limit = typeof def === 'string' ? undefined : def.limit;
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);
}
addItem(itemId: string, amount: number, slotId?: string): 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;
}
removeItem(itemId: string, amount: number, slotId?: string): 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?: string): 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;
}
getVariables(): Record<string, number> {
const result: Record<string, number> = {};
for (const [itemId, amount] of this.getItems()) {
result[`inventory.${itemId}`] = amount;
}
return result;
}
}

View File

@ -97,6 +97,17 @@ function isQuestStage(v: unknown): v is QuestStage {
&& Array.isArray(s.actions) && s.actions.every(isRPGAction);
}
export interface InventorySlotDefinition {
slotId: string;
limit?: number;
}
export type InventorySlotInput = string | InventorySlotDefinition;
export interface InventoryOptions {
maxAmountPerItem?: Record<string, number>;
}
export function isQuest(v: unknown): v is Quest {
if (typeof v !== 'object' || v === null) return false;
const q = v as Record<string, unknown>;