# Gameplay AI tasks

`hi.ai.tasks` extends the Brain executor with game-defined actions. The same action can run from a cooked Brain asset or a `hi.ai.brain.compile` definition in Zig. Built-in `move_to` and `wait` remain available. Tasks are ordinary gameplay code; the engine owns binding, activation receipts, cancellation reconciliation, completion/failure transitions and bounded diagnostics.

## Define and register a task

A task declares a stable identifier, a capability mask and a `Params` struct. Parameters currently support **defaulted `f32`, `i32`, and `bool` fields**, at most eight per task. Use integer slots to address game-owned targets or resources; borrowed actor/component pointers never belong in parameters.

```zig
const hi = @import("hikari_game");
const tasks = hi.ai.tasks;

pub const Charge = struct {
    pub const name = "charge";
    pub const capabilities: u64 = 1; // Game-defined: this adapter can charge.
    pub const Params = struct { seconds: f32 = 2 };

    pub fn start(_: hi.ActorContext, p: Params, _: tasks.Ticket) tasks.Result {
        if (p.seconds <= 0) return tasks.Result.done(.failure, "invalid_duration");
        return .{};
    }
    pub fn tick(_: hi.ActorContext, p: Params, _: tasks.Ticket, elapsed: f32, dt: f32) tasks.Result {
        if (elapsed + dt >= p.seconds) return tasks.Result.done(.success, "charged");
        return .{ .progress = (elapsed + dt) / p.seconds };
    }
    pub fn cancel(_: hi.ActorContext, _: tasks.Ticket) void {
        // Release any game-owned interaction/reservation here.
    }
};
pub const Tasks = tasks.Registry(.{Charge});

// Export this declaration from the game's root.zig.
pub const ai_tasks = Tasks.descriptors;
```

The game module copies this catalog into each World during content registration. The Brain editor uses a copy of the metadata, with no game function pointers. There are at most 32 task types per registry/catalog. A game composing plugin tasks exports one combined registry. Rebuild the game and reopen an already open Brain editor when changing the task catalog.

The context type belongs to the adapter. It may be `ActorContext` or a small borrowed struct providing operations for a swarm member. Handler context is supplied afresh for every call and must never escape the callback. Keep persistent action-specific state in the owning component/member, and clean it in `cancel`. An optional `finish(context, ticket, result)` hook runs once for terminal success/failure, including an accepted delayed result. Use it for cleanup or simulation-thread effects common to synchronous and asynchronous completion. It never runs for a stale receipt.

## Author an action in a Brain

In the Brain editor, select a state and choose the registered action in **Task**. Its parameters appear as shared numeric fields, integer text fields and checkboxes. The normal completion/failure destinations, transitions, validation, Save and Undo/Redo still apply. Unknown tasks/arguments or mismatched argument types prevent saving against the loaded catalog.

The corresponding state is:

```json
{
  "name": "charge_weapon",
  "task": "custom",
  "action": {
    "name": "charge",
    "arguments": [
      { "name": "seconds", "value": { "kind": "number", "number": 2.5 } }
    ]
  },
  "next": "fire",
  "failure": "retreat"
}
```

Argument values carry a kind and its matching payload (`number`, `integer`, or `boolean`). Other payload fields must remain at their defaults. Omitted arguments use the task's declared defaults. Shinra validates and encodes this data; the game registry validates task identities, field names, types and required capabilities when binding. Shinra does not execute or link gameplay handlers.

For a code-only Brain, use the same state in Zig:

```zig
const definition = try hi.ai.brain.compile(.{
    .entry = "charge_weapon",
    .target_count = 0,
    .states = &.{
        .{ .name = "charge_weapon", .task = .custom,
           .action = .{ .name = "charge", .arguments = &.{
               .{ .name = "seconds", .value = .{ .number = 2.5 } },
           } }, .next = "idle", .failure = "idle" },
        .{ .name = "idle", .next = "idle", .failure = "idle" },
    },
});
const bound = try Tasks.bind(&definition, 1);
```

No asset, JSON parsing, or editor is required for this path.

## Drive the lifecycle

