Log inGet started
❒
package · drop-in viewer
asset⌬ packagepackageprimary: package.yaml·originates fromworld 1d574edc-6…●1problem

aiKit

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 bo…

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

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:

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):

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

piecewhat it is
aiKit.apithe one interface: local AI = require("aiKit.api")
Brain componentsenses, memory and a brain on one character; preset picks which
AIDebug componentthe debug view's switches: on/off (V and a button) and its style (B and a button)
NoiseMaker componentbottles thrown on a timer, and keys that let a player throw a bottle (G) or fire a shot (F)
aiKit.presetsevery character type as plain data, and the perception styles
aiKit.fsma small state machine
aiKit.bta small behaviour tree
aiKit.brainsthe built-in brains, written with those two
aiKit.senses, aiKit.memorysight, hearing, light and touch; the fading memory of where others were
aiKit.actionsthe verbs brains are written in: move, face, attack, shoot, heal, slam, shout
aiKit.debug + aiDebug render featurethe debug view, drawn in the game view
aiKit.compathealth for characters nobody owns (see Multiplayer)
aiKit test suitea test for everything a game can call

Presets

presetbrainkindteamwhat it does
guardguardstate machineguardsWalks 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.
wandererwandererstate machineneutralStrolls about where it was placed; stops to watch anyone who comes close. Harmless.
chaserchaserstate machinemonstersRoams; runs at anything it sees or hears and hits it up close; gives up a few seconds after losing it.
rangedrangedbehaviour treebanditsShoots 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.
cowardcowardstate machinebanditsFights while healthy, flees and hides when hurt, heals, comes back.
ambusherambusherstate machinemonstersCrouches out of sight at its post, springs when prey comes close, returns to hiding afterwards.
snipersniperbehaviour treebanditsHolds 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.
healerhealerbehaviour treemonstersGoes to whichever ally is most hurt and heals it with a beam; backs away from enemies.
swarmswarmbehaviour treeswarmSmall and many: moves with its group, shares what any of them sees, mobs a target together.
turretturretstate machinemachinesFixed gun: sweeps its arc, locks on, fires bursts, keeps aiming at the last spot for a moment.
companioncompanionbehaviour treeplayersWaits 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.
bossbossbehaviour treemonstersBrawler at full health, gunner that keeps its distance below 60%, berserker that charges and slams the ground below 30%.
sentrysentrystate machineguardsSight cone. Stands its post sweeping its gaze; sees only inside the cone and only what walls do not hide; tracks and shouts for help.
listenerlistenerstate machinemonstersHearing. 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.
nightwatchguardstate machineguardsLight-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.
dummywandererstate machineplayersA 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:

AI.perceptionStyles()          -- { "sight", "hearing", "light" }
AI.perceptionStyle("light")    -- { id, label, description, preset = "nightwatch", senses = { ... } }
stylelabelshown by presetwhat it means
sightSight conesentrySees only inside a cone in front of it, up to its range, and only what walls do not hide.
hearingAll-around hearinglistenerBlind; hears noises all round, fainter with distance and through walls, and goes to where the noise was.
lightLight-sensitive sightnightwatchSees 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:

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 })
styleperceives (senses with)suitsbeaten by
sightsightstealth, guard-postshide-behind-walls
hearinghearinghorror, stealthstay-quiet
lightsight, lightstealth, night-levelsstay-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.

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

