92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
|
|
import type { World, Entity } from "../index";
|
||
|
|
import { Task, ChildOf } from "./task";
|
||
|
|
import { TaskRunner } from "./runner";
|
||
|
|
|
||
|
|
// ── Types ─────────────────────────────────────────────
|
||
|
|
|
||
|
|
/** Return value from a leaf's `run` function. */
|
||
|
|
export type LeafResult = "success" | "fail" | "cancel" | void;
|
||
|
|
|
||
|
|
/** Declarative behaviour-tree definition. */
|
||
|
|
export type TreeDef =
|
||
|
|
| { kind: "leaf"; run: (world: World) => LeafResult }
|
||
|
|
| { 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 are called each tick the leaf is active:
|
||
|
|
* - `"success"` → leaf succeeds
|
||
|
|
* - `"fail"` → leaf fails
|
||
|
|
* - `"cancel"` → leaf (and subtree) is cancelled
|
||
|
|
* - `undefined` → leaf stays Running, re-invoked next tick
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```ts
|
||
|
|
* const runner = buildTree(world, {
|
||
|
|
* kind: "repeat",
|
||
|
|
* child: {
|
||
|
|
* kind: "sequential",
|
||
|
|
* children: [
|
||
|
|
* { kind: "leaf", run: () => { doWork(); } },
|
||
|
|
* { kind: "leaf", run: () => { render(); } },
|
||
|
|
* ],
|
||
|
|
* },
|
||
|
|
* });
|
||
|
|
* runner.schedule(runner.root);
|
||
|
|
* setInterval(() => runner.tick(), 16);
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export function buildTree(world: World, def: TreeDef): TaskRunner {
|
||
|
|
const leafHandlers = new Map<Entity, (world: World) => LeafResult>();
|
||
|
|
|
||
|
|
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) => {
|
||
|
|
const handler = leafHandlers.get(entity);
|
||
|
|
if (!handler) return;
|
||
|
|
const result = handler(_w);
|
||
|
|
if (result === "success") runner.succeed(entity);
|
||
|
|
else if (result === "fail") runner.fail(entity);
|
||
|
|
else if (result === "cancel") runner.cancel(entity);
|
||
|
|
// undefined → leaf stays Running, re-invoked next tick
|
||
|
|
};
|
||
|
|
|
||
|
|
// Stash the root entity on the runner for convenience
|
||
|
|
(runner as any).root = root;
|
||
|
|
|
||
|
|
return runner;
|
||
|
|
}
|