1
0
Fork 0

QoL Improvements for actions and context

This commit is contained in:
Pabloader 2026-04-29 08:16:18 +00:00
parent 9bec7701e0
commit 066271205a
6 changed files with 71 additions and 33 deletions

View File

@ -158,8 +158,9 @@ export namespace Quests {
for (const stage of quest.stages) { for (const stage of quest.stages) {
for (const action of stage.actions) { for (const action of stage.actions) {
if (!actionSet.has(action.type)) { const type = typeof action === 'string' ? action : action.type;
errors.push(`stage '${stage.id}': unknown action type '${action.type}'`); if (!actionSet.has(type)) {
errors.push(`stage '${stage.id}': unknown action type '${type}'`);
} }
} }
} }

View File

@ -15,7 +15,7 @@ type EntityEventHandler = <T>(event: EntityEvent<T>) => void;
type WorldEventHandler = <T>(event: WorldEvent<T>) => void; type WorldEventHandler = <T>(event: WorldEvent<T>) => void;
export interface EvalContext { export interface EvalContext {
self: Entity; self: Entity | World;
world: World; world: World;
} }
@ -205,8 +205,26 @@ export class World {
get [WORLD_ENTITY_COUNTER](): number { return this.#entityCounter; } get [WORLD_ENTITY_COUNTER](): number { return this.#entityCounter; }
set [WORLD_ENTITY_COUNTER](n: number) { this.#entityCounter = n; } 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 { 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`); if (this.#entities.has(entityId)) throw new Error(`Entity '${entityId}' already exists`);
const entity = new Entity(entityId, this); const entity = new Entity(entityId, this);
this.#entities.set(entityId, entity); this.#entities.set(entityId, entity);
@ -312,6 +330,9 @@ export class World {
export function isEvalContext(v: unknown): v is EvalContext { export function isEvalContext(v: unknown): v is EvalContext {
return typeof v === 'object' && v != null 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; && (v as EvalContext).world instanceof World;
} }

View File

@ -46,7 +46,7 @@ export namespace Dialogs {
const actions = new Set<string>(); const actions = new Set<string>();
for (const node of dialog.nodes) { for (const node of dialog.nodes) {
for (const action of node.actions ?? []) { for (const action of node.actions ?? []) {
actions.add(action.type); actions.add(typeof action === 'string' ? action : action.type);
} }
} }
return Array.from(actions); 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`); errors.push(`node '${node.id}': nextNodeId '${node.nextNodeId}' does not match any node id`);
for (const action of node.actions ?? []) { for (const action of node.actions ?? []) {
if (!actionSet.has(action.type)) { const type = typeof action === 'string' ? action : action.type;
errors.push(`node '${node.id}': unknown action type '${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)) { if (isEvalContext(this.options)) {
await executeAction(action, this.options); await executeAction(action, this.options);
} else { } 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);
} }
} }

View File

@ -3,10 +3,13 @@ import type { EvalContext } from './core/world';
// ── Shared ──────────────────────────────────────────────────────────────────── // ── Shared ────────────────────────────────────────────────────────────────────
const RPGActionScheme = Type.Object({ const RPGActionScheme = Type.Union([
type: Type.String(), Type.Object({
arg: Type.Optional(Type.Any()), type: Type.String(),
}); arg: Type.Optional(Type.Any()),
}),
Type.String(),
]);
export type RPGCondition = string; export type RPGCondition = string;
export type RPGVariables = Record<string, string | number | boolean | undefined>; export type RPGVariables = Record<string, string | number | boolean | undefined>;

View File

