From f6b3b5b66eb7a2158449f57f53585b647025fe75 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Tue, 28 Apr 2026 21:06:01 +0000 Subject: [PATCH] Refactor the inventory --- src/common/rpg/components/inventory.ts | 24 ++++++++++++++++++------ src/common/rpg/components/item.ts | 24 ++++++++++++++++++++++-- src/common/rpg/core/world.ts | 8 ++++++++ src/common/rpg/utils/decorators.ts | 6 ++++-- src/common/rpg/utils/variables.ts | 16 ++++++++++++++-- src/games/playground/index.tsx | 8 ++++++-- 6 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/common/rpg/components/inventory.ts b/src/common/rpg/components/inventory.ts index e8465af..06b511b 100644 --- a/src/common/rpg/components/inventory.ts +++ b/src/common/rpg/components/inventory.ts @@ -1,7 +1,8 @@ -import type { InventorySlotInput, SlotId } from "../types"; +import type { InventorySlotInput, RPGVariables, SlotId } from "../types"; import { action } from "../utils/decorators"; -import { Component } from "../core/world"; +import { Component, type EvalContext } from "../core/world"; import { Stackable, Usable } from "./item"; +import { resolveVariables } from "../utils/variables"; interface SlotEntry { readonly slotId: SlotId; @@ -138,7 +139,7 @@ export class Inventory extends Component { } @action - async use(itemId?: string): Promise { + async use(itemId?: string, ctx?: EvalContext): Promise { if (!itemId) return false; if (this.getAmount(itemId) === 0) { @@ -162,7 +163,8 @@ export class Inventory extends Component { this.remove({ itemId, amount: 1 }); } - await usable.use({ self: this.entity, world: this.entity.world }); + const resolvedCtx = ctx ?? { self: this.entity, world: this.entity.world }; + await usable.use(resolvedCtx); return true; } @@ -188,7 +190,17 @@ export class Inventory extends Component { return result; } - override getVariables(): Record { - return Object.fromEntries(this.getItems()); + override getVariables(): RPGVariables { + const result: RPGVariables = {}; + for (const [itemId, amount] of this.getItems()) { + result[itemId] = amount; + const itemEntity = this.entity.world.getEntity(itemId); + if (itemEntity) { + for (const [key, value] of Object.entries(resolveVariables(itemEntity))) { + result[`${itemId}.${key}`] = value; + } + } + } + return result; } } diff --git a/src/common/rpg/components/item.ts b/src/common/rpg/components/item.ts index e562e6e..f343310 100644 --- a/src/common/rpg/components/item.ts +++ b/src/common/rpg/components/item.ts @@ -1,4 +1,4 @@ -import { Component, type EvalContext } from "../core/world"; +import { Component, type EvalContext, type World } from "../core/world"; import type { RPGAction } from "../types"; import { action, variable } from "../utils/decorators"; import { executeAction } from "../utils/variables"; @@ -28,9 +28,29 @@ export class Usable extends Component { } @action - async use(ctx: EvalContext): Promise { + async use(arg?: EvalContext, ctx?: EvalContext): Promise { + ctx = arg ?? ctx ?? this.context; + if (!ctx) return; for (const action of this.actions) { await executeAction(action, ctx); } } } + +export namespace Items { + export interface RegisterOptions { + maxStack?: number; + description?: string; + usable?: { actions: RPGAction[]; consumeOnUse?: boolean }; + } + + export function register(world: World, id: string, name: string, options?: RegisterOptions) { + const entity = world.createEntity(id); + entity.add('item', new Item(name, options?.description)); + if (options?.maxStack !== undefined) + entity.add('stackable', new Stackable(options.maxStack)); + if (options?.usable) + entity.add('usable', new Usable(options.usable.actions, options.usable.consumeOnUse)); + return entity; + } +} diff --git a/src/common/rpg/core/world.ts b/src/common/rpg/core/world.ts index 17d560e..281cc93 100644 --- a/src/common/rpg/core/world.ts +++ b/src/common/rpg/core/world.ts @@ -57,6 +57,10 @@ export abstract class Component { } return actions; } + + get context(): EvalContext { + return { self: this.entity, world: this.entity.world }; + } } export abstract class System { @@ -277,6 +281,10 @@ export class World { map.get(key)!.add(handler); return () => map.get(key)?.delete(handler); } + + *[Symbol.iterator]() { + yield* this.#entities.values(); + } } export function isEvalContext(v: unknown): v is EvalContext { diff --git a/src/common/rpg/utils/decorators.ts b/src/common/rpg/utils/decorators.ts index 0e4ee4d..c829023 100644 --- a/src/common/rpg/utils/decorators.ts +++ b/src/common/rpg/utils/decorators.ts @@ -3,13 +3,15 @@ import type { RPGVariables } from "../types"; export const ACTION_KEYS = Symbol('rpg.actions'); export const VARIABLE_KEYS = Symbol('rpg.variables'); -export function action unknown>( +export function action unknown>( target: T, context: ClassMethodDecoratorContext ): T { const prev = context.metadata[ACTION_KEYS] as Set | undefined; context.metadata[ACTION_KEYS] = new Set(prev).add(context.name); - return target; + return function(this: any, arg?: any, ctx?: any) { + return target.call(this, arg, ctx ?? this.context); + } as unknown as T; } type VariableContext = diff --git a/src/common/rpg/utils/variables.ts b/src/common/rpg/utils/variables.ts index 1417d98..67f046d 100644 --- a/src/common/rpg/utils/variables.ts +++ b/src/common/rpg/utils/variables.ts @@ -1,9 +1,21 @@ import type { RPGAction, RPGActions, RPGVariables } from "../types"; +import { World } from "../core/world"; import type { EvalContext, Entity } from "../core/world"; -export function resolveVariables(entity: Entity): RPGVariables { +export function resolveVariables(entity: Entity): RPGVariables; +export function resolveVariables(world: World): RPGVariables; +export function resolveVariables(entityOrWorld: Entity | World): RPGVariables { + if (entityOrWorld instanceof World) { + const result: RPGVariables = {}; + for (const entity of entityOrWorld) { + for (const [key, value] of Object.entries(resolveVariables(entity))) { + result[`${entity.id}.${key}`] = value; + } + } + return result; + } const result: RPGVariables = {}; - for (const [key, component] of entity) { + for (const [key, component] of entityOrWorld) { for (const [varKey, value] of Object.entries(component.getVariables())) { if (value != null) { if (varKey && varKey !== '.') { diff --git a/src/games/playground/index.tsx b/src/games/playground/index.tsx index 171c167..56c3877 100644 --- a/src/games/playground/index.tsx +++ b/src/games/playground/index.tsx @@ -4,12 +4,16 @@ import { Health } from "@common/rpg/components/stat"; import { Variables } from "@common/rpg/components/variables"; import { QuestLog } from "@common/rpg/components/questLog"; import { QuestSystem } from "@common/rpg/systems/questSystem"; +import { Items } from "@common/rpg/components/item"; import { resolveVariables, resolveActions } from "@common/rpg/utils/variables"; export default async function main() { const world = new World(); world.addSystem(new QuestSystem()); + Items.register(world, 'helmet', 'Iron Helmet'); + Items.register(world, 'boots', 'Leather Boots', { maxStack: 2 }); + const player = world.createEntity('player'); player.add('inventory', new Inventory(['head', 'legs'])); player.add('health', new Health(100, 100)); @@ -24,7 +28,7 @@ export default async function main() { stages: [], }); - console.log(resolveVariables(player)); + console.log(resolveVariables(world)); const inventory = player.get(Inventory)!; inventory.add({ itemId: 'helmet', amount: 1, slotId: 'head' }); @@ -33,5 +37,5 @@ export default async function main() { actions['inventory.add']({ itemId: 'boots', amount: 2 }); console.log(actions); - console.log(resolveVariables(player)); + console.log(resolveVariables(world)); }