ecs-observable/src/bt/tree-def.ts

151 lines
4.5 KiB
TypeScript
Raw Normal View History

import type { World, Entity } from "../index";
import { Task, ChildOf } from "./task";
import { TaskRunner } from "./runner";
// ── Cancel ────────────────────────────────────────────
/**
* Throw this inside a leaf `run` function to cancel the leaf and its subtree.
*
* @example
* ```ts
* { kind: "leaf", run: () => { throw Cancel; } }
* ```
*/
export const Cancel: unique symbol = Symbol("leaf.cancel");
// ── Tree definition ───────────────────────────────────
/** A leaf function — plain or generator. */
export type LeafFn =
| ((world: World, dt: number) => void)
| (() => Generator<number | void, void, number>);
/** Declarative behaviour-tree definition. */
export type TreeDef =
| { kind: "leaf"; run: LeafFn }
| { kind: "sequential"; children: TreeDef[] }
| { kind: "parallel"; children: TreeDef[] }
| { kind: "selector"; children: TreeDef[] }
| { kind: "random"; children: TreeDef[] }
| { kind: "repeat"; child: TreeDef };
// ── Builder ───────────────────────────────────────────
/**
* Recursively materialize a `TreeDef` into ECS entities and return a
* fully-wired `TaskRunner`.
*
* Leaf `run` functions:
* - **Plain function** runs once per tick. `return` = success. `throw` = fail.
* `throw Cancel` = cancel.
* - **Generator function** each `yield` suspends until next tick. The value
* yielded is the desired delay in ms (or `undefined` for next frame).
* Generator completion = success. `throw` = fail. `throw Cancel` = cancel.
*
* @example
* ```ts
* const runner = buildTree(world, {
* kind: "repeat",
* child: {
* kind: "sequential",
* children: [
* { kind: "leaf", run: () => { doWork(); } },
* { kind: "leaf", *run() { yield 1000; doLater(); } },
* ],
* },
* });
* runner.schedule(runner.root);
* setInterval(() => runner.tick(16), 16);
* ```
*/
export function buildTree(world: World, def: TreeDef): TaskRunner {
const leafHandlers = new Map<Entity, LeafFn>();
// Track generator iterators for multi-frame leaves
const generators = new Map<Entity, Generator<number | void, void, number>>();
function build(def: TreeDef, parent?: Entity): Entity {
const entity = world.spawn();
if (def.kind === "leaf") {
world.add(entity, Task, { kind: "leaf" });
leafHandlers.set(entity, def.run);
} else if (def.kind === "repeat") {
world.add(entity, Task, { kind: "repeat" });
build(def.child, entity);
} else {
world.add(entity, Task, { kind: def.kind });
for (const child of def.children) {
build(child, entity);
}
}
if (parent) {
world.relate(entity, ChildOf, parent);
}
return entity;
}
const root = build(def);
const runner = new TaskRunner(world);
runner.onLeaf = (_w, entity, dt) => {
const handler = leafHandlers.get(entity);
if (!handler) return;
try {
// Check if this leaf has an active generator
let gen = generators.get(entity);
if (gen) {
// Resume existing generator
const result = gen.next(dt);
if (result.done) {
generators.delete(entity);
runner.succeed(entity);
}
// If not done, leaf stays Running — nothing to do
} else {
// First invocation — call the handler
const ret = handler(_w, dt);
// Check if it returned a generator
if (ret != null && typeof (ret as any).next === "function") {
const gen = ret as Generator<number | void, void, number>;
generators.set(entity, gen);
const result = gen.next(dt);
if (result.done) {
generators.delete(entity);
runner.succeed(entity);
}
// Not done → leaf stays Running
} else {
// Plain function — returned undefined → success
runner.succeed(entity);
}
}
} catch (err) {
// Clean up generator if one was active
generators.delete(entity);
if (err === Cancel) {
runner.cancel(entity);
} else {
runner.fail(entity);
}
}
};
runner.onTerminal = (_w, entity) => {
// Clean up generator when a leaf reaches terminal by external means
generators.delete(entity);
};
// Stash the root entity on the runner for convenience
(runner as any).root = root;
return runner;
}