Pass context to all actions
This commit is contained in:
parent
f6b3b5b66e
commit
5dec0901ac
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue