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);
|
&& 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 {
|
export function isQuest(v: unknown): v is Quest {
|
||||||
if (typeof v !== 'object' || v === null) return false;
|
if (typeof v !== 'object' || v === null) return false;
|
||||||
const q = v as Record<string, unknown>;
|
const q = v as Record<string, unknown>;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue