Fixes and improvements
This commit is contained in:
parent
89651f6674
commit
b75dc078f9
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, type EvalContext } from "../core/world";
|
||||
import { isQuest, type Quest, type RPGVariables } from "../types";
|
||||
import { isQuest, type Quest, type QuestStage, type RPGVariables } from "../types";
|
||||
import { evaluateCondition, parseCondition } from "../utils/conditions";
|
||||
import { action } from "../utils/decorators";
|
||||
|
||||
export type QuestStatus = 'inactive' | 'active' | 'completed';
|
||||
export type QuestStatus = 'inactive' | 'active' | 'completed' | 'failed';
|
||||
|
||||
export interface QuestRuntimeState {
|
||||
status: QuestStatus;
|
||||
|
|
@ -46,11 +46,57 @@ export class QuestLog extends Component {
|
|||
return quest.conditions.every(c => evaluateCondition(parseCondition(c), ctx));
|
||||
}
|
||||
|
||||
@action
|
||||
fail(questId: string): boolean {
|
||||
const entry = this.#quests.get(questId);
|
||||
if (!entry || entry.state.status !== 'active') return false;
|
||||
entry.state.status = 'failed';
|
||||
this.emit('failed', { questId });
|
||||
return true;
|
||||
}
|
||||
|
||||
@action
|
||||
abandon(questId: string): boolean {
|
||||
const entry = this.#quests.get(questId);
|
||||
if (!entry || entry.state.status !== 'active') return false;
|
||||
entry.state.status = 'inactive';
|
||||
entry.state.stageIndex = 0;
|
||||
this.emit('abandoned', { questId });
|
||||
return true;
|
||||
}
|
||||
|
||||
getStage(questId: string): QuestStage | undefined {
|
||||
const entry = this.#quests.get(questId);
|
||||
if (!entry || entry.state.status !== 'active') return undefined;
|
||||
return entry.quest.stages[entry.state.stageIndex];
|
||||
}
|
||||
|
||||
getObjectiveProgress(
|
||||
questId: string,
|
||||
ctx: EvalContext,
|
||||
): Array<{ id: string; description: string; done: boolean }> | undefined {
|
||||
const stage = this.getStage(questId);
|
||||
if (!stage) return undefined;
|
||||
return stage.objectives.map(obj => ({
|
||||
id: obj.id,
|
||||
description: obj.description,
|
||||
done: evaluateCondition(parseCondition(obj.condition), ctx),
|
||||
}));
|
||||
}
|
||||
|
||||
/** @internal used by QuestSystem */
|
||||
entries(): IterableIterator<[string, QuestEntry]> {
|
||||
return this.#quests.entries();
|
||||
}
|
||||
|
||||
/** @internal called by QuestSystem when a fail condition is met */
|
||||
_fail(questId: string): void {
|
||||
const entry = this.#quests.get(questId);
|
||||
if (!entry) return;
|
||||
entry.state.status = 'failed';
|
||||
this.emit('failed', { questId });
|
||||
}
|
||||
|
||||
/** @internal called by QuestSystem after stage actions complete */
|
||||
_advance(questId: string): void {
|
||||
const entry = this.#quests.get(questId);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
import { ACTION_KEYS, VARIABLE_KEYS } from '../utils/decorators';
|
||||
import type { RPGActions, RPGVariables } from '../types';
|
||||
|
||||
interface EntityEvent<T = unknown> {
|
||||
target: Entity;
|
||||
data?: T;
|
||||
}
|
||||
interface WorldEvent<T = unknown> {
|
||||
target: World;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
type Class<T> = abstract new (...args: any[]) => T;
|
||||
type EventHandler = (data: unknown) => void;
|
||||
type EntityEventHandler = <T>(event: EntityEvent<T>) => void;
|
||||
type WorldEventHandler = <T>(event: WorldEvent<T>) => void;
|
||||
|
||||
export interface EvalContext {
|
||||
self: Entity;
|
||||
|
|
@ -132,15 +142,15 @@ export class Entity {
|
|||
this.world.emitGlobal(event, data);
|
||||
}
|
||||
|
||||
on(event: string, handler: EventHandler): () => void {
|
||||
on(event: string, handler: EntityEventHandler): () => void {
|
||||
return this.world.on(this.id, event, handler);
|
||||
}
|
||||
|
||||
off(event: string, handler: EventHandler): void {
|
||||
off(event: string, handler: EntityEventHandler): void {
|
||||
this.world.off(this.id, event, handler);
|
||||
}
|
||||
|
||||
once(event: string, handler: EventHandler): () => void {
|
||||
once(event: string, handler: EntityEventHandler): () => void {
|
||||
return this.world.once(this.id, event, handler);
|
||||
}
|
||||
|
||||
|
|
@ -165,8 +175,8 @@ export class World {
|
|||
readonly globals: RPGVariables = {};
|
||||
|
||||
readonly #entities = new Map<string, Entity>();
|
||||
readonly #handlers = new Map<string, Set<EventHandler>>();
|
||||
readonly #globalHandlers = new Map<string, Set<EventHandler>>();
|
||||
readonly #handlers = new Map<string, Set<EntityEventHandler>>();
|
||||
readonly #globalHandlers = new Map<string, Set<WorldEventHandler>>();
|
||||
readonly #systems: System[] = [];
|
||||
#entityCounter = 0;
|
||||
|
||||
|
|
@ -205,33 +215,42 @@ export class World {
|
|||
|
||||
removeSystem(system: System): void {
|
||||
const idx = this.#systems.indexOf(system);
|
||||
if (idx !== -1) { this.#systems.splice(idx, 1); system.onRemove(this); }
|
||||
if (idx !== -1) {
|
||||
this.#systems.splice(idx, 1);
|
||||
system.onRemove(this);
|
||||
}
|
||||
}
|
||||
|
||||
tick(dt: number): void {
|
||||
update(dt: number): void {
|
||||
for (const system of this.#systems) system.update(this, dt);
|
||||
}
|
||||
|
||||
emit(entityId: string, event: string, data?: unknown): void {
|
||||
this.#handlers.get(`${entityId}\0${event}`)?.forEach(h => h(data));
|
||||
const entity = this.getEntity(entityId);
|
||||
if (!entity) return;
|
||||
|
||||
this.#handlers.get(`${entityId}\0${event}`)?.forEach(h => h({
|
||||
target: entity,
|
||||
data,
|
||||
}));
|
||||
}
|
||||
|
||||
emitGlobal(event: string, data?: unknown): void {
|
||||
this.#globalHandlers.get(event)?.forEach(h => h(data));
|
||||
this.#globalHandlers.get(event)?.forEach(h => h({ target: this, data }));
|
||||
}
|
||||
|
||||
on(event: string, handler: EventHandler): () => void;
|
||||
on(entityId: string, event: string, handler: EventHandler): () => void;
|
||||
on(arg1: string, arg2: EventHandler | string, arg3?: EventHandler): () => void {
|
||||
on(event: string, handler: WorldEventHandler): () => void;
|
||||
on(entityId: string, event: string, handler: EntityEventHandler): () => void;
|
||||
on(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): () => void {
|
||||
if (typeof arg2 === 'string') {
|
||||
return this.#addHandler(this.#handlers, `${arg1}\0${arg2}`, arg3!);
|
||||
}
|
||||
return this.#addHandler(this.#globalHandlers, arg1, arg2);
|
||||
}
|
||||
|
||||
off(event: string, handler: EventHandler): void;
|
||||
off(entityId: string, event: string, handler: EventHandler): void;
|
||||
off(arg1: string, arg2: EventHandler | string, arg3?: EventHandler): void {
|
||||
off(event: string, handler: WorldEventHandler): void;
|
||||
off(entityId: string, event: string, handler: EntityEventHandler): void;
|
||||
off(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): void {
|
||||
if (typeof arg2 === 'string') {
|
||||
this.#handlers.get(`${arg1}\0${arg2}`)?.delete(arg3!);
|
||||
} else {
|
||||
|
|
@ -239,21 +258,21 @@ export class World {
|
|||
}
|
||||
}
|
||||
|
||||
once(event: string, handler: EventHandler): () => void;
|
||||
once(entityId: string, event: string, handler: EventHandler): () => void;
|
||||
once(arg1: string, arg2: EventHandler | string, arg3?: EventHandler): () => void {
|
||||
once(event: string, handler: WorldEventHandler): () => void;
|
||||
once(entityId: string, event: string, handler: EntityEventHandler): () => void;
|
||||
once(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): () => void {
|
||||
if (typeof arg2 === 'string') {
|
||||
const wrapped: EventHandler = data => { unsub(); arg3!(data); };
|
||||
const wrapped: EntityEventHandler = data => { unsub(); arg3!(data); };
|
||||
const unsub = this.on(arg1, arg2, wrapped);
|
||||
return unsub;
|
||||
}
|
||||
const h = arg2;
|
||||
const wrapped: EventHandler = data => { unsub(); h(data); };
|
||||
const wrapped: WorldEventHandler = data => { unsub(); h(data); };
|
||||
const unsub = this.on(arg1, wrapped);
|
||||
return unsub;
|
||||
}
|
||||
|
||||
#addHandler(map: Map<string, Set<EventHandler>>, key: string, handler: EventHandler): () => void {
|
||||
#addHandler<T extends WorldEventHandler | EntityEventHandler>(map: Map<string, Set<T>>, key: string, handler: T): () => void {
|
||||
if (!map.has(key)) map.set(key, new Set());
|
||||
map.get(key)!.add(handler);
|
||||
return () => map.get(key)?.delete(handler);
|
||||
|
|
|
|||
|
|
@ -243,8 +243,15 @@ export class DialogEngine {
|
|||
|
||||
async advance(nodeId?: string): Promise<DialogNodeView | null> {
|
||||
let targetId = nodeId ?? this.currentNode?.nextNodeId ?? (this.currentNode === null ? this.startNodeId : undefined);
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (targetId !== undefined) {
|
||||
if (visited.has(targetId)) {
|
||||
console.warn(`[DialogEngine] cycle detected at node '${targetId}', stopping`);
|
||||
return null;
|
||||
}
|
||||
visited.add(targetId);
|
||||
|
||||
const node = this.nodeMap.get(targetId);
|
||||
if (!node) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,20 @@ export class QuestSystem extends System {
|
|||
if (state.status !== 'active') continue;
|
||||
|
||||
const stage = quest.stages[state.stageIndex];
|
||||
if (!stage) continue;
|
||||
if (!stage) {
|
||||
console.warn(`[QuestSystem] quest '${questId}' is active but has no stage at index ${state.stageIndex}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stage.failConditions?.length) {
|
||||
const failed = stage.failConditions.some(c =>
|
||||
evaluateCondition(parseCondition(c), ctx)
|
||||
);
|
||||
if (failed) {
|
||||
questLog._fail(questId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const allDone = stage.objectives.every(obj =>
|
||||
evaluateCondition(parseCondition(obj.condition), ctx)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const QuestStageScheme = Type.Object({
|
|||
description: Type.String(),
|
||||
objectives: Type.Array(QuestObjectiveScheme),
|
||||
actions: Type.Array(RPGActionScheme),
|
||||
failConditions: Type.Optional(Type.Array(Type.String())),
|
||||
});
|
||||
|
||||
const QuestScheme = Type.Object({
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ export function resolveVariable(name: string, ctx: EvalContext): RPGVariables[st
|
|||
const entityId = name.slice(1, dotIdx);
|
||||
const varName = name.slice(dotIdx + 1);
|
||||
const entity = ctx.world.getEntity(entityId);
|
||||
if (!entity) return undefined;
|
||||
if (!entity) {
|
||||
console.warn(`[resolveVariable] entity '${entityId}' not found (referenced in '${name}')`);
|
||||
return undefined;
|
||||
}
|
||||
return resolveVariables(entity)[varName];
|
||||
}
|
||||
// bare name → self entity
|
||||
|
|
@ -53,14 +56,24 @@ export async function executeAction(action: RPGAction, ctx: EvalContext): Promis
|
|||
// @entityId.component.action → dispatch to another entity
|
||||
if (action.type.startsWith('@')) {
|
||||
const dotIdx = action.type.indexOf('.', 1);
|
||||
if (dotIdx === -1) return;
|
||||
if (dotIdx === -1) {
|
||||
console.warn(`[executeAction] malformed cross-entity action '${action.type}': missing '.' after entity id`);
|
||||
return;
|
||||
}
|
||||
const entityId = action.type.slice(1, dotIdx);
|
||||
const found = ctx.world.getEntity(entityId);
|
||||
if (!found) return;
|
||||
if (!found) {
|
||||
console.warn(`[executeAction] entity '${entityId}' not found (action '${action.type}')`);
|
||||
return;
|
||||
}
|
||||
entity = found;
|
||||
actionType = action.type.slice(dotIdx + 1);
|
||||
}
|
||||
|
||||
const actions = resolveActions(entity);
|
||||
return actions[actionType]?.(action.arg);
|
||||
if (!(actionType in actions)) {
|
||||
console.warn(`[executeAction] action '${actionType}' not found on entity '${entity.id}'`);
|
||||
return;
|
||||
}
|
||||
return actions[actionType](action.arg);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue