-
Notifications
You must be signed in to change notification settings - Fork 1
AI Flags
Every creature's behaviour is a bitmask. This is what each bit means, where the engine actually reads it, and what that does in play.
Checked against the tree on 2026-09-24. The authority is
creature/std_ai.h (the XStandardAI::Flag enum) and creature/std_ai.cpp
(XStandardAI::Move() and friends); everything below cites the line that
does the work, so a claim here can be checked rather than believed.
Flags are declared as enum Flag inside XStandardAI and registered for
content as the Lua table XStandardAI (std_ai.cpp, RegisterLua), so a
definition reads:
:AI(XStandardAI.RANDOM_MOVE + XStandardAI.ALLOW_PICK_UP + XStandardAI.PEACEFUL)+ in Lua and | in C++ come to the same thing, because no two base flags
share a bit.
Three things about this that are easy to get wrong:
-
:AI()replaces,SetAIFlag()adds.MonsterBuilder::AI()assigns (cr.ai_flags = flags), so a creature inheriting from another and giving its own:AI()keeps none of the parent's. ButXStandardAI::SetAIFlag()isai_flag |= aifandResAIFlag()is(ai_flag | aif) ^ aif, so anything handed flags at spawn time -Guardian(),GuardianClass(),NewCreature()'sai_flagsargument - adds to what the template already said and can never take a flag away. -
NONEis not in the Lua table. The enum has it;RegisterLuadoes not register it.:AI(0)is how content says "nothing". -
The flags are saved.
ar(ai_flag, ...)inXStandardAI::serialize, so a flag changed at runtime survives a save.
| Flag | Value | Read at | What it actually does |
|---|---|---|---|
NONE |
0x0000 |
- | No behaviour at all. Not exposed to Lua. |
ALLOW_PICK_UP |
0x0001 |
std_ai.cpp:153, 299, 306 |
Notices items while looking around, walks to the nearest one and picks it up. |
ALLOW_MOVE_WAY_UP |
0x0002 |
std_ai.cpp:170, 316 |
May take an up staircase. |
ALLOW_MOVE_WAY_DOWN |
0x0004 |
std_ai.cpp:169, 315 |
May take a down staircase. |
ALLOW_MOVE_OUT |
0x0008 |
std_ai.cpp:176 |
May leave for a location that does not otherwise allow wandering in. |
FIND_WAY |
0x0010 |
std_ai.cpp:762 |
Follows an enemy to another location, via RWayFound(). Without it MoveTo() gives up at the level boundary. |
PEACEFUL |
0x0020 |
std_ai.cpp:275, 577; location.cpp:239, 312
|
Does not start fights. On spawn its enemy_class is emptied, so it has no standing quarrel with anyone; it still counts a non-PEACEFUL intruder of another group as an enemy, and still fights back when attacked. It also does not pursue: a PEACEFUL creature never remembers last_enemy. |
COWARD |
0x0040 |
std_ai.cpp:856 |
In AttackEnemy(): runs only if the enemy's GetExp()/10 exceeds its own GetExp() * friends_count, or it is under a quarter of its hit points - and then only if TryToRunAway() finds a free square. See the note below; this fires far less often than it reads. |
ALLOW_PACK |
0x0100 |
std_ai.cpp:144, 322 |
Averages the position of friends in sight and drifts toward them when there is nothing else to do. |
ALLOW_WEAR_ITEM |
0x0200 |
std_ai.cpp:217 |
Spends a turn putting on something better, but only while no enemy is adjacent. |
GUARD_AREA |
0x0400 |
std_ai.cpp:310, 340; location.cpp:266, 319
|
At spawn, records the rectangle and learns its traps. Afterwards, any turn that ends with a step outside the rectangle is replaced by a step back toward its centre - unless the creature attacked, picked something up, or is escorting a companion. Also stops it wandering off down stairs. |
PROTECT_AREA |
0x0800 |
std_ai.cpp:545 |
Counts anyone of another group standing inside the rectangle as an enemy, whatever their class. |
RANDOM_MOVE |
0x1000 |
std_ai.cpp:334 |
Wanders one step at random when nothing else applies. |
EXPLORER_MOVE |
0x2000 |
nowhere | Declared and registered, never tested. See below. |
EXECUTE_SCRIPT |
0x4000 |
std_ai.cpp:296 |
With nothing else to do, runs the next step of the creature's script (ScriptCommand.MOVE_POINT / .CALL / .DROP_ITEM). Content never sets this by hand: ExecuteCreatureScript() sets it when it hands over the script - and clears GUARD_AREA at the same time, so giving a sentry an errand takes it off its post for good. |
NO_SWAP |
0x8000 |
xhero_input.cpp:463 |
The player cannot swap places with it by walking into it. Used to stop a guard being shuffled out of a doorway. |
Presets, not separate bits - each is an OR of the above.
| Preset | Composition | Value |
|---|---|---|
FREE_WAY |
ALLOW_MOVE_WAY_UP + ALLOW_MOVE_WAY_DOWN |
0x0006 (6) |
FREE_MOVE |
FREE_WAY + ALLOW_MOVE_OUT |
0x000E (14) |
INSECT |
FREE_WAY + RANDOM_MOVE |
0x1006 (4102) |
LO_ANIMAL |
FREE_WAY + RANDOM_MOVE + COWARD |
0x1046 (4166) |
HI_ANIMAL |
FREE_WAY + RANDOM_MOVE + FIND_WAY + COWARD |
0x1056 (4182) |
CREATURE |
ALLOW_PICK_UP + ALLOW_WEAR_ITEM + FREE_WAY + RANDOM_MOVE + FIND_WAY + COWARD |
0x1257 (4695) |
HUMAN |
ALLOW_PICK_UP + ALLOW_WEAR_ITEM + FREE_MOVE + RANDOM_MOVE + FIND_WAY |
0x121F (4639) |
Most flags only matter when the ones above them found nothing to do.
XStandardAI::Move() tries, in this order:
-
ALLOW_WEAR_ITEM- put something on (only with no enemy adjacent) - heal itself or a hurt ally beside it, when it has no enemy
- attack the enemy it can see (
COWARDmay turn this into flight) - hunt an invisible enemy it has just been hit by
- follow its companion
- pursue
last_enemy- dropped if there is no way there EXECUTE_SCRIPT-
ALLOW_PICK_UP- pick up what it is standing on, else walk to the nearest -
ALLOW_MOVE_WAY_UP/..._DOWN- take the stairs (skipped underGUARD_AREA) -
ALLOW_PACK- drift toward friends -
RANDOM_MOVE- wander
Then, after all of it, GUARD_AREA overrides the chosen step with one back
toward the post, and a last check stops the creature attacking a friend it
happens to be stepping into.
COWARD is nearly inert in a stand-up fight. All three of its
conditions have to hold at once, and in a melee they rarely do: with
several friends nearby GetExp() * friends_count makes the "enemy is
stronger" clause unreachable, and a creature hemmed in by its own line has
nowhere for TryToRunAway() to send it. It is effective in the case it was
written for - one creature, open ground - and hardly anywhere else.
EXPLORER_MOVE does nothing. It has a value and a Lua name and no
implementation; Move() never tests it. No content uses it. Either give it
a branch or delete it, but do not reach for it expecting behaviour.
Counted across world/ on 2026-09-24:
35 RANDOM_MOVE 16 ALLOW_PACK 3 NO_SWAP
27 ALLOW_WEAR_ITEM 7 HI_ANIMAL 3 GUARD_AREA
27 ALLOW_PICK_UP 6 HUMAN 2 FIND_WAY
23 COWARD 21 INSECT 1 PROTECT_AREA
22 PEACEFUL 17 CREATURE 1 ALLOW_MOVE_OUT
EXPLORER_MOVE, EXECUTE_SCRIPT and LO_ANIMAL appear in no
creature definition. EXECUTE_SCRIPT is not meant to: ExecuteCreatureScript()
sets it for you.
Representative shapes:
-- a beetle: wanders, uses stairs, nothing else
:AI(XStandardAI.INSECT)
-- an orc: hunts across levels, packs up, flees when outmatched
:AI(XStandardAI.HI_ANIMAL + XStandardAI.ALLOW_PACK)
-- a townsman: full kit and movement, starts nothing, runs
:AI(XStandardAI.HUMAN + XStandardAI.PEACEFUL + XStandardAI.COWARD)
-- a city guard: stands his post and does not run
:AI(XStandardAI.RANDOM_MOVE + XStandardAI.ALLOW_PICK_UP + XStandardAI.ALLOW_WEAR_ITEM)The guard's GUARD_AREA is not in the definition: Guardian() adds it at
spawn, along with the rectangle.
| What | Where |
|---|---|
| The enum |
creature/std_ai.h, XStandardAI::Flag
|
| The Lua table |
creature/std_ai.cpp, XStandardAI::RegisterLua
|
| Storage |
XStandardAI::ai_flag, saved by serialize()
|
| Read/write |
GetAIFlag(), SetAIFlag() (adds), ResAIFlag() (removes) |
| From Lua |
AsCreature(cr).xai:ResAIFlag(...), SetAIFlag(cr, ...), and :AI(...) in a definition |
| Where they are obeyed |
XStandardAI::Move() in creature/std_ai.cpp
|