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); /** 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(); // Track generator iterators for multi-frame leaves const generators = new Map>(); 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; 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; }