222 lines
6.5 KiB
TypeScript
222 lines
6.5 KiB
TypeScript
import type { World, Entity } from "../index";
|
|
import type { EntityDef, EntityDefChild } from "../entity-tree";
|
|
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
|
|
* leaf(() => { 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>);
|
|
|
|
export interface LeafTaskMeta {
|
|
readonly run: LeafFn;
|
|
}
|
|
|
|
export type TaskEntityDef = EntityDef<
|
|
typeof Task.type,
|
|
LeafTaskMeta | undefined
|
|
>;
|
|
|
|
/** Behaviour-tree definition accepted by `buildTree`. */
|
|
export type TreeDef = TaskEntityDef;
|
|
|
|
// ── Entity task factories ──────────────────────────────
|
|
|
|
function compactChildren(children: readonly EntityDefChild[]): EntityDef[] {
|
|
return children.filter(Boolean) as EntityDef[];
|
|
}
|
|
|
|
function task(
|
|
kind: typeof Task.type.kind,
|
|
children: readonly EntityDefChild[] = [],
|
|
meta?: LeafTaskMeta,
|
|
): TaskEntityDef {
|
|
return {
|
|
kind: "entity",
|
|
component: Task,
|
|
value: { kind },
|
|
children: compactChildren(children),
|
|
meta,
|
|
};
|
|
}
|
|
|
|
/** Create a leaf task entity definition. */
|
|
export function leaf(
|
|
run: LeafFn,
|
|
children: readonly EntityDefChild[] = [],
|
|
): TaskEntityDef {
|
|
return task("leaf", children, { run });
|
|
}
|
|
|
|
/** Create a sequential task entity definition. */
|
|
export function sequential(
|
|
children: readonly EntityDefChild[] = [],
|
|
): TaskEntityDef {
|
|
return task("sequential", children);
|
|
}
|
|
|
|
/** Create a parallel task entity definition. */
|
|
export function parallel(
|
|
children: readonly EntityDefChild[] = [],
|
|
): TaskEntityDef {
|
|
return task("parallel", children);
|
|
}
|
|
|
|
/** Create a selector task entity definition. */
|
|
export function selector(
|
|
children: readonly EntityDefChild[] = [],
|
|
): TaskEntityDef {
|
|
return task("selector", children);
|
|
}
|
|
|
|
/** Create a random task entity definition. */
|
|
export function random(
|
|
children: readonly EntityDefChild[] = [],
|
|
): TaskEntityDef {
|
|
return task("random", children);
|
|
}
|
|
|
|
/**
|
|
* Create a repeat task entity definition.
|
|
*
|
|
* `repeat(child)` and `repeat([child, metadataEntity])` are both supported.
|
|
* The runner operates only on child entities that have the `Task` component.
|
|
*/
|
|
export function repeat(child: EntityDef): TaskEntityDef;
|
|
export function repeat(children?: readonly EntityDefChild[]): TaskEntityDef;
|
|
export function repeat(
|
|
childOrChildren: EntityDef | readonly EntityDefChild[] = [],
|
|
): TaskEntityDef {
|
|
return task(
|
|
"repeat",
|
|
Array.isArray(childOrChildren) ? childOrChildren : [childOrChildren],
|
|
);
|
|
}
|
|
|
|
// ── Builder ───────────────────────────────────────────
|
|
|
|
/**
|
|
* Materialize a behaviour-tree definition into ECS entities and return a
|
|
* fully-wired `TaskRunner`.
|
|
*
|
|
* Definitions are `EntityDef` trees produced by the task factories (`leaf`,
|
|
* `sequential`, `parallel`, `selector`, `random`, `repeat`) and generic
|
|
* single-component entity factories. Non-task child entities are materialized
|
|
* into the ECS tree but ignored by `TaskRunner` execution.
|
|
*
|
|
* 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.
|
|
*/
|
|
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: EntityDef, parent?: Entity): Entity {
|
|
const entity = world.spawn();
|
|
world.add(entity, def.component, def.value);
|
|
|
|
if (parent !== undefined) {
|
|
world.relate(entity, ChildOf, parent);
|
|
}
|
|
|
|
if (def.component === Task) {
|
|
const taskData = world.get(entity, Task);
|
|
if (taskData.kind === "leaf") {
|
|
const run = (def.meta as LeafTaskMeta | undefined)?.run;
|
|
if (!run) {
|
|
throw new Error("Leaf task entity is missing a run function");
|
|
}
|
|
leafHandlers.set(entity, run);
|
|
}
|
|
}
|
|
|
|
for (const child of def.children) {
|
|
build(child, entity);
|
|
}
|
|
|
|
return entity;
|
|
}
|
|
|
|
const root = build(def);
|
|
|
|
if (!world.has(root, Task)) {
|
|
throw new Error("buildTree root must be a task entity");
|
|
}
|
|
|
|
const runner = new TaskRunner(world);
|
|
runner.root = root;
|
|
|
|
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);
|
|
};
|
|
|
|
return runner;
|
|
}
|