Leaf nodes are the terminal nodes of a behaviour tree -- they have no children and perform actual work or checks.
Action is the abstract base class for nodes that perform work. Subclasses implement onTick(ctx) to return a NodeResult.
Flags: Leaf, Action
import { Action, NodeResult, TickContext } from '@bt-studio/core';
class MoveToTarget extends Action {
readonly defaultName = 'MoveToTarget';
constructor(private entity: { x: number; targetX: number }) {
super('Move to target');
}
protected onTick(_ctx: TickContext): NodeResult {
if (Math.abs(this.entity.x - this.entity.targetX) < 1) {
return NodeResult.Succeeded;
}
this.entity.x += Math.sign(this.entity.targetX - this.entity.x);
return NodeResult.Running;
}
}For simple actions, use the static factory instead of creating a subclass:
import { Action, NodeResult } from '@bt-studio/core';
const patrol = Action.from('Patrol', (ctx) => {
// ctx.now gives you the current tick timestamp
console.log(`Patrolling at tick ${ctx.tickId}`);
return NodeResult.Running;
});AsyncAction is an abstract base class for asynchronous work (e.g., HTTP requests, file I/O). It runs execute(ctx, signal) which returns a Promise. The node natively bridges this Promise into the tick lifecycle, returning Running while pending, and appropriately Succeeded or Failed when settled.
Flags: Leaf, Action, Stateful, TimeBased
import { AsyncAction, CancellationSignal, NodeResult, TickContext } from '@bt-studio/core';
class FetchData extends AsyncAction {
protected async execute(ctx: TickContext, signal: CancellationSignal): Promise<NodeResult | void> {
// Note: To use native fetch with CancellationSignal, you can convert it or map onAbort
const controller = new AbortController();
signal.onAbort(() => controller.abort());
const res = await fetch('https://api.example.com/data', { signal: controller.signal });
if (!res.ok) throw new Error('Fetch failed');
return NodeResult.Succeeded; // returning undefined is also Succeeded
}
}import { AsyncAction, NodeResult } from '@bt-studio/core';
const waitAndSucceed = AsyncAction.from('Wait a bit', async (ctx, signal) => {
await new Promise(resolve => setTimeout(resolve, 1000));
});If execute() throws or the returned Promise rejects, the node returns Failed. The error is accessible via lastError:
const node = new FetchData();
// ... after a failed tick:
node.lastError; // the thrown/rejected errorWhen an AsyncAction is aborted (e.g., a parent Sequence preempts it), signal.onAbort() callbacks fire and the current run is invalidated. A generation-based guard ensures that late-resolving promises from a cancelled run are silently ignored — no stale results leak into future executions.
getDisplayState() returns { status, error? } where status is 'idle' | 'pending' | 'resolved' | 'rejected'. The error field (stringified) is present only when status is 'rejected'.
ConditionNode evaluates a boolean check. Returns Succeeded if the check is true, Failed if false. Conditions never return Running -- they are pure, synchronous checks.
Flags: Leaf, Condition
import { ConditionNode } from '@bt-studio/core';
const hasHealth = ConditionNode.from('Has health?', (ctx) => entity.health > 0);
const enemyNearby = ConditionNode.from('Enemy nearby?', () => entity.enemyDistance < 10);import { ConditionNode, TickContext } from '@bt-studio/core';
class IsTimerExpired extends ConditionNode {
constructor(private deadline: number) {
super('Timer expired?', (ctx: TickContext) => ctx.now >= this.deadline);
}
}Four ready-made leaf nodes are provided for common patterns and testing:
Always returns Succeeded. Useful as a placeholder or to cap a sequence.
import { AlwaysSuccess } from '@bt-studio/core';
const noop = new AlwaysSuccess();
const named = new AlwaysSuccess('Placeholder');Always returns Failed. Useful for forcing fallback evaluation or testing.
import { AlwaysFailure } from '@bt-studio/core';
const fail = new AlwaysFailure();Always returns Running. Useful for testing stateful decorators or keeping a branch alive indefinitely.
import { AlwaysRunning } from '@bt-studio/core';
const idle = new AlwaysRunning('Idle');Returns Running for a specified duration, then Succeeded. A time-based action that uses ctx.now to track elapsed time.
Flags: Leaf, Action, Stateful
import { Sleep } from '@bt-studio/core';
const wait = new Sleep(2000); // Returns Running for 2000 time units, then SucceededSleep exposes getDisplayState() returning { remainingTime: number } for inspector integration.