Bind before starting the Brain. `Tasks.bind(definition, capabilities)` validates the complete program and produces an immutable table of task/argument indices. Share this table across adapters using that definition and registry. It contains no function or asset pointers. Rebind whenever the program or registry changes; never combine a binding with a different definition.

At actor startup, create a driver using a fresh World receipt:

```zig
const Driver = Tasks.Driver(8); // Eight lifecycle trace entries.
var driver: Driver = .{
    .owner = actor.world().aiTaskOwner(hi.ai.population.Subject.actor(actor.id)),
};
```

Store the driver and Brain instance in the component's transient `state`. Acquire the owner once per component/member lifetime, never once per tick. Each simulation update:

```zig
_ = execution.decide(def, facts);
driver.step(actor, &bound, def, &execution, dt);
const movement = execution.step(def, actor.position(), targets, dt);
// Consume movement through the game's navigation/motor adapter.
```

The driver cancels an interrupted activation before starting the new custom task. `start` runs once; subsequent positive simulation steps call `tick`. A task may finish immediately from `start`. Success follows `next`; failure follows `failure`. Built-in/custom execution is bounded and does not recursively drain a chain of instant actions. Zero/nonfinite time does not advance a task.

If a transition occurs outside that update sequence, call `driver.sync(context, &execution)` immediately after it. On disable, destruction, retirement or rebinding, call `execution.cancel()` and `driver.cancel(context)` before releasing gameplay resources. Cancellation runs at most once for an active task; it drops any pending completion. Retired swarm rows must do this before recycling their storage.

## Delayed work and diagnostics

Handlers receive a `Ticket` containing the World, actor/swarm identity, World-issued incarnation, Brain activation and driver serial. A worker may retain that value and copied inputs, never an `ActorContext`, driver pointer, component pointer, or borrowed definition. Marshal its result back to the owning simulation thread, resolve the still-live subject, then call:

```zig
const accepted = driver.report(&execution, ticket,
    tasks.Result.done(.success, "animation_finished"));
```

The driver rejects old activations, other owners/Worlds, retired/replaced lifetimes, duplicate pending deliveries and invalid progress. Accepted results are consumed by the next positive simulation step. Reconcile cancellation before the delivery can cause gameplay effects; the worker must not perform those effects itself. `report` is not a cross-thread queue.

`driver.latest` reports progress (0–1), status and a bounded identifier explaining the result. `recent(age)` returns lifecycle history with its ticket. History records starts, finishes and cancellations rather than a record every tick. `Driver(0)` removes history storage for populations. Tasks are opt-in: ordinary `brain.CompactInstance` stays unchanged, and populations do not need actor objects to implement a task. Capability masks are game-defined contracts checked at binding, not runtime object type guesses.

## Playable acceptance test

Open **Tutorials → AI Gameplay Tasks** (`scenes/ai_tasks.json`) in the example game. Its one guard uses `brains/guard_door.brain.json`; the shared two-guard Brain demonstration remains in AI Perception & Brains.

1. Disable **Targets detectable**, choose **Noise**, radius **16 m**, and click behind the blue door around `(0, 0, -2)`.
2. The guard approaches its authored interaction point, runs `open_door` for two seconds, opens the door, investigates, looks around and returns.
3. Enable **Door locked**, press **Reset door encounter**, and repeat. `door_locked` explains the failure and the guard returns without opening it.
4. Unlock/reset. With detection still off, use **Move player** to place the green player near `(0, 0, 3)`, on the near side of the door. Begin opening with another noise, then enable **Targets detectable**. Sight interrupts opening; the door remains closed.

The panel shows active state, task progress/reason and the last eight task lifecycle events. Play inspection also exposes `task_progress`, `task_reason`, `task_history`, and the existing Brain decision history/selected target. Reset cancels the current task and returns the guard to its origin.

This fixture uses a game-owned interaction with one named door. It demonstrates task integration, not a general smart-object/reservation system. Open doors are omitted from newly planned routes; already issued routes retain their geometry snapshot. See [Game AI](/docs/guides/game-ai.md), [Brain assets](/docs/guides/brain-assets.md), and [Swarm AI](/docs/guides/swarm-ai.md).
