From 066271205ae04fa9c2c05e2e13cab4d77b75fafe Mon Sep 17 00:00:00 2001 From: Pabloader Date: Wed, 29 Apr 2026 08:16:18 +0000 Subject: [PATCH] QoL Improvements for actions and context --- src/common/rpg/components/questLog.ts | 5 ++-- src/common/rpg/core/world.ts | 27 ++++++++++++++++--- src/common/rpg/dialog.ts | 11 +++++--- src/common/rpg/types.ts | 11 +++++--- src/common/rpg/utils/variables.ts | 38 +++++++++++++++++---------- src/games/playground/index.tsx | 12 ++++----- 6 files changed, 71 insertions(+), 33 deletions(-) diff --git a/src/common/rpg/components/questLog.ts b/src/common/rpg/components/questLog.ts index 9891976..66383d4 100644 --- a/src/common/rpg/components/questLog.ts +++ b/src/common/rpg/components/questLog.ts @@ -158,8 +158,9 @@ export namespace Quests { for (const stage of quest.stages) { for (const action of stage.actions) { - if (!actionSet.has(action.type)) { - errors.push(`stage '${stage.id}': unknown action type '${action.type}'`); + const type = typeof action === 'string' ? action : action.type; + if (!actionSet.has(type)) { + errors.push(`stage '${stage.id}': unknown action type '${type}'`); } } } diff --git a/src/common/rpg/core/world.ts b/src/common/rpg/core/world.ts index a2cafea..7193133 100644 --- a/src/common/rpg/core/world.ts +++ b/src/common/rpg/core/world.ts @@ -15,7 +15,7 @@ type EntityEventHandler = (event: EntityEvent) => void; type WorldEventHandler = (event: WorldEvent) => void; export interface EvalContext { - self: Entity; + self: Entity | World; world: World; } @@ -205,8 +205,26 @@ export class World { get [WORLD_ENTITY_COUNTER](): number { return this.#entityCounter; } set [WORLD_ENTITY_COUNTER](n: number) { this.#entityCounter = n; } + get id() { return 'world'; } + + get context(): EvalContext { + return { self: this, world: this }; + } + + /** + * Create a new entity and add it to the world. + * + * @param id - How the entity ID is determined: + * - Omitted → auto-generated: `entity_1`, `entity_2`, … + * - Plain string → used as-is: `createEntity('player')` → `'player'` + * - Template with `*` → `*` is replaced by the auto-incremented counter: + * `createEntity('enemy_*')` → `'enemy_1'`, `'enemy_2'`, … + * @throws If an entity with the resolved ID already exists. + */ createEntity(id?: string): Entity { - const entityId = id ?? `entity_${++this.#entityCounter}`; + const entityId = id == null + ? `entity_${++this.#entityCounter}` + : id.includes('*') ? id.replace('*', String(++this.#entityCounter)) : id; if (this.#entities.has(entityId)) throw new Error(`Entity '${entityId}' already exists`); const entity = new Entity(entityId, this); this.#entities.set(entityId, entity); @@ -312,6 +330,9 @@ export class World { export function isEvalContext(v: unknown): v is EvalContext { return typeof v === 'object' && v != null - && (v as EvalContext).self instanceof Entity + && ( + (v as EvalContext).self instanceof Entity + || (v as EvalContext).self instanceof World + ) && (v as EvalContext).world instanceof World; } diff --git a/src/common/rpg/dialog.ts b/src/common/rpg/dialog.ts index 1429b0c..b86af28 100644 --- a/src/common/rpg/dialog.ts +++ b/src/common/rpg/dialog.ts @@ -46,7 +46,7 @@ export namespace Dialogs { const actions = new Set(); for (const node of dialog.nodes) { for (const action of node.actions ?? []) { - actions.add(action.type); + actions.add(typeof action === 'string' ? action : action.type); } } return Array.from(actions); @@ -76,8 +76,9 @@ export namespace Dialogs { errors.push(`node '${node.id}': nextNodeId '${node.nextNodeId}' does not match any node id`); for (const action of node.actions ?? []) { - if (!actionSet.has(action.type)) { - errors.push(`node '${node.id}': unknown action type '${action.type}'`); + const type = typeof action === 'string' ? action : action.type; + if (!actionSet.has(type)) { + errors.push(`node '${node.id}': unknown action type '${type}'`); } } @@ -238,7 +239,9 @@ export class DialogEngine { if (isEvalContext(this.options)) { await executeAction(action, this.options); } else { - await this.options.actions[action.type]?.(action.arg); + const type = typeof action === 'string' ? action: action.type; + const arg = typeof action === 'string' ? undefined : action.arg; + await this.options.actions[type]?.(arg); } } diff --git a/src/common/rpg/types.ts b/src/common/rpg/types.ts index 818c475..4accd39 100644 --- a/src/common/rpg/types.ts +++ b/src/common/rpg/types.ts @@ -3,10 +3,13 @@ import type { EvalContext } from './core/world'; // ── Shared ──────────────────────────────────────────────────────────────────── -const RPGActionScheme = Type.Object({ - type: Type.String(), - arg: Type.Optional(Type.Any()), -}); +const RPGActionScheme = Type.Union([ + Type.Object({ + type: Type.String(), + arg: Type.Optional(Type.Any()), + }), + Type.String(), +]); export type RPGCondition = string; export type RPGVariables = Record; diff --git a/src/common/rpg/utils/variables.ts b/src/common/rpg/utils/variables.ts index cb94426..3ee1fd6 100644 --- a/src/common/rpg/utils/variables.ts +++ b/src/common/rpg/utils/variables.ts @@ -1,19 +1,17 @@ import type { RPGAction, RPGActions, RPGVariables } from "../types"; -import { World } from "../core/world"; +import { isEvalContext, World } from "../core/world"; import type { EvalContext, Entity } from "../core/world"; -export function resolveVariables(entity: Entity): RPGVariables; -export function resolveVariables(world: World): RPGVariables; -export function resolveVariables(entityOrWorld: Entity | World): RPGVariables { +export function resolveVariables(target: Entity | World): RPGVariables { const result: RPGVariables = {}; - if (entityOrWorld instanceof World) { - for (const entity of entityOrWorld) { + if (target instanceof World) { + for (const entity of target) { for (const [key, value] of Object.entries(resolveVariables(entity))) { result[`${entity.id}.${key}`] = value; } } } else { - for (const [key, component] of entityOrWorld) { + for (const [key, component] of target) { for (const [varKey, value] of Object.entries(component.getVariables())) { if (value != null) { if (varKey && varKey !== '.') { @@ -49,18 +47,16 @@ export function resolveVariable(name: string, ctx: EvalContext): RPGVariables[st // bare name → self entity return resolveVariables(ctx.self)[name]; } -export function resolveActions(entity: Entity): RPGActions; -export function resolveActions(world: World): RPGActions; -export function resolveActions(entityOrWorld: Entity | World): RPGActions { +export function resolveActions(target: Entity | World): RPGActions { const result: RPGActions = {}; - if (entityOrWorld instanceof World) { - for (const entity of entityOrWorld) { + if (target instanceof World) { + for (const entity of target) { for (const [key, value] of Object.entries(resolveActions(entity))) { result[`${entity.id}.${key}`] = value; } } } else { - for (const [key, component] of entityOrWorld) { + for (const [key, component] of target) { for (const [actionKey, fn] of Object.entries(component.getActions())) { result[`${key}.${actionKey}`] = fn; } @@ -69,7 +65,21 @@ export function resolveActions(entityOrWorld: Entity | World): RPGActions { return result; } -export async function executeAction(action: RPGAction, ctx: EvalContext): Promise { +interface Contextable { + readonly context: EvalContext; +} + +export async function executeAction(action: RPGAction, ctx: EvalContext | Contextable): Promise { + if (typeof action === 'string') { + action = { type: action }; + } + if ('context' in ctx) { + ctx = ctx.context; + } + if (!action.type) { + console.warn(`[executeAction] action missing 'type' property`); + return; + } let entity = ctx.self; let actionType = action.type; diff --git a/src/games/playground/index.tsx b/src/games/playground/index.tsx index 2e80b07..b5de39f 100644 --- a/src/games/playground/index.tsx +++ b/src/games/playground/index.tsx @@ -6,6 +6,7 @@ import { QuestLog } from "@common/rpg/components/questLog"; import { QuestSystem } from "@common/rpg/systems/questSystem"; import { Items } from "@common/rpg/components/item"; import { resolveVariables, resolveActions, executeAction } from "@common/rpg/utils/variables"; +import { Serialization } from "@common/rpg/core/serialization"; export default async function main() { const world = new World(); @@ -18,15 +19,12 @@ export default async function main() { player.add('inventory', new Inventory(['head', 'legs'])); player.add('health', new Health(100, 100)); player.add('vars', new Variables()); - player.add('quests', new QuestLog()); - - const quests = player.get(QuestLog)!; - quests.addQuest({ + player.add('quests', new QuestLog([{ id: 'test', description: 'Test quest', title: 'Test', stages: [], - }); + }])); console.log(resolveVariables(world)); @@ -36,8 +34,10 @@ export default async function main() { const vars = player.get(Variables)!; vars.set({ key: 'test', value: 'test' }); - await executeAction({ type: 'inventory.add', arg: { itemId: 'boots', amount: 2 } }, player.context); + await executeAction({ type: 'inventory.add', arg: { itemId: 'boots', amount: 2 } }, player); + await executeAction('quests.test.start', player); console.log(resolveActions(world)); console.log(resolveVariables(world)); + console.log(Serialization.serialize(world)); }