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 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; }