keydefaultmeaning
basenonepreset this one starts from
label, description, family, tagsnames for menus, the debug label and AI.find
brainchaserthe state machine or behaviour tree it runs (built-in or AI.defineBrain)
teammonstersits team; players is the players' team
hostileotherswho it fights: others (every team but its own and neutral), none, or a list of teams
mobiletruefalse = never moves (turrets, perched snipers); no NavAgent is added
movewalker, 2.0 / 4.0style (any navKit movement style), walk and run speeds, turnRate
lookhumanoidshape (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)
health100 HP, timedSecondWind settings it is given when it has no health: maxHealth, deathStyle, respawnDelay, regenRate; false = none
senses.sight15 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×1sensitivity (multiplies every noise's reach), threshold (weakest noise it reacts to, 0..1), wallDamp (share of a noise that gets through a wall)
memory10 s / 7 sforget (a seen spot fades over this), forgetSound, share (radius it tells allies what it saw), shareWith (team or group)
combatmelee 1.7 mkind (melee, ranged, sniper, none), range, damage, cooldown, type (SecondWind damage type), accuracy, noise (how far its attacks are heard), windup (sniper), volley
patrolwait 1.5 swait at each route point (the route itself is on the Brain: a list of points or a spline)
wander7 mradius, waitMin, waitMax
search10 s, 5 mtime, radius, spots, lookTime
chase2 sgiveUp: seconds without sight before it goes to look
suspicion2 show long it stares at a glimpse or a noise before going to look
flee35% → 80%below, recoverAbove, distance (coward)
keepDistance6–11 mmin, max (ranged, healer)
ambush6 mspringRange, chaseTime
heal12 HPamount, range, cooldown, below (healer)
follow2.5 mdistance, recruitRadius, guardRadius, healBelow, healAmount, healCooldown (companion)
turretturnSpeed, sweep, sweepSpeed (turret, sentry, sniper)
phasesnoneboss phases: { name, above, tactic = melee/ranged/berserk, run, combat, keepDistance, tint }
footsteps0how far its own footsteps carry, metres (dummies)
shoutfalseshouts when it starts a chase (guard)
leash0territory 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".
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
kindreachalarms its own team
footstep5 mno
run11 mno
impact10 myes
bottle16 myes
shout22 myes
gunshot35 myes
alarm40 myes
explosion50 myes

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:

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:

styledraws
full (default)everything below
minimalonly the state labels over each character's head
conesonly the vision cones, filled on the ground
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):

eventwhenpayload
spottedit starts seeing someone it treats as an enemywho (entity id), x, y, z (where they are)
lostit stops seeing themwho, x, y, z (where it last saw them)
hearda noise reaches itkind, who (who made it, or ""), x, y, z (where the noise was), strength (how loud it was there, 0..1)
diedits health runs outby (who dealt the last hit, or "")
stateChangedits state changesstate, from, reason
actedit does something every screen drawskind (melee, shot, snipe, heal, slam, shout), fx, fy, fz from, tx, ty, tz to
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.

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"))

callwhat it does
AI.addBrain(e, preset?, opts?) -> BrainAdd (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) -> tablepreset, state, alert, phase, team, authority, alive, memory, heard, position, facing, home, brain, brainKind.
AI.isAuthority(e) -> booleanWhether 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?) -> numberLight level at a spot, 0 dark .. ~1 daylight.
AI.canSee(e, target) -> booleanCould 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?) -> tableBrains, 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 offabout 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 walkabout 3.8 ms
SecondWind + Lifeline, health for all 30about 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:

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

Interface

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

This asset doesn't declare a schema or structured payload.

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.

78+items
◇
component · born here
❒asset
# aiKit.AIDebug The debug view's toggle: a key (V by default) and, with `button`, an on-screen button in the bottom-right corner that works with a mouse or a tap. Put it on any plain entity in the scene (`AI.addDebugToggle()` does it); each player switches their own view. `hint` adds a line of help text above the button; `:toggle()` switches it from code.
▲ 0↑ born
◇
component · born here
❒asset
# aiKit.Brain Gives a character a brain and senses: `entity.component.add("aiKit.Brain", { preset = "guard" })` on a synced entity. On the peer that runs it, it adds a navKit `NavAgent` (unless the preset does not move) and SecondWind health (unless it has some, or `addHealth` is off), senses ten times a second, remembers, and runs the preset's state machine or behaviour tree. Every peer builds the same body from the synced preset. Fields: `preset`, `team`, `group`, `route`, `leader`, `active`, `addHealth`, `leash`, `leashCenter`; read-only `state`, `alert`, `phase`, `facing`, `pose`, `aiming`, `aimPoint`, `debugData`. Changing `preset` while running changes its behaviour at once.
▲ 0↑ born
◇
component · born here
❒asset
# aiKit.NoiseMaker Noises for brains to hear without code. With `interval` it lobs a bottle every few seconds to a random spot within `scatter` metres; the smash is a noise that does not give away the thrower. With `playerKeys` the local player throws a bottle where they look (G) or fires a shot (F) that every brain in earshot hears and that hurts the character it hits through SecondWind. Put it on any entity; `:throwAt(pos)` throws one from code.
▲ 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
·
renderfeature · born here
❒asset
# aiKit.aiDebug Draws aiKit's debug view every frame from the buffers `aiKit.debug` uploaded, in the `afterLighting` phase with the engine's `debugGeometry` vertex-colour shader: a depth-tested triangle list for the filled vision cones, a depth-tested line list for cone edges, hearing rings and noise rings, and an always-on-top line list for remembered spots and heard-noise lines. Enqueues nothing while the view is off.
▲ 0↑ born
·
testsuite · born here
❒asset
# aiKit test suite Automated tests for the aiKit package: every public call of `aiKit.api`, the `fsm`, `bt` and `memory` modules, the Brain, AIDebug and NoiseMaker components, and combat and healing through SecondWind. Run it alone with `tools.use("tests", "run", { suite = "aiKit" })` (edit mode; the tests that need brains to think enter play themselves). ## What it covers - **Constants**: `AI.VERSION` is 1.1.0 and matches `package.yaml` and, when it exists, the first heading of the package `README.md` (skipped with a log line while there is no README); `AI.BRAIN`, `AI.DEBUG` and `aiKit.NoiseMaker` resolve as components; the re-exported modules, `AI.vec` and `AI.NOISES`. - **Presets**: the 16 built-ins in order; each names a registered brain of kind fsm or bt, both kinds used; `AI.preset` returns a merged copy; `AI.definePreset` with a base inherits everything and overrides deep keys (the eagle-eared guard differs from the guard only in hearing sensitivity and search time), unknown bases error; `AI.tunePreset` changes a preset and presets based on it, and is undone; `AI.find` by family, tag, brainKind, brain, id, lists of values, and its error on unknown keys; `AI.catalog`. - **Perception styles**: `AI.perceptionStyles` / `AI.perceptionStyle` (names, fields, copies, the error for an unknown style); `AI.styles` with each style's tags (what it perceives, what it suits, what beats it) and its answer matching `perceptionStyle`; `AI.styles(tag)` and a list of tags as filters, loose spelling, unknown tags and bad filters. - **README**: exactly one "Known limits" and one "Version history" section, limits cited by GitHub issue only, and the history naming this version. - **State machine** (`AI.fsm`): define/get/names, new, update, enter → update → exit → enter order with from/reason, `time` resetting only on a switch, transitions (`from = "*"` and named) checked before the state's update, unknown states as errors, `label()`. - **Behaviour tree** (`AI.bt`): sequence and selector with and without memory, parallel, condition, action (success / failure / running / nil), wait, invert, succeed, cooldown, timeout, repeatUntilFail, `tree.active`, `ticks`, `last`, `reset`, define/get/names, errors for bad nodes. - **Memory** (`AI.memory`): saw / glimpsed / heard / told, fading over `forget` and `forgetSound`, `update` dropping faded spots and clearing `visible`, `best()` ranking (visible, then sight, report, sound), a fresh sighting not overwritten by weaker evidence, `visible()`, `drop`, `forgetAll`, `count`, `list` order. - **Edit mode**: `AI.addBrain` (fields, update in place, errors), `AI.brain`, `AI.info`, `AI.isAuthority`; the edit-mode command fallback (see Known bugs); `AI.debug` / `AI.debugOn`; `AI.addDebugToggle` and `AIDebug:toggle`; `AI.lightAt` beside a lamp and inside a dark roofed box. - **Senses** (play): a sentry sees a dummy in front of it (a 'sight' spot, `AI.canSee` agrees) but not through a wall or outside its cone; a listener hears a noise in reach as a 'sound' spot at the noise (not at its maker), keyed by the maker or by `noise:<seq>` when unattributed, and not one beyond reach; a noise through a wall is weaker by `wallDamp`; a nightwatch spots a lit dummy and misses one in the dark beyond `darkRange`. - **Brains** (play): a guard walks its route, chases and attacks a dummy that comes into view, searches when it vanishes behind a wall and goes back to patrolling; `AI.setPreset` guard → turret while running (Sweep, nothing else changed); `AI.setActive`; `AI.setRoute`; `AI.setTeam` / `AI.teamOf` (brains and plain entities); `AI.alert`; `AI.forget`; `AI.info`; `AI.brains`; `AI.isAuthority`; `AI.stats` and its sections; `AI.setLeader` with a real player avatar (the canonical player scene) and the companion following; `NoiseMaker:throwAt` putting a bottle noise on `AI.recentNoises`; `AI.defineBrain` with an FSM table and a BT root, each run by a preset. - **Combat** (play): `Compat.damage` / `Compat.heal` on SecondWind health; a melee chaser hurting a training dummy; a healer healing a hurt ally. ## How it works Play-mode tests flip this caller's own session into play, build temporary floors and walls at x/z 400..460 (the companion test uses the canonical player scene around the origin), bake a temporary navKit map when something has to walk, and spawn synced characters with `AI.addBrain`. Targets are a training dummy that never moves (`aitest_target`, based on `dummy`) so a test can place it. Every entity, map, test preset and tuned value is removed in cleanup, and the runner discards the play session after each test. Test presets and brains are prefixed `aitest_`; brains registered with `AI.defineBrain` stay registered until the module reloads (aiKit has no call to remove one). ## What it leaves alone The look of the bodies, the debug view's drawing, the effects, the multiplayer relay between peers (one peer is all a test has) and the other presets' full behaviours (ranged kiting, sniper, swarm, coward, ambusher, boss phases) beyond resolving to a working brain. ## Known bugs it states - `BUG: commands in edit mode write the fields`: `AI.setPreset`, `setTeam`, `setActive` and `setRoute` return true in edit mode but change nothing (see the comment above the test).
▲ 0↑ born
▣
module · born here
❒asset
# aiKit.api The one interface to aiKit: `local AI = require("aiKit.api")`. Games call it to give characters brains (`AI.addBrain`), switch presets while running (`AI.setPreset`, from any peer), add or tweak presets (`AI.definePreset`, `AI.tunePreset`), make noises (`AI.noise`), read what a character is doing and remembers (`AI.info`), register brains of their own (`AI.defineBrain`) and switch the debug view (`AI.debug`). It also hands out the building blocks (`AI.fsm`, `AI.bt`, `AI.memory`, `AI.actions`, `AI.helpers`). See the package README for every call.
▲ 0↑ born
▣
module · born here
❒asset
# 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`.
▲ 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
▣
module · born here
❒asset
# aiKit.compat Health for characters nobody owns. SecondWind changes health only on an entity's owner; in sessions where the relay leaves scene entities unowned nobody would. `damage` and `heal` apply aiKit's hits and heals directly through SecondWind's own pipeline when the target is unowned and this peer is the room host, and otherwise call SecondWind as usual; `respawn` brings a dead unowned character back. Used by `aiKit.actions`, the Brain and `NoiseMaker`.
▲ 0↑ born
▣
module · born here
❒asset
# aiKit.debug The switchable debug view: each character's vision cone (filled, coloured by alertness), hearing ring, lines to heard noises, remembered spots, patrol route, and every noise on the bus. Lines are rebuilt eight times a second for characters near the camera, packed into GPU buffers and drawn by the `aiKit.aiDebug` render feature in the game view. `set`, `toggle`, `isOn`; driven by the `AIDebug` component or `AI.debug`.
▲ 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.fx Short effects every screen draws: tracers, heal beams, the sniper's laser, hit flashes and slam rings. Entities are pooled and local; the Brain draws them where they happen and other screens draw them from the `aiKit:fx` message.
▲ 0↑ born
▣
module · born here
❒asset
# aiKit.looks Simple stylised bodies (humanoid, brute, bug, turret) built from the engine's unit meshes as local children, coloured and equipped from a preset's `look` (head: cap, helmet, hood, horns, hat, blind, target; gear: baton, rifle, longRifle, sword, staff, claws, spear, lantern, cannon). Every peer builds its own from the synced preset. `update` animates walking arms, crouching, turning, the attack lunge and hiding when down.
▲ 0↑ born
▣
module · born here
❒asset
# aiKit.memory What a character remembers about where others were: spots (seen, glimpsed, told by an ally, heard), each with a time and a confidence that fades over `forget` / `forgetSound` seconds. `best()` picks the spot most worth acting on, `visible()` the nearest one seen this tick. Senses write it; brains read it, so characters only act on where someone was. Available to games as `AI.memory`.
▲ 0↑ born
▣
module · born here
❒asset
# aiKit.presets Every character type as plain data: `DEFAULTS` plus the 16 built-in presets (guard, wanderer, chaser, ranged, coward, ambusher, sniper, healer, swarm, turret, companion, boss, sentry, listener, nightwatch, dummy). `resolve(name)` merges defaults, the `base` chain and the preset; `define` and `tune` add or change presets (tables merge key by key, lists replace). Read by the Brain component; changed through `AI.definePreset` and `AI.tunePreset`.
▲ 0↑ born
▣
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
# aiKit.senses Sight (range, cone, clear line from the eyes to chest or head, awareness that fills with time, distance and light), light-sensitive sight (`needsLight`, reading `registry.lightAt`), hearing (noise reach times sensitivity, linear falloff, damped through walls, own team ignored unless alarming), touch, territory and sharing sightings with allies. The Brain component runs `tick(b, dt)` on the peer that runs the character, about ten times a second; everything it learns goes into the character's memory.
▲ 0↑ born
▣
module · pulled in
❒asset
# navKit.crowd Runs every NavAgent once per frame on the peer in charge of it: a time-budgeted planning queue, path following with corner smoothing and arrival slowdown, sampled velocity avoidance weighted by mass, overlap pushing, vehicle steering with a turning circle, flock forces and the boids module's leader-follow for swarms, flight with the boids ray probe, link traversal (walk, climb, leap, warp, wait for doors), re-planning when obstacles move or block the way, and stuck detection. Fires navKit's events.
▲ 0↓ import
▣
module · pulled in
❒asset
# navKit.api The one interface to navKit: `local NK = require("navKit.api")`. Bakes maps (`NK.bake`), adds agents, obstacles, links and click-to-move, sends agents to targets from any peer (`NK.setTarget`, `NK.follow`, `NK.stop`, `NK.setStyle`), finds paths without an agent (`NK.findPath`), lists the catalog of movement styles, map types and link kinds (`NK.catalog`, `NK.find`), switches debug drawing (`NK.debug`) and reports events (`NK.on`). The package README documents every call.
▲ 0↓ import
▣
module · pulled in
❒asset
# api The one interface to SecondWind: `local HK = require("SecondWind.api")`. - **Damage:** `damage`, `heal`, `kill`, `revive`, `explode`. - **Configure:** `attach`, `configure`, `setModel`, `setDeathStyle`, `applyPreset`, `setRules`, `endRound`. - **Query:** `state`, `isAlive`, `isDowned`, `config`, `get`, `lifeline`, `all`, `rules`, `ruleValues`, `nameOf`, `isPlayer`, `catalog`, `killFeed`. - **Hooks:** `on` (events) and `setDamageFilter`. Calls on a target this peer does not own are sent to its owner, who checks them (attacker ownership, teams, the damage cap, config permission) before applying them. The package README lists every argument.
▲ 0↓ import
backing path · aiKit.package

Problems

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

These counts cover only the parts this page shows. Problems on the parts it leaves out are not counted here.

1+problem

Inside

1 problem across 1 item
aiKit.package/fsm.modulemodule · composite
●
structure.description
`README.md` still carries the [[describe]] placeholder where its description goes. Say what the module provides and who calls it.
⌬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.