2026-06-01 23:57:19 +08:00
|
|
|
import type { World, Entity } from "../index";
|
|
|
|
|
import { Task, ChildOf } from "./task";
|
|
|
|
|
import { TaskRunner } from "./runner";
|
|
|
|
|
|
2026-06-02 00:10:18 +08:00
|
|
|
// ── Cancel ────────────────────────────────────────────
|
2026-06-01 23:57:19 +08:00
|
|
|
|
2026-06-02 00:10:18 +08:00
|
|
|
/**
|
|
|
|
|
* 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>);
|
2026-06-01 23:57:19 +08:00
|
|
|
|
|
|
|
|
/** Declarative behaviour-tree definition. */
|
|
|
|
|
export type TreeDef =
|
2026-06-02 00:10:18 +08:00
|
|
|
| { kind: "leaf"; run: LeafFn }
|
2026-06-01 23:57:19 +08:00
|
|
|
| { 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`.
|
|
|
|
|
*
|
2026-06-02 00:10:18 +08:00
|
|
|
* 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.
|
2026-06-01 23:57:19 +08:00
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
* ```ts
|
|
|
|
|
* const runner = buildTree(world, {
|
|
|
|
|
* kind: "repeat",
|
|
|
|
|
* child: {
|
|
|
|
|
* kind: "sequential",
|
|
|
|
|
* children: [
|
|
|
|
|
* { kind: "leaf", run: () => { doWork(); } },
|
2026-06-02 00:10:18 +08:00
|
|
|
* { kind: "leaf", *run() { yield 1000; doLater(); } },
|
2026-06-01 23:57:19 +08:00
|
|
|
* ],
|
|
|
|
|
* },
|
|
|
|
|
* });
|
|
|
|
|
* runner.schedule(runner.root);
|
2026-06-02 00:10:18 +08:00
|
|
|
* setInterval(() => runner.tick(16), 16);
|
2026-06-01 23:57:19 +08:00
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
export function buildTree(world: World, def: TreeDef): TaskRunner {
|
2026-06-02 00:10:18 +08:00
|
|
|
const leafHandlers = new Map<Entity, LeafFn>();
|
|
|
|
|
// Track generator iterators for multi-frame leaves
|
|
|
|
|
const generators = new Map<Entity, Generator<number | void, void, number>>();
|
2026-06-01 23:57:19 +08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2026-06-02 00:10:18 +08:00
|
|
|
runner.onLeaf = (_w, entity, dt) => {
|
2026-06-01 23:57:19 +08:00
|
|
|
const handler = leafHandlers.get(entity);
|
|
|
|
|
if (!handler) return;
|
2026-06-02 00:10:18 +08:00
|
|
|
|
|
|
|
|
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);
|
2026-06-01 23:57:19 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Stash the root entity on the runner for convenience
|
|
|
|
|
(runner as any).root = root;
|
|
|
|
|
|
|
|
|
|
return runner;
|
|
|
|
|
}
|