1
0
Fork 0

Pass context to all actions

This commit is contained in:
Pabloader 2026-04-28 21:46:28 +00:00
parent f6b3b5b66e
commit 5dec0901ac
9 changed files with 106 additions and 64 deletions

View File

@ -1,7 +1,6 @@
import { Component, type EvalContext } from "../core/world";
import { isQuest, type Quest, type QuestStage, type RPGVariables } from "../types";
import { evaluateCondition, parseCondition } from "../utils/conditions";
import { action } from "../utils/decorators";
import { evaluateCondition } from "../utils/conditions";
export type QuestStatus = 'inactive' | 'active' | 'completed' | 'failed';
@ -42,22 +41,18 @@ export class QuestLog extends Component {
return true;
}
@action
start(questId: string): boolean {
return this.#transition('start', questId, 'inactive', 'active', 'started');
}
@action
complete(questId: string): boolean {
return this.#transition('complete', questId, 'active', 'completed', 'completed');
}
@action
fail(questId: string): boolean {
return this.#transition('fail', questId, 'active', 'failed', 'failed');
}
@action
abandon(questId: string): boolean {
return this.#transition('abandon', questId, 'active', 'inactive', 'abandoned');
}
@ -71,7 +66,7 @@ export class QuestLog extends Component {
if (!entry) return false;
const { quest } = entry;
if (!quest.conditions?.length) return true;
return quest.conditions.every(c => evaluateCondition(parseCondition(c), ctx));
return quest.conditions.every(c => evaluateCondition(c, ctx));
}
getStage(questId: string): QuestStage | undefined {
@ -89,7 +84,7 @@ export class QuestLog extends Component {
return stage.objectives.map(obj => ({
id: obj.id,
description: obj.description,
done: evaluateCondition(parseCondition(obj.condition), ctx),
done: evaluateCondition(obj.condition, ctx),
}));
}
@ -98,11 +93,6 @@ export class QuestLog extends Component {
return this.#quests.entries();
}
/** @internal called by QuestSystem when a fail condition is met */
_fail(questId: string): void {
this.#transition('_fail', questId, 'active', 'failed', 'failed');
}
/** @internal called by QuestSystem after stage actions complete */
_advance(questId: string): void {
const entry = this.#quests.get(questId);
@ -117,6 +107,21 @@ export class QuestLog extends Component {
}
}
override getActions() {
const result = { ...super.getActions() };
for (const questId of this.#quests.keys()) {
const entry = this.#quests.get(questId)!;
if (entry.state.status === 'inactive') {
result[`${questId}.start`] = this.start.bind(this, questId);
} else if (entry.state.status === 'active') {
result[`${questId}.complete`] = this.complete.bind(this, questId);
result[`${questId}.fail`] = this.fail.bind(this, questId);
result[`${questId}.abandon`] = this.abandon.bind(this, questId);
}
}
return result;
}
override getVariables(): RPGVariables {
const result: RPGVariables = {};
for (const [questId, { state }] of this.#quests) {

View File

@ -59,14 +59,14 @@ export abstract class Component {
}
get context(): EvalContext {
return { self: this.entity, world: this.entity.world };
return this.entity.context;
}
}
export abstract class System {
onAdd(_world: World): void { }
onRemove(_world: World): void { }
abstract update(world: World, dt: number): void;
update(_world: World, _dt: number): void { };
}
export class Entity {
@ -77,6 +77,10 @@ export class Entity {
readonly world: World,
) { }
get context(): EvalContext {
return { self: this, world: this.world };
}
add<T extends Component>(key: string, component: T): T {
const existing = this.#components.get(key);
if (existing) existing.onRemove();
@ -207,7 +211,9 @@ export class World {
*query<T extends Component>(ctor: Class<T>): Generator<[Entity, string, T]> {
for (const entity of this.#entities.values()) {
for (const [key, component] of entity) {
if (component instanceof ctor) yield [entity, key, component as T];
if (component instanceof ctor) {
yield [entity, key, component as T]
}
}
}
}

View File

@ -3,6 +3,7 @@ import {
type Dialog,
type DialogChoice,
type DialogNode,
type RPGAction,
type RPGActions,
type RPGVariables,
} from "./types";
@ -233,7 +234,7 @@ export class DialogEngine {
return this.options.variables;
}
private async runAction(action: { type: string; arg?: string | number | boolean }): Promise<void> {
private async runAction(action: RPGAction): Promise<void> {
if (isEvalContext(this.options)) {
await executeAction(action, this.options);
} else {

View File

@ -1,19 +1,15 @@
import { System, type Entity, type World } from "../core/world";
import { evaluateCondition, parseCondition } from "../utils/conditions";
import { evaluateCondition } from "../utils/conditions";
import { executeAction } from "../utils/variables";
import { QuestLog } from "../components/questLog";
export class QuestSystem extends System {
override update(world: World, _dt: number): void {
async triggerCheck(world: World): Promise<void> {
for (const [entity] of world.query(QuestLog)) {
void this.#checkEntity(entity, world);
await this.#checkEntity(entity, world);
}
}
async triggerCheck(entity: Entity, world: World): Promise<void> {
await this.#checkEntity(entity, world);
}
async #checkEntity(entity: Entity, world: World): Promise<void> {
const questLog = entity.get(QuestLog);
if (!questLog) return;
@ -31,16 +27,16 @@ export class QuestSystem extends System {
if (stage.failConditions?.length) {
const failed = stage.failConditions.some(c =>
evaluateCondition(parseCondition(c), ctx)
evaluateCondition(c, ctx)
);
if (failed) {
questLog._fail(questId);
questLog.fail(questId);
continue;
}
}
const allDone = stage.objectives.every(obj =>
evaluateCondition(parseCondition(obj.condition), ctx)
evaluateCondition(obj.condition, ctx)
);
if (!allDone) continue;

View File

@ -1,15 +1,16 @@
import { Type, type Static } from '../typebox';
import type { EvalContext } from './core/world';
// ── Shared ────────────────────────────────────────────────────────────────────
const RPGActionScheme = Type.Object({
type: Type.String(),
arg: Type.Optional(Type.Union([Type.String(), Type.Number(), Type.Boolean()])),
arg: Type.Optional(Type.Any()),
});
export type RPGCondition = string;
export type RPGVariables = Record<string, string | number | boolean | undefined>;
export type RPGActions = Record<string, (arg?: any) => unknown>;
export type RPGActions = Record<string, (arg?: any, ctx?: EvalContext) => unknown>;
export type RPGAction = Static<typeof RPGActionScheme>;

View File

@ -1,5 +1,5 @@
import type { RPGCondition, RPGVariables } from "../types";
import type { EvalContext } from "../core/world";
import { isEvalContext, type EvalContext } from "../core/world";
import { resolveVariable } from "./variables";
type ConditionOperator = '==' | '!=' | '>' | '<' | '>=' | '<=' | 'null' | '~null';
@ -60,21 +60,22 @@ function evalParsed({ negate, operator, value }: ParsedCondition, val: RPGVariab
return false;
}
export function evaluateCondition(parsed: ParsedCondition, variables: RPGVariables): boolean;
export function evaluateCondition(parsed: ParsedCondition, ctx: EvalContext): boolean;
export function evaluateCondition(parsed: ParsedCondition, variablesOrCtx: RPGVariables | EvalContext): boolean {
type Cond = ParsedCondition | RPGCondition;
export function evaluateCondition(condition: Cond, variables: RPGVariables): boolean;
export function evaluateCondition(condition: Cond, ctx: EvalContext): boolean;
export function evaluateCondition(condition: Cond, variablesOrCtx: RPGVariables | EvalContext): boolean {
const parsed = typeof condition === 'string' ? parseCondition(condition) : condition;
const val = isEvalContext(variablesOrCtx)
? resolveVariable(parsed.variable, variablesOrCtx)
: variablesOrCtx[parsed.variable];
return evalParsed(parsed, val);
}
export function evaluateConditions(conditions: RPGCondition[], variables: RPGVariables): boolean;
export function evaluateConditions(conditions: RPGCondition[], ctx: EvalContext): boolean;
export function evaluateConditions(conditions: RPGCondition[], variablesOrCtx: RPGVariables | EvalContext): boolean {
return conditions.every(c => evaluateCondition(parseCondition(c), variablesOrCtx as RPGVariables));
}
function isEvalContext(v: RPGVariables | EvalContext): v is EvalContext {
return 'self' in v && 'world' in v;
export function evaluateConditions(conditions: Cond[], variables: RPGVariables): boolean;
export function evaluateConditions(conditions: Cond[], ctx: EvalContext): boolean;
export function evaluateConditions(conditions: Cond[], variablesOrCtx: RPGVariables | EvalContext): boolean {
return conditions.every(c => evaluateCondition(c, variablesOrCtx as RPGVariables));
}

View File

@ -5,23 +5,22 @@ 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 {
const result: 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 entityOrWorld) {
for (const [varKey, value] of Object.entries(component.getVariables())) {
if (value != null) {
if (varKey && varKey !== '.') {
result[`${key}.${varKey}`] = value;
} else {
result[key] = value;
} else {
for (const [key, component] of entityOrWorld) {
for (const [varKey, value] of Object.entries(component.getVariables())) {
if (value != null) {
if (varKey && varKey !== '.') {
result[`${key}.${varKey}`] = value;
} else {
result[key] = value;
}
}
}
}
@ -50,12 +49,21 @@ 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(entity: Entity): RPGActions;
export function resolveActions(world: World): RPGActions;
export function resolveActions(entityOrWorld: Entity | World): RPGActions {
const result: RPGActions = {};
for (const [key, component] of entity) {
for (const [actionKey, fn] of Object.entries(component.getActions())) {
result[`${key}.${actionKey}`] = fn;
if (entityOrWorld instanceof World) {
for (const entity of entityOrWorld) {
for (const [key, value] of Object.entries(resolveActions(entity))) {
result[`${entity.id}.${key}`] = value;
}
}
} else {
for (const [key, component] of entityOrWorld) {
for (const [actionKey, fn] of Object.entries(component.getActions())) {
result[`${key}.${actionKey}`] = fn;
}
}
}
return result;
@ -87,5 +95,5 @@ export async function executeAction(action: RPGAction, ctx: EvalContext): Promis
console.warn(`[executeAction] action '${actionType}' not found on entity '${entity.id}'`);
return;
}
return actions[actionType](action.arg);
return actions[actionType](action.arg, ctx);
}

View File

@ -4,6 +4,14 @@ const GlobalNumber = Number;
const GlobalArray = Array;
export namespace Type {
export function Any(args: { description?: string } = {}) {
const result: TAny = {};
if (args.description) {
result.description = args.description;
}
return result;
}
export function String<S extends string = string>(args: { description?: string, enum?: S[] } = {}) {
const result: TString<S> = {
type: 'string',
@ -106,6 +114,8 @@ export namespace Type {
}
function check(scheme: TScheme, value: unknown, path: string): CheckError[] {
if (!('type' in scheme)) return [];
if (value == null) {
if ((scheme as any)[optional]) return [];
return [{ path, message: `Expected ${scheme.type} at ${path}, got ${value}` }];
@ -187,6 +197,10 @@ export namespace Type {
}
}
export interface TAny {
description?: string;
}
export interface TString<T extends string = string> {
type: 'string';
enum?: T[];
@ -219,6 +233,10 @@ export interface TObject<T extends TProperties = TProperties> {
required?: string[];
}
export interface TOptionalAny extends TAny {
[optional]: true;
}
export interface TOptionalString<T extends string = string> extends TString<T> {
[optional]: true;
}
@ -256,11 +274,12 @@ export type TOptional<T extends TScheme = TScheme> =
T extends TArray<infer I> ? TOptionalArray<I> :
T extends TObject<infer P> ? TOptionalObject<P> :
T extends TUnion<infer U> ? TOptionalUnion<U> :
T extends TAny ? TOptionalAny :
never;
export type IsOptional<T> = T extends { [optional]: true } ? true : false;
export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TUnion | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject | TOptionalUnion;
export type TScheme = TAny | TString | TNumber | TBoolean | TArray | TObject | TUnion | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject | TOptionalUnion;
type Prettify<T> = { [K in keyof T]: T[K] } & {};
@ -279,8 +298,10 @@ type StaticObject<T extends TProperties> = Prettify<
type StaticUnion<T extends TScheme[]> =
T extends [infer First extends TScheme, ...infer Rest extends TScheme[]]
? Static<First> | StaticUnion<Rest>
: never;
? Static<First> | StaticUnion<Rest>
: never;
type StaticAny = any;
export type Static<T extends TScheme> =
T extends TString<infer S> ? S :
@ -289,4 +310,5 @@ export type Static<T extends TScheme> =
T extends TArray<infer I> ? Static<I>[] :
T extends TObject<infer P> ? StaticObject<P> :
T extends TUnion<infer U> ? StaticUnion<U> :
T extends TAny ? StaticAny :
never;

View File

@ -5,7 +5,7 @@ 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";
import { resolveVariables, resolveActions, executeAction } from "@common/rpg/utils/variables";
export default async function main() {
const world = new World();
@ -33,9 +33,11 @@ export default async function main() {
const inventory = player.get(Inventory)!;
inventory.add({ itemId: 'helmet', amount: 1, slotId: 'head' });
const actions = resolveActions(player);
actions['inventory.add']({ itemId: 'boots', amount: 2 });
const vars = player.get(Variables)!;
vars.set({ key: 'test', value: 'test' });
console.log(actions);
await executeAction({ type: 'inventory.add', arg: { itemId: 'boots', amount: 2 } }, player.context);
console.log(resolveActions(world));
console.log(resolveVariables(world));
}