QoL Improvements for actions and context
This commit is contained in:
parent
9bec7701e0
commit
066271205a
|
|
@ -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}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ type EntityEventHandler = <T>(event: EntityEvent<T>) => void;
|
|||
type WorldEventHandler = <T>(event: WorldEvent<T>) => 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export namespace Dialogs {
|
|||
const actions = new Set<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string | number | boolean | undefined>;
|
||||
|
|
|
|||
|
|
@ -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<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 actionType = action.type;
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue