Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofpackage aiKit.package·originates fromworld 1d574edc-6…

brains

The built-in brains: state machines (guard, wanderer, chaser, coward, ambusher, turret, sentry, listener) and behaviour trees (ranged, sniper, healer, swarm, companion, boss), plus the helpers they share (`H.enemy`, `H.lead`, `H.handle`, `H.allies`). A preset's `brain` names one…

byclaude-code @ aura·posted 2d ago
What it does

aiKit.brains

The built-in brains: state machines (guard, wanderer, chaser, coward, ambusher, turret, sentry, listener) and behaviour trees (ranged, sniper, healer, swarm, companion, boss), plus the helpers they share (H.enemy, H.lead, H.handle, H.allies). A preset's brain names one of them; get(name) returns the definition. Exposed as AI.brainLib and AI.helpers.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

aiKit.brains — the built-in brains. Half are state machines (aiKit.fsm), half behaviour trees (aiKit.bt), so both styles have worked examples: state machines: guard, wanderer, chaser, coward, ambusher, turret, sentry, listener behaviour trees: ranged, sniper, healer, swarm, companion, boss A preset's `brain` names one of these. Every brain reads only its own memory (aiKit.memory) to decide where enemies are, and acts through aiKit.actions. The context every state and node receives is the brain record `b`: b.pos, b.facing, b.home, b.homeYaw, b.route, b.mem, b.preset, b.combat, b.machine / b.tree, b.st (scratch for the current state), b.focus (the spot it is dealing with), b.leader (companions), b.phase (boss). Your own brain: build an FSM.define(...) or BT.define(...) with any name and set a preset's `brain` to it (see the README, "Writing your own brain").

hostileSpot(b: any, s: any) → boolean

argtypedescription
bany
sany

handled(b: any, s: any) → boolean

argtypedescription
bany
sany

enemy(b: any) → any

The nearest enemy it can see right now.

argtypedescription
bany

lead(b: any) → any

The most useful lead it has not dealt with yet: a glimpse, a report from an ally, a noise, or where it last saw someone.

argtypedescription
bany

handle(b: any, s: any?) → void

argtypedescription
bany
sany?

now( ) → number

stranger(b: any, within: number) → any

Someone close enough to count as "near" (any team but its own), visible.

argtypedescription
bany
withinnumber

allies(b: any, within: number?) → void

argtypedescription
bany
withinnumber?

alarm(b: any) → void

Calm-state interruption: an enemy in sight, or a new lead.

argtypedescription
bany

lookAround(b: any, baseYaw: number, t: number, amount: number?) → void

argtypedescription
bany
baseYawnumber
tnumber
amountnumber?

patrolState( ) → any

enter(b: ?) → void

argtypedescription
b?

update(b: ?, dt: ?) → void

argtypedescription
b?
dt?

roamState(label: string?) → any

argtypedescription
labelstring?

enter(b: ?) → void

argtypedescription
b?

update(b: ?, dt: ?) → void

argtypedescription
b?
dt?

suspiciousState( ) → any

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

investigateState( ) → any

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

chaseState(onLost: string?) → any

argtypedescription
onLoststring?

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

attackState( ) → any

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

searchState( ) → any

enter(b: ?) → void

argtypedescription
b?

update(b: ?, dt: ?) → void

argtypedescription
b?
dt?

nearestRouteIndex(b: any) → number

argtypedescription
bany

returnState(calm: string) → any

argtypedescription
calmstring

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

fighter(name: string, calmName: string, calm: any, extra: { [string]: any }?, transitions: { any }?) → any

A fighter's whole loop around one calm state.

argtypedescription
namestring
calmNamestring
calmany
extra{ [string]: any }?
transitions{ any }?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

update(b: ?, dt: ?) → void

argtypedescription
b?
dt?

update(b: ?, dt: ?) → void

argtypedescription
b?
dt?

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

hurt(b: any) → boolean

coward: fights while healthy, flees and hides when hurt, comes back healed

argtypedescription
bany

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

enter(b: ?) → void

argtypedescription
b?

exit(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

enter(b: ?) → void

argtypedescription
b?

exit(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

update(b: ?, dt: ?) → void

argtypedescription
b?
dt?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

towerFSM(name: string, trackLabel: string) → void

turret and sentry: sweep → track → (shoot | shout) → hold → sweep

argtypedescription
namestring
trackLabelstring

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

enter(b: ?) → void

argtypedescription
b?

update(b: ?, _dt: ?) → void

argtypedescription
b?
_dt?

update(b: ?, dt: ?) → void

argtypedescription
b?
dt?

cond(name: string, fn: (b: any) → void

argtypedescription
namestring
fn(b: any

act(name: string, fn: (b: any, dt: number) → void

argtypedescription
namestring
fn(b: any, dt: number

seesEnemy(b: any) → boolean

argtypedescription
bany

enemyDist(b: any) → number

argtypedescription
bany

hasLead(b: any) → boolean

argtypedescription
bany

kiteBranch(label: string, getKeep: (b: any) → void

argtypedescription
labelstring
getKeep(b: any

shoot(b: any) → void

argtypedescription
bany

targetPos(b: any) → any

argtypedescription
bany

huntAtRange(getKeep: (b: any) → void

Go to where the enemy was last known, but only as far as the edge of the band it fights from; once there, watch the spot, then give it up.

argtypedescription
getKeep(b: any

presetKeep(b: any) → any return b

argtypedescription
bany

presetCombat(b: any) → any return b

argtypedescription
bany

scanAct(label: string) → any

sniper: aim with a laser while the target stays in sight, then one heavy shot

argtypedescription
labelstring

hurtAlly(b: any) → any

healer: back away from enemies, heal the most hurt ally, stay with the group

argtypedescription
bany

groupLeader(b: any) → any

swarm: mob what any of the group knows about, otherwise roam with the group

argtypedescription
bany

leaderOk(b: any) → boolean

companion: wait at post, join a player who comes close, fight for them, patch them up

argtypedescription
bany

phaseOf(b: any) → void

boss: phases by health; each phase has its own tactic

argtypedescription
bany

phaseTactic(t: string) → void

argtypedescription
tstring

meleeBranch(label: string) → any

argtypedescription
labelstring

get(name: string) → any

The definition a preset's `brain` names: { kind = "fsm"|"bt", ... }.

argtypedescription
namestring

names( ) → void

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

20+items
▣
module · born here
❒asset
# aiKit.registry What every brain on a peer shares: the brain records, the players (from the scene's players registry), teams, positions (navKit agents by their drawn body), health checks, network authority (owner, or the room host while nobody owns it), the noise bus (`noise`, sent to other peers when needed), the light level at a spot (`lightAt`, from the scene's lights with shadow rays), automatic footstep noises for the local player, the `aiKit:cmd` / `aiKit:noise` / `aiKit:fx` channels, and the timing counters behind `AI.stats()`.
▲ 0↑ born
▣
module · born here
❒asset●1problem
# aiKit.fsm A small finite state machine. `FSM.define(name, { initial, states, transitions })` checks and names a definition; `FSM.new(def, ctx)` makes a machine whose `update(dt)` runs global transitions, then the current state's `update`, which can return the next state's name. `enter`, `exit` and `label` per state; `machine.state`, `machine.time`, `machine:go(name, reason)`. The guard, wanderer, chaser, coward, ambusher, turret, sentry and listener brains are written with it; games use it through `AI.fsm` or `AI.defineBrain`.
▲ 0↑ born
▣
module · born here
❒asset
# aiKit.actions The verbs brains are written in: `moveTo`, `follow`, `stop`, `arrived` (through navKit), `faceTo`, `faceYaw`, `attack` (melee, ranged and sniper shots along real rays), `heal`, `slam`, `shout` (through SecondWind via `aiKit.compat`), and `randomPoint` / `fleePoint` on the walkable map. Used by the built-in brains and by games writing their own (`AI.actions`).
▲ 0↑ born
▣
module · born here
❒asset
# aiKit.bt A small behaviour tree: `sequence`, `selector` (optionally with memory of the running child), `parallel`, `condition`, `action`, `wait`, `invert`, `succeed`, `cooldown`, `timeout`, `repeatUntilFail`. Nodes are shared definitions; what a node remembers lives on each tree instance (`BT.new(root)`), and `tree.active` names the leaf that ran last, which the debug view shows. The ranged, sniper, healer, swarm, companion and boss brains are written with it; games use it through `AI.bt` or `AI.defineBrain`.
▲ 0↑ born
❒
package · born here
❒asset
# aiKit 1.1.0 Brains and senses for the non-player characters of any multiplayer Zero world. Put a `Brain` on a character, pick a preset, and it walks, looks, listens, remembers, fights and gives up like the character it is meant to be: a guard on its rounds, a sniper on a perch, a swarm, a boss that changes tactics. Change the preset while the game runs and it behaves differently at once. Presets are plain data, so a designer can make "a guard that hears better but gives up later" without touching code. Version **1.1.0** (`AI.VERSION`). The version in this README, `package.yaml` and `AI.VERSION` move together, and a test checks that they do. aiKit moves characters with **navKit 1.2.1** (the Pathwise world) and gives them health, damage, death and healing with **SecondWind 1.2.0** (the Second Wind world), which itself runs on **Lifeline 1.0.1** and **MatchFlow 1.1.0**. aiKit 1.1.0 is built and tested against exactly those versions. It adds the parts neither has: senses, memory, decisions and the debug view. ## Quick start **1. Install aiKit** into `/source`. One call brings everything it builds on with it: navKit 1.2.1 (movement), SecondWind 1.2.0 (health and damage), and the Lifeline 1.0.1 and MatchFlow 1.1.0 packages SecondWind uses. They land under `/source/deps/`, and `require("navKit.api")` and `require("SecondWind.api")` reach them from your own scripts too. ``` zeromind_install guid=549ff927-79bc-4db2-b5b2-35d5f3fd59cb at=/source ``` (or search ZeroMind for "aiKit" and install `aiKit.package`). If your world already has navKit or SecondWind installed, aiKit uses those; keep them at the versions above. **2. Add to `scenes/main.scene/build.luau`.** At the very top of the file: ```lua local AI = require("aiKit.api") local NK = require("navKit.api") ``` and at the end of `function content()`, just before its `end` (a new world's scene already has the ground, the light and the player): ```lua -- the ground the characters can walk on (baked when play starts) entity.spawn("NavMap").component.add(NK.MAP, { kind = "grid", size = { x = 40, y = 6, z = 40 } }) -- a wall to hide behind (wide enough to hide the spawn point from the guard) local wall = entity.spawn("Wall", { position = { 0, 1, 9 }, scale = { 16, 2, 0.5 } }) wall.component.add("Model", { model = "cube" }) wall.component.add("BoxCollider", {}) -- a guard walking a square behind it local guard = entity.spawnSynced("Guard", { position = { -6, 0, 12 } }) guard.component.add("aiKit.Brain", { preset = "guard", route = { { -6, 0, 12 }, { 6, 0, 12 }, { 6, 0, 18 }, { -6, 0, 18 } }, }) -- V (or the button in the bottom-right corner) shows what it sees, hears and remembers AI.addDebugToggle() ``` **3. Press play.** The blue guard walks its square behind the wall. Walk around the wall into its view: it stops, stares ("Suspicious?"), then runs at you ("Chase!"). Duck back behind the wall and it runs to where it **last saw** you, searches around there, gives up and goes back to its route. Press V to see its vision cone, the ring it would hear your footsteps in, and a red marker where it remembers you; press B to switch between the full view, state labels only and cones only. Walk slowly and it does not hear you at all. Change `preset = "guard"` to `"nightwatch"` and it only sees you where it is lit; to `"turret"` and it stands and sweeps; to `"chaser"` and it hunts anything it sees or hears. From code, while playing: `AI.setPreset(guard, "listener")`. ## What you get | piece | what it is | |---|---| | `aiKit.api` | the one interface: `local AI = require("aiKit.api")` | | `Brain` component | senses, memory and a brain on one character; `preset` picks which | | `AIDebug` component | the debug view's switches: on/off (V and a button) and its style (B and a button) | | `NoiseMaker` component | bottles thrown on a timer, and keys that let a player throw a bottle (G) or fire a shot (F) | | `aiKit.presets` | every character type as plain data, and the perception styles | | `aiKit.fsm` | a small state machine | | `aiKit.bt` | a small behaviour tree | | `aiKit.brains` | the built-in brains, written with those two | | `aiKit.senses`, `aiKit.memory` | sight, hearing, light and touch; the fading memory of where others were | | `aiKit.actions` | the verbs brains are written in: move, face, attack, shoot, heal, slam, shout | | `aiKit.debug` + `aiDebug` render feature | the debug view, drawn in the game view | | `aiKit.compat` | health for characters nobody owns (see Multiplayer) | | `aiKit` test suite | a test for everything a game can call | ## Presets | preset | brain | kind | team | what it does | |---|---|---|---|---| | `guard` | guard | state machine | guards | Walks its patrol route. Spots you only when it can really see you, chases, loses you when you break line of sight, searches where it last saw you, gives up and goes back. Shouts so other guards come. | | `wanderer` | wanderer | state machine | neutral | Strolls about where it was placed; stops to watch anyone who comes close. Harmless. | | `chaser` | chaser | state machine | monsters | Roams; runs at anything it sees or hears and hits it up close; gives up a few seconds after losing it. | | `ranged` | ranged | behaviour tree | bandits | Shoots and keeps its distance: when you come closer than 6 m it backs away to the middle of its 6–11 m band (round to the side when a wall is behind it), keeping its eyes on you, and fights from there; steps forward when you are farther than 11 m; strafes between shots. When it loses you it goes only as far as the edge of that band and watches the spot. | | `coward` | coward | state machine | bandits | Fights while healthy, flees and hides when hurt, heals, comes back. | | `ambusher` | ambusher | state machine | monsters | Crouches out of sight at its post, springs when prey comes close, returns to hiding afterwards. | | `sniper` | sniper | behaviour tree | bandits | Holds a perch, watches a long narrow cone, aims with a visible laser and fires one heavy shot. Break line of sight and the aim is lost. | | `healer` | healer | behaviour tree | monsters | Goes to whichever ally is most hurt and heals it with a beam; backs away from enemies. | | `swarm` | swarm | behaviour tree | swarm | Small and many: moves with its group, shares what any of them sees, mobs a target together. | | `turret` | turret | state machine | machines | Fixed gun: sweeps its arc, locks on, fires bursts, keeps aiming at the last spot for a moment. | | `companion` | companion | behaviour tree | players | Waits at its post until a player comes close, then follows that player, fights what threatens them and patches them up when they are badly hurt. | | `boss` | boss | behaviour tree | monsters | Brawler at full health, gunner that keeps its distance below 60%, berserker that charges and slams the ground below 30%. | | `sentry` | sentry | state machine | guards | **Sight cone.** Stands its post sweeping its gaze; sees only inside the cone and only what walls do not hide; tracks and shouts for help. | | `listener` | listener | state machine | monsters | **Hearing.** Blind, hears all round; noises are fainter with distance and through walls; it goes to where the noise was and swings at anything it bumps into. | | `nightwatch` | guard | state machine | guards | **Light-sensitive sight.** A guard whose eyes need light: stand in a lamp's pool and it spots you at once; keep to the dark and you slip past. | | `dummy` | wanderer | state machine | players | A harmless stand-in for a player that walks a route and makes footstep noises, so demos run with nobody playing. | `AI.find({ family = "perception" })`, `AI.find({ tag = "stealth" })`, `AI.find({ brainKind = "bt" })` filter them. ### Perception styles The three ways a character can take the world in have a list of their own, asked for the way presets are: ```lua AI.perceptionStyles() -- { "sight", "hearing", "light" } AI.perceptionStyle("light") -- { id, label, description, preset = "nightwatch", senses = { ... } } ``` | style | label | shown by preset | what it means | |---|---|---|---| | `sight` | Sight cone | `sentry` | Sees only inside a cone in front of it, up to its range, and only what walls do not hide. | | `hearing` | All-around hearing | `listener` | Blind; hears noises all round, fainter with distance and through walls, and goes to where the noise was. | | `light` | Light-sensitive sight | `nightwatch` | Sees only where there is light: spots you at once in a lamp's pool, misses you in the dark. | `senses` in the answer is that preset's own sense settings (a copy), so a style can be put on any preset: `AI.definePreset("darkGuard", { base = "guard", senses = AI.perceptionStyle("light").senses })`. #### Picking a style by its tags `AI.styles()` lists the three styles with tags, so you can pick one by what it senses with, what it suits, or how a player beats it. Each entry is `AI.perceptionStyle(name)`'s answer plus `perceives`, `suits`, `beatenBy` and `tags` (all of them in one list). Pass a tag to keep only the styles that carry it, or a list of tags to keep those that carry every one: ```lua AI.styles() -- all three, in order, each with its tags AI.styles("stealth") -- sight, hearing and light AI.styles("night-levels") -- light AI.styles("stay-quiet") -- hearing AI.styles({ "sight", "stealth" }) -- sight and light (both see; light needs a lamp) local pick = AI.styles("guard-posts")[1] AI.definePreset("postGuard", { base = "guard", senses = pick.senses }) ``` | style | perceives (senses with) | suits | beaten by | |---|---|---|---| | `sight` | `sight` | `stealth`, `guard-posts` | `hide-behind-walls` | | `hearing` | `hearing` | `horror`, `stealth` | `stay-quiet` | | `light` | `sight`, `light` | `stealth`, `night-levels` | `stay-in-the-dark` | Tags are lower case with hyphens; `"Guard posts"` and `"night_levels"` find the same tags. A tag no style carries answers an empty list. ## Making your own presets Presets are tables. `AI.definePreset` adds one, starting from a `base`; `AI.tunePreset` changes one for the whole game. Tables merge key by key, so you only write what differs. Call either on every peer (the scene's `entrypoint.luau`, or a module your components require) before the characters that use it start; a running character picks up the change at once. ```lua local AI = require("aiKit.api") -- a guard that hears better but is slower to give up AI.definePreset("eagleGuard", { base = "guard", label = "Eagle-eared guard", senses = { hearing = { sensitivity = 2 } }, -- hears every noise twice as far search = { time = 25, spots = 8 }, -- searches longer before it goes back chase = { giveUp = 5 }, }) AI.tunePreset("guard", { move = { walk = 1.6 } }) -- every guard in the game strolls slower ``` Then `Brain { preset = "eagleGuard" }`. `false` switches a sense off (`senses = { sight = false }` is a blind character). ### Preset reference | key | default | meaning | |---|---|---| | `base` | none | preset this one starts from | | `label`, `description`, `family`, `tags` | | names for menus, the debug label and `AI.find` | | `brain` | `chaser` | the state machine or behaviour tree it runs (built-in or `AI.defineBrain`) | | `team` | `monsters` | its team; `players` is the players' team | | `hostile` | `others` | who it fights: `others` (every team but its own and `neutral`), `none`, or a list of teams | | `mobile` | `true` | `false` = never moves (turrets, perched snipers); no NavAgent is added | | `move` | walker, 2.0 / 4.0 | `style` (any navKit movement style), `walk` and `run` speeds, `turnRate` | | `look` | humanoid | `shape` (humanoid, brute, bug, turret, none), `color`, `accent`, `size`, `head` (cap, helmet, hood, horns, hat, blind, target, none), `gear` (baton, rifle, longRifle, sword, staff, claws, spear, lantern, cannon, none) | | `health` | 100 HP, timed | SecondWind settings it is given when it has no health: `maxHealth`, `deathStyle`, `respawnDelay`, `regenRate`; `false` = none | | `senses.sight` | 15 m, 110° | `range`, `fov` (whole cone, degrees), `eyeHeight`, `spotTime` (seconds of clear close view to be sure), `closeRange` (felt whatever the cone), `needsLight`, `lightThreshold` (0 dark .. 1 daylight), `darkRange` | | `senses.hearing` | ×1 | `sensitivity` (multiplies every noise's reach), `threshold` (weakest noise it reacts to, 0..1), `wallDamp` (share of a noise that gets through a wall) | | `memory` | 10 s / 7 s | `forget` (a seen spot fades over this), `forgetSound`, `share` (radius it tells allies what it saw), `shareWith` (`team` or `group`) | | `combat` | melee 1.7 m | `kind` (melee, ranged, sniper, none), `range`, `damage`, `cooldown`, `type` (SecondWind damage type), `accuracy`, `noise` (how far its attacks are heard), `windup` (sniper), `volley` | | `patrol` | wait 1.5 s | `wait` at each route point (the route itself is on the Brain: a list of points or a spline) | | `wander` | 7 m | `radius`, `waitMin`, `waitMax` | | `search` | 10 s, 5 m | `time`, `radius`, `spots`, `lookTime` | | `chase` | 2 s | `giveUp`: seconds without sight before it goes to look | | `suspicion` | 2 s | how long it stares at a glimpse or a noise before going to look | | `flee` | 35% → 80% | `below`, `recoverAbove`, `distance` (coward) | | `keepDistance` | 6–11 m | `min`, `max` (ranged, healer) | | `ambush` | 6 m | `springRange`, `chaseTime` | | `heal` | 12 HP | `amount`, `range`, `cooldown`, `below` (healer) | | `follow` | 2.5 m | `distance`, `recruitRadius`, `guardRadius`, `healBelow`, `healAmount`, `healCooldown` (companion) | | `turret` | | `turnSpeed`, `sweep`, `sweepSpeed` (turret, sentry, sniper) | | `phases` | none | boss phases: `{ name, above, tactic = melee/ranged/berserk, run, combat, keepDistance, tint }` | | `footsteps` | 0 | how far its own footsteps carry, metres (dummies) | | `shout` | false | shouts when it starts a chase (guard) | | `leash` | 0 | territory radius; 0 = no limit | ## Senses Each sense writes a **spot** into the character's memory: where someone was, when, and how it knows (`sight`, `glimpse`, `told` by an ally, or `sound`). Brains only ever act on those spots, never on where a player really is: lose them and they go to where you **were**. - **Sight.** Inside `range`, inside the cone around where the body faces, and with a clear ray from its eyes to your chest or head (any collider in between hides you). Awareness fills while it can see you, faster up close and in good light, and drains while it cannot: a glimpse makes it suspicious, a full bar is a sighting. - **Light-sensitive sight** (`needsLight`). It reads the light level where you stand from the scene's own lights (sun unless shadowed, ambient, point and spot lights unless blocked) and cannot see you below `lightThreshold`, except within `darkRange`. `AI.lightAt(pos)` tells you what it sees. - **Hearing.** Every noise reaches `radius × sensitivity` metres and fades linearly with distance; a wall in between lets `wallDamp` of it through. Noises from its own team are ignored unless they are alarming (shots, shouts, explosions). It hears where the noise **was**: a thrown bottle draws it to the bottle. - **Touch.** Anything within about a metre is noticed, eyes or not. - **Memory.** Spots fade over `forget` / `forgetSound` seconds. With `memory.share`, a character tells nearby allies what it saw. - **Territory.** With `leash` (on the preset or the Brain), it ignores everything further than that from its territory centre. ### Noises Players' footsteps are heard without any setup: walking is heard within 5 m, running within 11 m, moving slowly is silent (`AI.config.quietSpeed`, `walkNoise`, `runNoise`, `runSpeed`). Make any other noise from any peer with `AI.noise(position, loudness, madeBy, kind)`: - `position`: where it happened; - `loudness`: how far it carries in metres, or the name of one of the noises below for its loudness; - `madeBy`: who made it (an entity, id or name). Leave it out when the noise gives nobody away: a thrown bottle draws listeners to the bottle, not to the thrower; - `kind` (optional): what it was, shown in the debug view and in the `heard` event. It defaults to the noise's name, or `"noise"`. ```lua AI.noise(position, "gunshot", shooter) -- the shooter gives itself away AI.noise(bottlePos, "bottle") -- listeners go to the bottle AI.noise(position, 6, nil, "creak") -- your own noise, heard within 6 m ``` | kind | reach | alarms its own team | |---|---|---| | `footstep` | 5 m | no | | `run` | 11 m | no | | `impact` | 10 m | yes | | `bottle` | 16 m | yes | | `shout` | 22 m | yes | | `gunshot` | 35 m | yes | | `alarm` | 40 m | yes | | `explosion` | 50 m | yes | The `NoiseMaker` component throws bottles on a timer (`interval`, `scatter`), and with `playerKeys = true` lets the local player throw a bottle where they look (G) or fire a shot (F) that hurts the character it hits. ## State machines and behaviour trees Half the built-in brains are state machines, half behaviour trees, so both styles have worked examples in `aiKit.brains`. Write your own with the same pieces and give it to a preset: ```lua local AI = require("aiKit.api") local A, H = AI.actions, AI.helpers -- a state machine: a lookout that runs to the bell when it sees someone AI.defineBrain("bellRinger", { initial = "watch", states = { watch = { label = "Watch", update = function(b, dt) if H.enemy(b) then return "ring", "saw someone" end A.faceYaw(b, b.homeYaw + math.sin(b.machine.time) * 0.8) end, }, ring = { label = "Ring the bell!", enter = function(b) A.shout(b, 40) end, update = function(b, dt) if b.machine.time > 3 then return "watch" end end, }, }, }) -- a behaviour tree: attack what you see, else wander local BT = AI.bt AI.defineBrain("simpleHunter", BT.selector("hunter", { BT.sequence("fight", { BT.condition("sees an enemy", function(b) b.target = H.enemy(b); return b.target ~= nil end), BT.action("Attack", function(b, dt) if A.inReach(b, b.target) then A.attack(b, b.target) else A.moveTo(b, b.target.pos, "run") end return "running" end), }), BT.action("Wander", function(b, dt) if A.arrived(b) then A.moveTo(b, A.randomPoint(b, b.home, 6)) end return "running" end), })) AI.definePreset("bellRinger", { base = "sentry", brain = "bellRinger" }) ``` Every state and node receives the brain record `b`: `b.pos`, `b.facing`, `b.home`, `b.homeYaw`, `b.route`, `b.mem` (its memory), `b.preset`, `b.combat`, `b.machine` / `b.tree`, `b.st` (scratch), `b.focus`. Helpers: `H.enemy(b)` (nearest enemy it sees now), `H.lead(b)` (best lead it has not dealt with), `H.handle(b, spot)`, `H.allies(b, radius)`. Actions: `A.moveTo(b, pos, "walk"|"run")`, `A.follow(b, id, distance)`, `A.stop(b)`, `A.arrived(b)`, `A.faceTo(b, pos)`, `A.faceYaw(b, yaw)`, `A.attack(b, spot)`, `A.inReach(b, spot)`, `A.heal(b, allyId, amount)`, `A.slam(b, radius, damage)`, `A.shout(b, radius)`, `A.randomPoint(b, center, radius)`, `A.fleePoint(b, from, distance)`. `aiKit.fsm`: `FSM.define(name, { initial, states = { name = { enter, update, exit, label } }, transitions = { { from, to, when, reason } } })`, `FSM.new(def, ctx)`, `machine:update(dt)`, `machine:go(state, reason)`, `machine.state`, `machine.time`, `machine:label()`. A state's `update` returns the next state's name to switch. `aiKit.bt`: `sequence`, `selector` (both with `{ memory = true }` to resume a running child), `parallel`, `condition`, `action` (returns `"success"`, `"failure"` or `"running"`), `wait`, `invert`, `succeed`, `cooldown`, `timeout`, `repeatUntilFail`; `BT.new(root)`, `tree:tick(ctx, dt)`, `tree.active` (the leaf that ran). Node definitions are shared; what a node remembers lives on each tree instance. ## The debug view `AI.addDebugToggle()` (or an `AIDebug` component) adds two switches in the bottom-right corner of the game view: **on/off** (key V, or the "AI debug view" button) and **style** (key B, or the "Style" button). From code: `AI.debug(true)` / `AI.debug(false)` switches it, `AI.debugStyle(name)` picks a style. It has three styles: | style | draws | |---|---| | `full` (default) | everything below | | `minimal` | only the state labels over each character's head | | `cones` | only the vision cones, filled on the ground | ```lua AI.debugStyle("cones") -- pick a style (the view stays on or off as it was) AI.debug("minimal") -- switch the view on in a style AI.debugStyles() -- { "full", "minimal", "cones" }, the order the B key cycles ``` What the full view shows, for every character within 70 m of your camera (so a whole level is readable from where you stand): - its **state** over its head (and its preset), coloured by how alert it is, on a dark plate; labels grow with distance so a character 40-odd metres away is still readable; - its **vision cone**, filled on the ground: green calm, yellow suspicious, red sure, violet when its eyes need light; - a blue ring: how far it would hear a walking footstep; - blue lines to the **noises it heard**, fading; - the **spots it remembers**: red seen, yellow glimpsed, orange told by an ally, blue heard, fading as it forgets, with a line from its head; - its patrol route, and every noise on the bus as a ring as far as it carries. Each player switches their own view and style. Other players' characters are drawn too: the peer running them sends what they remember only while somebody has the view on. ## Events Each Brain fires these on the peer that runs it. Subscribe with `brain.events.<name>:connect(fn)`: | event | when | payload | |---|---|---| | `spotted` | it starts seeing someone it treats as an enemy | `who` (entity id), `x, y, z` (where they are) | | `lost` | it stops seeing them | `who`, `x, y, z` (where it last saw them) | | `heard` | a noise reaches it | `kind`, `who` (who made it, or `""`), `x, y, z` (where the noise was), `strength` (how loud it was there, 0..1) | | `died` | its health runs out | `by` (who dealt the last hit, or `""`) | | `stateChanged` | its state changes | `state`, `from`, `reason` | | `acted` | it does something every screen draws | `kind` (melee, shot, snipe, heal, slam, shout), `fx, fy, fz` from, `tx, ty, tz` to | ```lua local guard = AI.brain(entity.find("Guard")) guard.events.spotted:connect(function(e) print("guard spotted", e.who) end) guard.events.lost:connect(function(e) print("lost them at", e.x, e.z) end) guard.events.heard:connect(function(e) print("heard a", e.kind) end) guard.events.died:connect(function(e) print("guard down, by", e.by) end) ``` ## Patrol routes and splines A Brain's `route` is a list of points, or a spline from the engine's spline library (`@builtin::modules.spline`). A spline becomes evenly spaced patrol points, 1.5 m apart by default, that the character walks through without stopping; it pauses (the preset's `patrol.wait`) only at the points you placed on the spline, or nowhere with `pause = false`. A closed spline loops like any route. A list of points pauses at every point, as before. ```lua local spline = require("@builtin::modules.spline") local path = spline.new({ { x = -6, y = 0, z = 12 }, { x = 6, y = 0, z = 12 }, { x = 6, y = 0, z = 18 }, { x = -6, y = 0, z = 18 } }, { mode = "catmullRom", closed = true }) guard.component.add("aiKit.Brain", { preset = "guard", route = path }) -- on the Brain: pauses at the 4 placed points AI.setRoute(guard, path) -- or while it runs AI.setRoute(guard, { spline = path, pause = false }) -- one smooth loop, never stopping AI.setRoute(guard, { spline = path, spacing = 3 }) -- points 3 m apart ``` ## Health and damage Characters get SecondWind health from their preset when they have none (`addHealth = false` on the Brain to give them your own with `HK.attach`). Every hit goes through SecondWind, so damage types, teams, friendly fire, death styles, respawns, the kill feed and hit effects follow the game's SecondWind rules. Players need SecondWind health of their own to be hurt (`HK.attach(body, { team = "players" })` in the player setup). ## Multiplayer - **The host thinks.** The peer that owns a character's synced entity (or the room host while nobody owns it, the same rule navKit uses) senses, remembers and decides for it, and moves it through navKit. Put characters on synced entities (`entity.spawnSynced`). - **Home is where it stands when a peer starts running it.** A character's home (where it returns, wanders round and respawns) is taken from where it really stands the first time a peer runs it, after that peer has the room's positions. A player who joins and takes over characters keeps them where they are; they do not walk off to the middle of the world. - **Every screen sees the same thing.** The preset, state, alertness, boss phase, pose, facing of characters that do not walk, and a sniper's laser are synced fields; every peer builds the same body from the preset. Shots, hits, heals and slams reach other screens as one small `aiKit:fx` message each (sent only when someone else is in the room). Positions travel in navKit's crowd line. - **Any player can be the target.** Players come from the scene's normal player setup (`layers.active.players`); every player's body is a candidate for every character's senses, and each player's own footsteps are reported by that player's peer. - **Commands from anyone.** `AI.setPreset`, `setRoute`, `setTeam`, `setActive`, `setLeader`, `forget` and `alert` work from any peer: they are sent to the peer running the character (`aiKit:cmd`). `AI.noise` from any peer reaches the brains. - **Debug data only on demand.** What a character remembers is sent (`debugData`, a few hundred bytes, three times a second) only while some player has the debug view on. - **Hits and heals reach the right peer.** A hit or heal aiKit makes (a character's attack, a player's `NoiseMaker` shot, a healer's beam) on something this peer may not change goes to the peer that may (its owner, or the room host while nobody owns it) on `aiKit:hit`, and is applied there through SecondWind's own damage pipeline, capped by SecondWind's `maxRemoteDamage` rule. aiKit uses its own channel for this because it re-subscribes every play session (below). - **Characters nobody owns.** SecondWind (through Lifeline) only lets an entity's owner change its health. The relay can leave synced entities with no owner; in a brand-new editor world that is every character. Then `aiKit.compat` applies aiKit's hits and heals on the room host through SecondWind's own pipeline, and brings its characters back after death. For the length of each such call, Lifeline counts that one entity as the host's (`Compat.asOwner`); nothing else about Lifeline's rule changes. An owned entity is only ever changed by its owner. A SecondWind `Trap`, a Lifeline damage zone or a game's own `HK.damage` call goes through SecondWind's ownership check and does not reach an unowned character; from a game's own code, use `require("aiKit.compat").damage(target, amount, opts)` and `.heal(target, amount, opts)`, which take SecondWind's arguments. - **One peer per character.** Every peer that runs characters says which ones every 2 s (`aiKit:runs`, only while someone else is in the room). A peer that is only standing in for a character nobody owns steps back when another peer runs it as its owner, or stands in for it with a lower peer id. So a peer whose view of the room is wrong does not end up driving characters another peer also drives. - **Messages survive play sessions.** aiKit subscribes its channels (`aiKit:cmd`, `aiKit:noise`, `aiKit:fx`, `aiKit:hit`, `aiKit:debug`, `aiKit:runs`, `aiKit:throw`) again whenever a peer's mode or rooms change, and a reload of the module silences the older subscriptions, so no message is lost or handled twice. ## API reference (`local AI = require("aiKit.api")`) | call | what it does | |---|---| | `AI.addBrain(e, preset?, opts?) -> Brain` | Add (or update) a Brain; `opts` sets any Brain field. | | `AI.brain(e) -> Brain?` | The Brain of an entity, name, id or component. | | `AI.brains() -> { {id, name, preset, state, team, authority, alive} }` | Every Brain on this peer. | | `AI.info(e) -> table` | `preset, state, alert, phase, team, authority, alive, memory, heard, position, facing, home, brain, brainKind`. | | `AI.isAuthority(e) -> boolean` | Whether this peer runs that character. | | `AI.setPreset(e, preset)` | Switch preset while it runs (any peer). | | `AI.setRoute(e, route)` | New patrol route: a list of points, a spline, or `{ spline = path, spacing = m, pause = false }`. | | `AI.setTeam(e, team)`, `AI.teamOf(e)` | Put a character or a player on a team. | | `AI.setActive(e, on)` | Switch a brain off and on. | | `AI.setLeader(e, player)` | Companions: follow this player. | | `AI.forget(e)` | Forget everything it saw and heard. | | `AI.alert(e, pos)` | Tell it someone was seen at a spot. | | `AI.presets()`, `AI.preset(name)` | Preset names; one preset's full settings (a copy). | | `AI.perceptionStyles()`, `AI.perceptionStyle(name)` | Perception style names (`sight`, `hearing`, `light`); one style's `label`, `description`, `preset` and `senses` (a copy). | | `AI.styles(tag?)` | The perception styles with their tags (`perceives`, `suits`, `beatenBy`, `tags`); a tag or a list of tags keeps only the styles carrying them. | | `AI.definePreset(name, def)`, `AI.tunePreset(name, changes)` | Add a preset; change one. | | `AI.find(query?)`, `AI.catalog()` | Presets with labels, filtered by `family`, `tag`, `brainKind`, `brain`, `id`; everything labelled. | | `AI.defineBrain(name, def)` | Register a state machine table or behaviour tree root as a brain. | | `AI.noise(pos, loudness, madeBy?, kind?)`, `AI.recentNoises()` | Make a noise (loudness in metres or a noise name; `madeBy` = who gives themselves away); the noises still on the bus. | | `AI.lightAt(pos, exclude?) -> number` | Light level at a spot, 0 dark .. ~1 daylight. | | `AI.canSee(e, target) -> boolean` | Could it see that entity right now (cone, range, walls, light)? | | `AI.debug(on)`, `AI.debugOn()` | The debug view on this screen: `true`, `false`, or a style name to switch it on in that style. | | `AI.debugStyle(name?) -> string`, `AI.debugStyles()` | Pick the view's style (`full`, `minimal`, `cones`) and answer the current one; the style names. | | `AI.addDebugToggle(opts?)` | Add the on-screen switches and keys (an `AIDebug` component). | | `AI.stats(keep?) -> table` | Brains, sense ticks, raycasts and script milliseconds by section since the last call. | Also: `AI.VERSION`, `AI.BRAIN` / `AI.DEBUG` (component names), `AI.NOISES`, `AI.config` (footstep speeds and reaches, light scales, `senseRate`, `thinkRate`, `noiseLife`), and the modules `AI.fsm`, `AI.bt`, `AI.memory`, `AI.senses`, `AI.actions`, `AI.helpers`, `AI.brainLib`. ### `Brain` component Settings: `preset`, `team` (empty = the preset's), `group` (swarms that move and share together), `route` (a list of points or a spline), `leader` (companions), `active`, `addHealth`, `leash` and `leashCenter` (territory). Read-only, synced: `state`, `alert` (0 calm .. 1 sure), `phase`, `facing`, `pose`, `aiming`, `aimPoint`, `debugData`. Methods: `brain:info()`, `brain:record()`. Events (on the peer that runs it; see Events): `spotted`, `lost`, `heard`, `died`, `stateChanged`, `acted`. ### `AIDebug` component `key` (`KeyV`, on/off), `styleKey` (`KeyB`, next style), `button` (true: the two on-screen buttons), `style` (`full`, `minimal` or `cones`; set it to pick one, read it for the current one), `startOn`, `hint` (a line of help text above the buttons, wrapped to fit), `on` (read-only); `:toggle()`, `:setStyle(name)`. ### `NoiseMaker` component `interval` (0 = off), `scatter`, `kind`, `radius`, `playerKeys`, `throwKey` (`KeyG`), `shootKey` (`KeyF`), `shotDamage`; `:throwAt(pos)`. ## Performance Measured on the native desktop engine in this world's showcase, with 30 characters running at once (every preset, the showcase minus its six swarm members). Per frame: | | per frame, 30 characters | |---|---| | aiKit, debug view off | about 2.0 ms of its own script time (`AI.stats()`); about 3.1 ms counting every engine call its Brains make (the profiler) | | aiKit, debug view on (`full`) | about 2.9 ms (`AI.stats()`); `minimal` and `cones` cost about the same | | navKit, moving the 27 that walk | about 3.8 ms | | SecondWind + Lifeline, health for all 30 | about 3.6 ms | So 30 characters with their movement and health come to roughly 10–11 ms a frame on that machine, about 0.35 ms per character all in. Each character senses 10 times a second (`AI.config.senseRate`) and decides 15 times a second (`thinkRate`), staggered so they do not all think on the same frame; navKit keeps the bodies moving smoothly in between. Lower `senseRate` and `thinkRate` to trade reaction time for frame time. The debug view rebuilds its lines 8 times a second for characters within 70 m of your camera. Its state labels are 3D text on a dark plate; aiKit rewrites a label at most twice a second and only when its text changes. The costliest single things are SecondWind's hit pipeline (about 1 ms per hit) and navKit planning; see their READMEs. ## Tests The `aiKit` test suite (72 tests) covers every public call: - constants and versions, and this README's one Known limits and one Version history section; - presets: the built-ins, `definePreset`, `tunePreset`, `find`, `catalog`; - the perception styles: `perceptionStyles`, `perceptionStyle`, and `styles` with its tags and tag filter; - the state machine, the behaviour tree and memory; - sight through and around walls, cones, hearing and wall damping, light-sensitive sight; - every Brain command; patrol routes from point lists and splines, and a smooth spline walked without stopping; - the noise call; - the debug view, its styles and its on-screen switch; - the `spotted`, `lost`, `heard` and `died` events; - the guard's whole loop, and the ranged attacker keeping its distance; - a character's home taken where it stands when a peer starts running it; - `NoiseMaker`, custom brains, damage and healing through SecondWind; - the multiplayer wiring; - this README's first example, run in a new world's scene. Run it with: ```lua tools.use("tests", "run", { suite = "aiKit" }) ``` It runs the same way in any world that installed aiKit. ## Known limits Open engine issues aiKit is affected by: - **No engine debug drawing in play cameras** ([OrigoZero/zero#19540](https://github.com/OrigoZero/zero/issues/19540)). aiKit draws its debug view with its own render feature (`aiKit.aiDebug`), as navKit does; a headless engine draws nothing. - **Tops of 3D text clipped** ([OrigoZero/zero#19525](https://github.com/OrigoZero/zero/issues/19525)). The debug view's state labels and the showcase's signs lose the top edge of tall letters. - **Late registration in a browser tab** ([OrigoZero/zero#19309](https://github.com/OrigoZero/zero/issues/19309)). Components of installed kits can register after a scene's first frame in a browser; a character spawned before its Brain registers starts thinking a moment later. Limits of aiKit's own design: - **Characters have no colliders.** navKit moves them along its map, so walls and other characters block them but physics does not; a shot hits a character when its line passes close enough to the body. Players walk through characters. - **Light reading is an estimate.** It sums the scene's lights with shadow rays; it does not read emissive surfaces, baked lighting or light colour. - **Territory is a circle.** `leash` is a radius around a centre, not an area shape. - **Hearing does not path around corners.** A wall between halves a noise; the noise does not travel around the wall. - **Unowned characters and SecondWind's own calls.** aiKit's hits and heals reach characters nobody owns (see Multiplayer); SecondWind traps, Lifeline zones and direct `HK.damage` calls do not. ## Version history - **1.1.0**: First published release. Adds `AI.styles(tag?)`: the three perception styles with tags for what each senses with (sight, hearing, light), what it suits (stealth, horror, guard posts, night levels) and how a player beats it (hide behind walls, stay quiet, stay in the dark), filtered by one tag or a list. `AI.perceptionStyles()` and `AI.perceptionStyle(name)` answer exactly as in 1.0.0. Looking a character up by its name no longer writes an engine error. The test suite (72 tests) now runs in any world that installs aiKit. Built against navKit 1.2.1 and SecondWind 1.2.0, as 1.0.0 was. - **1.0.0**: Pushed, never released. 16 presets (12 behaviours, 3 perception styles, a training dummy), with the perception styles also listed on their own (`AI.perceptionStyles`); state machine and behaviour tree brains; sight, hearing, light and touch senses; fading memory shared between allies; territory; patrol routes from points or splines, smooth splines walked without stopping; a debug view in the game view with three styles (`full`, `minimal`, `cones`) and on-screen switches, readable across a whole level; `spotted`, `lost`, `heard`, `died`, `stateChanged` and `acted` events; `AI.noise(position, loudness, madeBy, kind)`; NoiseMaker; health through SecondWind 1.2.0 and movement through navKit 1.2.1; multiplayer (hits over `aiKit:hit`, one peer per character via `aiKit:runs`, subscriptions renewed every session, home taken where a character stands when a peer starts running it); a 68-test suite.
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.