Skip to content

AI Flags

Joachim de Groot edited this page Sep 24, 2026 · 1 revision

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.


How they are used

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. But XStandardAI::SetAIFlag() is ai_flag |= aif and ResAIFlag() is (ai_flag | aif) ^ aif, so anything handed flags at spawn time - Guardian(), GuardianClass(), NewCreature()'s ai_flags argument - adds to what the template already said and can never take a flag away.
  • NONE is not in the Lua table. The enum has it; RegisterLua does not register it. :AI(0) is how content says "nothing".
  • The flags are saved. ar(ai_flag, ...) in XStandardAI::serialize, so a flag changed at runtime survives a save.

The flags

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.

Composites

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)

Precedence

Most flags only matter when the ones above them found nothing to do. XStandardAI::Move() tries, in this order:

  1. ALLOW_WEAR_ITEM - put something on (only with no enemy adjacent)
  2. heal itself or a hurt ally beside it, when it has no enemy
  3. attack the enemy it can see (COWARD may turn this into flight)
  4. hunt an invisible enemy it has just been hit by
  5. follow its companion
  6. pursue last_enemy - dropped if there is no way there
  7. EXECUTE_SCRIPT
  8. ALLOW_PICK_UP - pick up what it is standing on, else walk to the nearest
  9. ALLOW_MOVE_WAY_UP / ..._DOWN - take the stairs (skipped under GUARD_AREA)
  10. ALLOW_PACK - drift toward friends
  11. 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.


Two things worth knowing before tuning

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.


What the world actually uses

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.


Where it lives

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

Clone this wiki locally