diff --git a/src/common/rpg/components/inventory.ts b/src/common/rpg/components/inventory.ts index 7649354..e8465af 100644 --- a/src/common/rpg/components/inventory.ts +++ b/src/common/rpg/components/inventory.ts @@ -1,6 +1,7 @@ -import type { InventoryOptions, InventorySlotInput, SlotId } from "../types"; +import type { InventorySlotInput, SlotId } from "../types"; import { action } from "../utils/decorators"; import { Component } from "../core/world"; +import { Stackable, Usable } from "./item"; interface SlotEntry { readonly slotId: SlotId; @@ -16,9 +17,8 @@ interface SlotUpdateArgs { export class Inventory extends Component { private readonly slots: Map; - private readonly maxAmountPerItem: Record; - constructor(slotDefs: Array, options?: InventoryOptions) { + constructor(slotDefs: Array) { super(); this.slots = new Map( slotDefs.map(def => { @@ -27,17 +27,15 @@ export class Inventory extends Component { 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); + const stackable = this.entity.world.getEntity(itemId)?.get(Stackable); + const stackCap = stackable ? stackable.maxStack : 1; + return Math.min(limitCap, stackCap); } - // Remaining space in slot for itemId 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); @@ -48,6 +46,11 @@ export class Inventory extends Component { if (amount < 0) return false; if (amount === 0) return true; + if (!this.entity.world.getEntity(itemId)) { + console.warn(`[Inventory] add: item entity '${itemId}' not found`); + return false; + } + if (slotId !== undefined) { const slot = this.slots.get(slotId); if (!slot) return false; @@ -134,6 +137,35 @@ export class Inventory extends Component { return false; } + @action + async use(itemId?: string): Promise { + if (!itemId) return false; + + if (this.getAmount(itemId) === 0) { + console.warn(`[Inventory] use: item '${itemId}' not in inventory`); + return false; + } + + const itemEntity = this.entity.world.getEntity(itemId); + if (!itemEntity) { + console.warn(`[Inventory] use: item entity '${itemId}' not found`); + return false; + } + + const usable = itemEntity.get(Usable); + if (!usable) { + console.warn(`[Inventory] use: item '${itemId}' has no Usable component`); + return false; + } + + if (usable.consumeOnUse) { + this.remove({ itemId, amount: 1 }); + } + + await usable.use({ self: this.entity, world: this.entity.world }); + return true; + } + getAmount(itemId: string, slotId?: SlotId): number { if (slotId !== undefined) { const slot = this.slots.get(slotId); diff --git a/src/common/rpg/components/item.ts b/src/common/rpg/components/item.ts new file mode 100644 index 0000000..e562e6e --- /dev/null +++ b/src/common/rpg/components/item.ts @@ -0,0 +1,36 @@ +import { Component, type EvalContext } from "../core/world"; +import type { RPGAction } from "../types"; +import { action, variable } from "../utils/decorators"; +import { executeAction } from "../utils/variables"; + +export class Item extends Component { + constructor( + readonly name: string, + readonly description: string = '', + ) { + super(); + } +} + +export class Stackable extends Component { + @variable readonly maxStack: number; + constructor(maxStack: number) { + super(); + this.maxStack = maxStack; + } +} + +export class Usable extends Component { + @variable readonly consumeOnUse: boolean; + constructor(private readonly actions: RPGAction[], consumeOnUse = true) { + super(); + this.consumeOnUse = consumeOnUse; + } + + @action + async use(ctx: EvalContext): Promise { + for (const action of this.actions) { + await executeAction(action, ctx); + } + } +} diff --git a/src/common/rpg/core/world.ts b/src/common/rpg/core/world.ts index 374ba92..17d560e 100644 --- a/src/common/rpg/core/world.ts +++ b/src/common/rpg/core/world.ts @@ -70,7 +70,7 @@ export class Entity { constructor( readonly id: string, - private readonly world: World, + readonly world: World, ) { } add(key: string, component: T): T { diff --git a/src/common/rpg/types.ts b/src/common/rpg/types.ts index ca150f1..68540c8 100644 --- a/src/common/rpg/types.ts +++ b/src/common/rpg/types.ts @@ -12,6 +12,7 @@ export type RPGVariables = Record export type RPGActions = Record unknown>; export type RPGAction = Static; + // ── Dialog ──────────────────────────────────────────────────────────────────── const DialogChoiceScheme = Type.Object({ @@ -85,6 +86,4 @@ export interface InventorySlotDefinition { export type InventorySlotInput = SlotId | InventorySlotDefinition; -export interface InventoryOptions { - maxAmountPerItem?: Record; -} +