UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

370 lines (369 loc) 15.1 kB
/** * 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 {};