Inventory system
This commit is contained in:
parent
00fc43a564
commit
8c7969771d
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Reference in New Issue