Intent
new Intent()Intent - Base class for AI behaviors
Intents represent high-level goals or behaviors (Patrol, Attack, Flee, etc.). The Coordinator selects which intent is active based on priority and activation conditions. Only one intent is active at a time.
Quick Start: Creating a Custom Intent
The AgentIntent Output
The update() method must return an AgentIntent object that tells the
agent what actions to take. Use the this._intent property (pre-allocated)
to avoid memory allocations.
AgentIntent Structure
update(body, state, memory, delta) {
this._intent.reset(); // Always reset first!
// MOVEMENT: Where to go
this._intent.move.destination = { x: 10, y: 0, z: 5 }; // World position
this._intent.move.speed = 'walk'; // 'walk' | 'run' | 'sprint'
this._intent.move.strafe = true; // Sideways movement while facing target
this._intent.move.stop = false; // Stop and hold position
// AIMING: Where to look
this._intent.aim.target = { x: 15, y: 1.5, z: 8 }; // World position to aim at
this._intent.aim.smoothing = 0.15; // 0 = instant, higher = slower turn
this._intent.aim.leadTarget = true; // Predict target movement
this._intent.aim.overrideMovementRotation = true; // Aim overrides move direction
// FIRING: When to shoot
this._intent.fire.trigger = true; // Fire this frame
this._intent.fire.mode = 'single'; // 'single' | 'burst' | 'auto'
this._intent.fire.burstCount = 3; // Shots per burst
return this._intent;
}
Common Patterns
// Move to position
this._intent.move.destination = targetPosition;
this._intent.move.speed = 'run';
// Stop moving
this._intent.move.stop = true;
// Strafe around target while aiming
this._intent.move.strafe = true;
this._intent.aim.target = enemyPosition;
// Fire at target
this._intent.aim.target = enemyPosition;
this._intent.fire.trigger = true;
Reading from State and Memory
Intents read perception data written by Sensors.
State API (instant data from sensors)
canActivate(state, memory) {
// Get values with defaults
const targetVisible = state.get('targetVisible', false);
const targetPosition = state.get('targetPosition'); // null if not set
const health = state.get('health', 100);
const ammo = state.get('ammo', 30);
// Check if data is fresh (age in seconds)
const age = state.getAge('targetPosition');
if (age > 2.0) {
// Data is stale, target might have moved
}
return targetVisible && health > 20;
}
Memory API (time-decaying data)
update(body, state, memory, delta) {
// Recall decaying memories
const lastSeenPos = memory.recall('lastSeenPosition'); // null if decayed
const lastDamageDir = memory.recall('lastDamageDirection');
// Check confidence (1.0 = fresh, 0.0 = about to expire)
const confidence = memory.getConfidence('lastSeenPosition');
if (confidence < 0.3) {
// Memory is fading, less reliable
}
// Check if memory exists
if (memory.has('wasRecentlyAttacked')) {
// React to recent attack
}
}
Complete Examples
Example 1: Investigate Intent
Go investigate a sound that was heard.
import { Intent } from '@three-blocks/pro';
class InvestigateIntent extends Intent {
static type = 'InvestigateIntent';
constructor(config = {}) {
super({ priority: 40, minDwellTime: 3.0, ...config });
this._investigateTime = config.investigateTime ?? 5.0;
this._timeAtLocation = 0;
}
canActivate(state, memory) {
// Activate if we heard a sound and aren't in combat
const heardSound = memory.has('lastHeardSound');
const inCombat = state.get('targetVisible', false);
return heardSound && !inCombat;
}
onActivate(state, memory) {
super.onActivate(state, memory);
this._timeAtLocation = 0;
}
update(body, state, memory, delta) {
this._intent.reset();
const sound = memory.recall('lastHeardSound');
if (!sound) {
// Memory decayed, stop investigating
this._intent.move.stop = true;
return this._intent;
}
// Move to sound location
const myPos = body.transformer.position;
const dx = sound.position.x - myPos.x;
const dz = sound.position.z - myPos.z;
const distance = Math.sqrt(dx * dx + dz * dz);
if (distance > 2.0) {
// Still moving to location
this._intent.move.destination = sound.position;
this._intent.move.speed = 'walk';
} else {
// At location, look around
this._timeAtLocation += delta;
this._intent.move.stop = true;
// Rotate to scan area
const angle = this._timeAtLocation * 0.5;
this._intent.aim.target = {
x: myPos.x + Math.sin(angle) * 5,
y: myPos.y + 1,
z: myPos.z + Math.cos(angle) * 5,
};
// Clear memory after investigating
if (this._timeAtLocation > this._investigateTime) {
memory.forget('lastHeardSound');
}
}
return this._intent;
}
serialize() {
return {
...super.serialize(),
investigateTime: this._investigateTime,
};
}
}
Example 2: Take Cover Intent
Find cover when under fire.
import { Intent } from '@three-blocks/pro';
class TakeCoverIntent extends Intent {
static type = 'TakeCoverIntent';
constructor(config = {}) {
super({ priority: 85, minDwellTime: 3.0, ...config });
this._coverPositions = config.coverPositions ?? [];
this._currentCover = null;
}
canActivate(state, memory) {
// Activate when under fire and low health
const underFire = state.get('underFire', false);
const health = state.get('health', 100);
return underFire && health < 50;
}
onActivate(state, memory) {
super.onActivate(state, memory);
// Find nearest cover position
this._currentCover = this._findNearestCover(state);
}
_findNearestCover(state) {
const myPos = state.get('myPosition');
if (!myPos || this._coverPositions.length === 0) return null;
let nearest = null;
let nearestDist = Infinity;
for (const cover of this._coverPositions) {
const dx = cover.x - myPos.x;
const dz = cover.z - myPos.z;
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist < nearestDist) {
nearestDist = dist;
nearest = cover;
}
}
return nearest;
}
update(body, state, memory, delta) {
this._intent.reset();
if (!this._currentCover) {
// No cover found, just crouch
this._intent.move.stop = true;
return this._intent;
}
// Move to cover
this._intent.move.destination = this._currentCover;
this._intent.move.speed = 'sprint';
return this._intent;
}
serialize() {
return {
...super.serialize(),
coverPositions: this._coverPositions,
};
}
}
Example 3: Reload Intent
Reload when out of ammo.
import { Intent } from '@three-blocks/pro';
class ReloadIntent extends Intent {
static type = 'ReloadIntent';
constructor(config = {}) {
super({ priority: 70, minDwellTime: 2.0, ...config });
this._reloadTime = config.reloadTime ?? 2.0;
this._reloadProgress = 0;
}
canActivate(state, memory) {
const isOutOfAmmo = state.get('isOutOfAmmo', false);
const isReloading = state.get('isReloading', false);
return isOutOfAmmo && !isReloading;
}
onActivate(state, memory) {
super.onActivate(state, memory);
this._reloadProgress = 0;
}
update(body, state, memory, delta) {
this._intent.reset();
// Stop and reload
this._intent.move.stop = true;
this._reloadProgress += delta;
// Keep aiming at last known threat
const threatPos = memory.recall('lastSeenPosition');
if (threatPos) {
this._intent.aim.target = threatPos;
}
// Reload complete (your game should handle actual ammo)
if (this._reloadProgress >= this._reloadTime) {
// Signal reload complete via state
state.set('reloadComplete', true);
}
return this._intent;
}
serialize() {
return {
...super.serialize(),
reloadTime: this._reloadTime,
};
}
}
Priority System
When multiple intents can activate, the Coordinator selects the one with highest priority. Common priority ranges:
| Priority | Intent Type | Description |
|---|---|---|
| 0-20 | Idle, Wander | Default behaviors when nothing else to do |
| 20-40 | Patrol | Routine movement along waypoints |
| 40-60 | Investigate | React to sounds or other stimuli |
| 60-80 | Engage, Attack | Combat behaviors |
| 80-100 | Flee, TakeCover | Survival behaviors (highest priority) |
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
priority |
number | 0 | Selection priority (higher = more important) |
minDwellTime |
number | 1.0 | Minimum seconds to stay active before switching |
Lifecycle
canActivate(state, memory)- Called each frame to check eligibilityonActivate(state, memory)- Called once when intent becomes activeupdate(body, state, memory, delta)- Called each frame while activeonDeactivate(state, memory)- Called when switching to another intentdispose()- Called when intent is removed
Example
import { Intent } from '@three-blocks/pro';
class FleeIntent extends Intent {
// REQUIRED: Unique type identifier for serialization
static type = 'FleeIntent';
constructor(config = {}) {
// Pass config to parent (sets priority, minDwellTime)
super({ priority: 90, minDwellTime: 2.0, ...config });
// Store custom config
this._fleeDistance = config.fleeDistance ?? 20;
this._healthThreshold = config.healthThreshold ?? 30;
}
// REQUIRED: Return true when this intent should be considered
canActivate(state, memory) {
const health = state.get('health', 100);
const threatScore = state.get('threatScore', 0);
return health < this._healthThreshold && threatScore > 0.5;
}
// REQUIRED: Return AgentIntent with move/aim/fire instructions
update(body, state, memory, delta) {
// Reset the reusable intent object
this._intent.reset();
const threatDir = memory.recall('lastDamageDirection');
if (threatDir) {
// Flee opposite to threat direction
const pos = body.transformer.position;
this._intent.move.destination = {
x: pos.x - threatDir.x * this._fleeDistance,
y: pos.y,
z: pos.z - threatDir.z * this._fleeDistance,
};
this._intent.move.speed = 'sprint';
}
return this._intent;
}
// REQUIRED: Include custom config in serialization
serialize() {
return {
...super.serialize(),
fleeDistance: this._fleeDistance,
healthThreshold: this._healthThreshold,
};
}
}
Properties
# .type : string
Intent type identifier. REQUIRED: Override in subclass.
This string is used for serialization and factory lookup. Must match the key used when registering the intent factory.
# .id : string
Unique ID for this intent instance. Auto-generated, used for tracking and removal.
# .type :
Get intent type.
# .priority :
Get selection priority.
# .priority :
Set selection priority.
# .minDwellTime :
Get minimum dwell time.
# .minDwellTime :
Set minimum dwell time.
# .isActive :
Check if this intent is currently active.
Methods
emit#
emit(eventType : string, params : Object)Emit an AI event that will be dispatched to the main thread.
Use this to notify user code of significant actions or state changes. Events are queued and dispatched at the end of each physics frame.
Parameters
eventTypestringparamsoptionalObjectDefault is
{}.canActivate#
canActivate(state : State, memory : Memory) : booleanCheck if this intent can be activated. REQUIRED: Override in subclass.
Return true when conditions are met for this behavior.
The Coordinator calls this each frame to determine which intent to select.
Parameters
Returns
boolean — True if this intent should be considered for activation.update#
update(body : Object, state : State, memory : Memory, delta : number) : AgentIntentUpdate this intent and return action instructions. REQUIRED: Override in subclass.
Called every frame while this intent is active.
Return an AgentIntent with move/aim/fire instructions.
Important: Always call this._intent.reset() at the start!
Parameters
Returns
AgentIntent — Action instructions for the agent.onActivate#
onActivate(state : State, memory : Memory)Called when this intent becomes active. Override to initialize state when intent starts.
Important: Always call super.onActivate(state, memory)!
Parameters
onDeactivate#
onDeactivate(state : State, memory : Memory)Called when this intent is deactivated. Override to clean up state when intent ends.
Important: Always call super.onDeactivate(state, memory)!
Parameters
set#
set(config : Object) : IntentConfigure intent parameters at runtime.
Parameters
configObjectpriorityoptionalnumberNew priority value.minDwellTimeoptionalnumberNew minimum dwell time.
Returns
Intent — This instance for chaining.serialize#
serialize() : ObjectSerialize intent configuration for worker transfer.
REQUIRED: Override in subclass to include custom config.
Always call super.serialize() and spread the result.
Returns
Object — Serialized configuration including type, id, and all config.dispose#
dispose()Clean up intent resources. Override in subclass if you need to clean up event listeners, timers, or other resources.