@ -1,19 +1,17 @@
import type { RPGAction, RPGActions, RPGVariables } from "../types"; 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"; import type { EvalContext, Entity } from "../core/world";
export function resolveVariables(entity: Entity): RPGVariables; export function resolveVariables(target: Entity | World): RPGVariables {
export function resolveVariables(world: World): RPGVariables;
export function resolveVariables(entityOrWorld: Entity | World): RPGVariables {
const result: RPGVariables = {}; const result: RPGVariables = {};
if (entityOrWorld instanceof World) { if (target instanceof World) {
for (const entity of entityOrWorld) { for (const entity of target) {
for (const [key, value] of Object.entries(resolveVariables(entity))) { for (const [key, value] of Object.entries(resolveVariables(entity))) {
result[`${entity.id}.${key}`] = value; result[`${entity.id}.${key}`] = value;
} }
} }
} else { } else {
for (const [key, component] of entityOrWorld) { for (const [key, component] of target) {
for (const [varKey, value] of Object.entries(component.getVariables())) { for (const [varKey, value] of Object.entries(component.getVariables())) {
if (value != null) { if (value != null) {
if (varKey && varKey !== '.') { if (varKey && varKey !== '.') {
@ -49,18 +47,16 @@ export function resolveVariable(name: string, ctx: EvalContext): RPGVariables[st
// bare name → self entity // bare name → self entity
return resolveVariables(ctx.self)[name]; return resolveVariables(ctx.self)[name];
} }
export function resolveActions(entity: Entity): RPGActions; export function resolveActions(target: Entity | World): RPGActions {
export function resolveActions(world: World): RPGActions;
export function resolveActions(entityOrWorld: Entity | World): RPGActions {
const result: RPGActions = {}; const result: RPGActions = {};
if (entityOrWorld instanceof World) { if (target instanceof World) {
for (const entity of entityOrWorld) { for (const entity of target) {
for (const [key, value] of Object.entries(resolveActions(entity))) { for (const [key, value] of Object.entries(resolveActions(entity))) {
result[`${entity.id}.${key}`] = value; result[`${entity.id}.${key}`] = value;
} }
} }
} else { } else {
for (const [key, component] of entityOrWorld) { for (const [key, component] of target) {
for (const [actionKey, fn] of Object.entries(component.getActions())) { for (const [actionKey, fn] of Object.entries(component.getActions())) {
result[`${key}.${actionKey}`] = fn; result[`${key}.${actionKey}`] = fn;
} }
@ -69,7 +65,21 @@ export function resolveActions(entityOrWorld: Entity | World): RPGActions {
return result; return result;
} }
export async function executeAction(action: RPGAction, ctx: EvalContext): Promise<unknown> { interface Contextable {
readonly context: EvalContext;
}
export async function executeAction(action: RPGAction, ctx: EvalContext | Contextable): Promise<unknown> {
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 entity = ctx.self;
let actionType = action.type; let actionType = action.type;

View File

@ -6,6 +6,7 @@ import { QuestLog } from "@common/rpg/components/questLog";
import { QuestSystem } from "@common/rpg/systems/questSystem"; import { QuestSystem } from "@common/rpg/systems/questSystem";
import { Items } from "@common/rpg/components/item"; import { Items } from "@common/rpg/components/item";
import { resolveVariables, resolveActions, executeAction } from "@common/rpg/utils/variables"; import { resolveVariables, resolveActions, executeAction } from "@common/rpg/utils/variables";
import { Serialization } from "@common/rpg/core/serialization";
export default async function main() { export default async function main() {
const world = new World(); const world = new World();
@ -18,15 +19,12 @@ export default async function main() {
player.add('inventory', new Inventory(['head', 'legs'])); player.add('inventory', new Inventory(['head', 'legs']));
player.add('health', new Health(100, 100)); player.add('health', new Health(100, 100));
player.add('vars', new Variables()); player.add('vars', new Variables());
player.add('quests', new QuestLog()); player.add('quests', new QuestLog([{
const quests = player.get(QuestLog)!;
quests.addQuest({
id: 'test', id: 'test',
description: 'Test quest', description: 'Test quest',
title: 'Test', title: 'Test',
stages: [], stages: [],
}); }]));
console.log(resolveVariables(world)); console.log(resolveVariables(world));
@ -36,8 +34,10 @@ export default async function main() {
const vars = player.get(Variables)!; const vars = player.get(Variables)!;
vars.set({ key: 'test', value: 'test' }); 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(resolveActions(world));
console.log(resolveVariables(world)); console.log(resolveVariables(world));
console.log(Serialization.serialize(world));
} }