Intent

@three-blocks/proAIIntentBehavior
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

  1. canActivate(state, memory) - Called each frame to check eligibility
  2. onActivate(state, memory) - Called once when intent becomes active
  3. update(body, state, memory, delta) - Called each frame while active
  4. onDeactivate(state, memory) - Called when switching to another intent
  5. dispose() - 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
eventTypestring
The event type (e.g., 'fire', 'engage', 'flee').
paramsoptionalObject
Event payload data.
Default is {}.

canActivate#

canActivate(state : State, memory : Memory) : boolean

Check 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
Current perception state from sensors.
Time-decaying memory from sensors.
Returns
boolean — True if this intent should be considered for activation.

update#

update(body : Object, state : State, memory : Memory, delta : number) : AgentIntent

Update 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
bodyObject
The physics body. Access position via body.transformer.position.
Current perception state from sensors.
Time-decaying memory from sensors.
deltanumber
Time delta in seconds since last frame.
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
Current perception state.
Time-decaying memory.

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
Current perception state.
Time-decaying memory.

set#

set(config : Object) : Intent

Configure intent parameters at runtime.

Parameters
configObject
Configuration object.
  • priorityoptionalnumber
    New priority value.
  • minDwellTimeoptionalnumber
    New minimum dwell time.
Returns
Intent — This instance for chaining.

serialize#

serialize() : Object

Serialize 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.