ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
370 lines (369 loc) • 15.1 kB
TypeScript
/**
* Behavior Tree Plugin for ECSpresso
*
* Provides composable, priority-driven AI via behavior trees. Shared immutable
* tree definitions drive per-entity runtime state. Uses hybrid traversal
* (Approach C): re-evaluate from root each tick to preserve priority, resume
* running leaves, and abort diverged running nodes via `onAbort`.
*
* Each entity gets a `behaviorTree` component referencing a shared definition
* plus a typed blackboard for per-entity AI memory. One system processes all
* behavior-tree entities each tick.
*
* Node types:
* Composites — sequence, selector, parallel
* Decorators — inverter, repeat, cooldown, guard
* Leaves — action (tick → NodeStatus, optional onAbort), condition (predicate)
*/
import { type BasePluginOptions, type BaseWorld, type ComponentsConfig, type EventsConfig } from 'ecspresso';
/**
* Return value from behavior tree node ticks.
*
* - `Success` (0) — node completed successfully
* - `Failure` (1) — node failed
* - `Running` (2) — node still executing, will resume next tick
*/
export declare const NodeStatus: {
readonly Success: 0;
readonly Failure: 1;
readonly Running: 2;
};
export type NodeStatus = (typeof NodeStatus)[keyof typeof NodeStatus];
/** BaseWorld narrowed to behavior-tree components for typed access in helpers. */
type BehaviorTreeWorld = BaseWorld<BehaviorTreeComponentTypes>;
/**
* Context passed to all leaf node callbacks (action tick, condition check,
* onAbort, guard predicates).
*
* @template BB - Blackboard type for per-entity AI memory
* @template W - World interface type (default: BehaviorTreeWorld)
*/
export interface BehaviorTreeContext<BB extends object = Record<string, unknown>, W extends BaseWorld<BehaviorTreeComponentTypes> = BehaviorTreeWorld> {
readonly ecs: W;
readonly entityId: number;
readonly dt: number;
readonly blackboard: BB;
}
/**
* Action leaf — executes behavior each tick.
* Returns `Running` for multi-frame actions. Optional `onAbort` fires
* when a higher-priority branch preempts this running action.
*/
export interface ActionNode<BB extends object = Record<string, unknown>> {
readonly type: 'action';
readonly name: string;
readonly tick: (ctx: BehaviorTreeContext<BB>) => NodeStatus;
readonly onAbort?: (ctx: BehaviorTreeContext<BB>) => void;
nodeIndex: number;
}
/**
* Condition leaf — checks a predicate. Returns Success or Failure, never Running.
*/
export interface ConditionNode<BB extends object = Record<string, unknown>> {
readonly type: 'condition';
readonly name: string;
readonly check: (ctx: BehaviorTreeContext<BB>) => boolean;
nodeIndex: number;
}
/**
* Sequence composite — runs children left-to-right.
* Fails on first failure, succeeds when all succeed.
* Resumes from stored child index when a running node exists.
*/
export interface SequenceNode<BB extends object = Record<string, unknown>> {
readonly type: 'sequence';
readonly children: readonly BehaviorTreeNode<BB>[];
nodeIndex: number;
}
/**
* Selector composite — runs children left-to-right.
* Succeeds on first success, fails when all fail.
* Always re-evaluates from child 0 to preserve priority ordering.
*/
export interface SelectorNode<BB extends object = Record<string, unknown>> {
readonly type: 'selector';
readonly children: readonly BehaviorTreeNode<BB>[];
nodeIndex: number;
}
/**
* Parallel composite — ticks all children each frame.
* Configurable success/failure thresholds.
*
* Limitation (v1): only one running leaf is tracked for abort.
* Other running children in a parallel stop being ticked if the
* tree path diverges but do not receive an `onAbort` call.
*/
export interface ParallelNode<BB extends object = Record<string, unknown>> {
readonly type: 'parallel';
readonly children: readonly BehaviorTreeNode<BB>[];
readonly successThreshold: number;
readonly failureThreshold: number;
nodeIndex: number;
}
/** Decorator — inverts child result (Success↔Failure), passes Running through. */
export interface InverterNode<BB extends object = Record<string, unknown>> {
readonly type: 'inverter';
readonly child: BehaviorTreeNode<BB>;
nodeIndex: number;
}
/** Decorator — repeats child `count` times (or forever when count is -1). */
export interface RepeatNode<BB extends object = Record<string, unknown>> {
readonly type: 'repeat';
readonly child: BehaviorTreeNode<BB>;
readonly count: number;
nodeIndex: number;
}
/** Decorator — prevents child re-entry for `duration` seconds after completion. */
export interface CooldownNode<BB extends object = Record<string, unknown>> {
readonly type: 'cooldown';
readonly child: BehaviorTreeNode<BB>;
readonly duration: number;
nodeIndex: number;
}
/** Decorator — conditional gate. Ticks child only when condition passes. */
export interface GuardNode<BB extends object = Record<string, unknown>> {
readonly type: 'guard';
readonly child: BehaviorTreeNode<BB>;
readonly condition: (ctx: BehaviorTreeContext<BB>) => boolean;
nodeIndex: number;
}
/** Union of all behavior tree node types. */
export type BehaviorTreeNode<BB extends object = Record<string, unknown>> = ActionNode<BB> | ConditionNode<BB> | SequenceNode<BB> | SelectorNode<BB> | ParallelNode<BB> | InverterNode<BB> | RepeatNode<BB> | CooldownNode<BB> | GuardNode<BB>;
/**
* Create an action leaf node.
*
* @param name - Human-readable name (used in abort events)
* @param tick - Called each frame while this node is active; return NodeStatus
* @param options - Optional `onAbort` callback fired when preempted by a higher-priority branch
*/
export declare function action<BB extends object>(name: string, tick: (ctx: BehaviorTreeContext<BB>) => NodeStatus, options?: {
onAbort?: (ctx: BehaviorTreeContext<BB>) => void;
}): ActionNode<BB>;
/**
* Create a condition leaf node.
*
* @param name - Human-readable name
* @param check - Predicate returning true (Success) or false (Failure). Never Running.
*/
export declare function condition<BB extends object>(name: string, check: (ctx: BehaviorTreeContext<BB>) => boolean): ConditionNode<BB>;
/**
* Create a sequence composite. Runs children L→R, fails on first failure.
*/
export declare function sequence<BB extends object>(children: BehaviorTreeNode<BB>[]): SequenceNode<BB>;
/**
* Create a selector composite. Runs children L→R, succeeds on first success.
* Always starts from child 0 to re-evaluate priority.
*/
export declare function selector<BB extends object>(children: BehaviorTreeNode<BB>[]): SelectorNode<BB>;
/**
* Create a parallel composite. Ticks all children each frame.
*
* @param children - Child nodes to tick in parallel
* @param options.successThreshold - Successes needed for parallel to succeed (default: all)
* @param options.failureThreshold - Failures needed for parallel to fail (default: all)
*/
export declare function parallel<BB extends object>(children: BehaviorTreeNode<BB>[], options?: {
successThreshold?: number;
failureThreshold?: number;
}): ParallelNode<BB>;
/** Create an inverter decorator. Flips Success↔Failure, passes Running. */
export declare function inverter<BB extends object>(child: BehaviorTreeNode<BB>): InverterNode<BB>;
/**
* Create a repeat decorator.
*
* @param child - Node to repeat
* @param count - Number of repetitions, or -1 for infinite (default: -1)
*/
export declare function repeat<BB extends object>(child: BehaviorTreeNode<BB>, count?: number): RepeatNode<BB>;
/**
* Create a cooldown decorator. Prevents re-entry for `duration` seconds
* after child completes (Success or Failure).
*/
export declare function cooldown<BB extends object>(child: BehaviorTreeNode<BB>, duration: number): CooldownNode<BB>;
/**
* Create a guard decorator. Ticks child only when condition returns true.
* Returns Failure when condition is false.
*/
export declare function guard<BB extends object>(cond: (ctx: BehaviorTreeContext<BB>) => boolean, child: BehaviorTreeNode<BB>): GuardNode<BB>;
/**
* Immutable behavior tree definition. Shared across entities.
*
* @template BB - Blackboard type for per-entity AI memory
*/
export interface BehaviorTreeDefinition<BB extends object = Record<string, unknown>> {
readonly id: string;
readonly root: BehaviorTreeNode<BB>;
readonly nodeCount: number;
}
/**
* Define a behavior tree with a typed blackboard.
*
* The `blackboard` value serves as both the type source and the default
* initial state cloned for each entity via `createBehaviorTree`.
*
* @param id - Unique identifier for this tree definition
* @param config - `{ blackboard, root }` — default blackboard + root node
* @returns Frozen BehaviorTreeDefinition
*
* @example
* ```typescript
* const tree = defineBehaviorTree('patrol', {
* blackboard: { targetId: null as number | null, timer: 0 },
* root: selector([
* guard(ctx => ctx.blackboard.targetId !== null, action('chase', ...)),
* action('wander', ...),
* ]),
* });
* ```
*/
export declare function defineBehaviorTree<BB extends object>(id: string, config: {
blackboard: BB;
root: BehaviorTreeNode<BB>;
}): BehaviorTreeDefinition<BB>;
/**
* Runtime behavior tree state stored on each entity.
*
* The `blackboard` is typed as `object` at the component level.
* Inside tree callbacks, the `BehaviorTreeContext<BB>` provides
* typed access to the blackboard via the tree definition's generic.
* Outside the tree, cast the blackboard to the specific BB type.
*/
export interface BehaviorTreeComponent {
readonly definition: BehaviorTreeDefinition<Record<string, unknown>>;
blackboard: object;
/** Index of the currently running leaf, or -1 if none. */
runningNodeIndex: number;
/**
* Dense per-node state array (sized to `definition.nodeCount`).
* Semantics vary by node type:
* - sequence/selector: child progress index
* - repeat: completed iteration count
* - cooldown: expiry timestamp (elapsedTime when cooldown ends)
*/
nodeState: Float64Array;
/** Accumulated time (seconds) for cooldown tracking. */
elapsedTime: number;
}
/**
* Component types provided by the behavior tree plugin.
*/
export interface BehaviorTreeComponentTypes {
behaviorTree: BehaviorTreeComponent;
}
/**
* Event published when a running action is preempted (aborted) by a
* higher-priority branch taking over.
*/
export interface BehaviorTreeAbortEvent {
entityId: number;
/** nodeIndex of the aborted action */
nodeIndex: number;
/** Human-readable name of the aborted action */
nodeName: string;
/** Definition id of the behavior tree */
definitionId: string;
}
/**
* Event types provided by the behavior tree plugin.
*/
export interface BehaviorTreeEventTypes {
behaviorTreeAbort: BehaviorTreeAbortEvent;
}
/**
* WorldConfig representing the behavior tree plugin's provided types.
*/
export type BehaviorTreeWorldConfig = ComponentsConfig<BehaviorTreeComponentTypes> & EventsConfig<BehaviorTreeEventTypes>;
/**
* Create a `behaviorTree` component from a definition.
*
* @param definition - Shared tree definition
* @param blackboard - Optional partial overrides for the default blackboard
* @returns Component object suitable for spreading into spawn()
*
* @example
* ```typescript
* ecs.spawn({
* ...createBehaviorTree(villagerTree, { hunger: 80 }),
* ...createLocalTransform(100, 200),
* });
* ```
*/
export declare function createBehaviorTree<BB extends object>(definition: BehaviorTreeDefinition<BB>, blackboard?: Partial<BB>): Pick<BehaviorTreeComponentTypes, 'behaviorTree'>;
/**
* Check whether an entity's behavior tree has a running action.
*/
export declare function isBehaviorTreeRunning(ecs: {
getComponent(entityId: number, name: 'behaviorTree'): BehaviorTreeComponent | undefined;
}, entityId: number): boolean;
/**
* Reset an entity's behavior tree: abort any running action, clear all
* composite progress, and optionally reset the blackboard.
*/
export declare function resetBehaviorTree(ecs: BehaviorTreeWorld, entityId: number, blackboard?: Partial<Record<string, unknown>>): void;
/**
* Typed helpers for the behavior tree plugin.
* Creates helpers that validate callback parameters against the world type W.
* Call after `.build()` using `typeof ecs`.
*/
export interface BehaviorTreeHelpers<W extends BaseWorld<BehaviorTreeComponentTypes>> {
defineBehaviorTree: <BB extends object>(id: string, config: {
blackboard: BB;
root: BehaviorTreeNode<BB>;
}) => BehaviorTreeDefinition<BB>;
action: <BB extends object>(name: string, tick: (ctx: BehaviorTreeContext<BB, W>) => NodeStatus, options?: {
onAbort?: (ctx: BehaviorTreeContext<BB, W>) => void;
}) => ActionNode<BB>;
condition: <BB extends object>(name: string, check: (ctx: BehaviorTreeContext<BB, W>) => boolean) => ConditionNode<BB>;
guard: <BB extends object>(cond: (ctx: BehaviorTreeContext<BB, W>) => boolean, child: BehaviorTreeNode<BB>) => GuardNode<BB>;
}
/**
* Create typed behavior tree helpers bound to a specific world type.
*
* @example
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createBehaviorTreePlugin())
* .build();
*
* const { defineBehaviorTree, action, condition, guard } = ecs.getHelpers(createBehaviorTreeHelpers);
* ```
*/
export declare function createBehaviorTreeHelpers<W extends BaseWorld<BehaviorTreeComponentTypes> = BehaviorTreeWorld>(_world?: W): BehaviorTreeHelpers<W>;
/**
* Configuration options for the behavior tree plugin.
*/
export interface BehaviorTreePluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {
}
/**
* Create a behavior tree plugin for ECSpresso.
*
* Provides composable, priority-driven AI via behavior trees with:
* - Hybrid traversal: re-evaluate from root each tick, resume running leaves
* - Automatic abort with `onAbort` callback when preempted
* - Typed blackboard for per-entity AI memory
* - `behaviorTreeAbort` events on preemption
*
* @example
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createBehaviorTreePlugin())
* .build();
*
* const { defineBehaviorTree, action, condition, guard } = ecs.getHelpers(createBehaviorTreeHelpers);
*
* const tree = defineBehaviorTree('villager', {
* blackboard: { hunger: 100, targetId: null as number | null },
* root: selector([
* guard(ctx => ctx.blackboard.hunger < 30, action('eat', ...)),
* action('wander', ...),
* ]),
* });
*
* ecs.spawn({
* ...createBehaviorTree(tree),
* ...createLocalTransform(100, 200),
* });
* ```
*/
export declare function createBehaviorTreePlugin<G extends string = 'ai'>(options?: BehaviorTreePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, BehaviorTreeComponentTypes>, BehaviorTreeEventTypes>, import("ecspresso").EmptyConfig, "behavior-tree-update", G, never, never>;
export {};