Laguna's Battle AI Documentation


Overview
The Battle AI system uses text scripts to define behavior for monsters in battle. The scripting language is very specific and just powerful enough to do what it needs to do. There are some seemingly reasonable things which can’t be done with it, but these things are by no means necessary in creating capable and challenging monster brains. However, in general the language is easily expandable from within the Anachronox source(currently unavailable).

Prerequisites:

  1. Knowledge of basic programming, and the study of APE.

 

Any given monster that exists in the Game Data Base should have a key that specifies which script is used for it: these scripts are typically located in anoxdata/battle/ai/.

A typical monster.gdb file:


level int 5
xp int 60
aiscript file "battle/ai/scriptname.txt" = The monster AI script filename. See GDB Object for more information.

When battle starts, the game will load the script file for each monster, examine them and report any errors found. When the monster’s timer is up, the game will stop time and execute the script. The script then decides what the best action(s) is(are) and makes it(them) happen. When the monster is done doing everything it decided to do, it’s turn is over and battle time resumes.

Scripting Language
In an abstract sense, the script files are simply a series of condition-action blocks. (A block is everything between a left bracket and a right bracket; there will be more info on syntax later). Each block begins with one or more conditions to be met, and one or more actions to be performed if all the conditions are true. The game will proceed through the script one block at a time until a true condition is found. Then it will execute all the actions in that block and quit.

Basic Syntax
First, here is an example of a very simple script:

# Test script, just basic
default
{
(attack nelohp)
}

Or, in a conceptualized version:

# comment

condition
{
(action)
}

First of all, everything on a line that comes after a pound sign ' #' is a comment and will be ignored by the game. You can use them anywhere you want, but everything after it on that line is a comment.

In the example above, 'default' is the only condition. Default is a special conditional which is always true. It’s used to tell the AI what the default behavior is in case none of the other conditions are true. In this case, we use default because we want the monster to do the same thing every time.

Notice the brackets. Every condition must be followed by a block of actions, which must exist entirely between an open and a close bracket. To be totally clear: only comments and conditions can be outside the brackets. Only actions can be inside the brackets (with one exception, which we will talk about in a later section). Also, each individual action must be in parentheses.

'Attack nelohp' will make the monster try to attack, in any way possible, it’s nearest enemy with the lowest hit points. What’s that mean?

“In any way possible” means that it will think about each attack it’s capable of, starting with the most powerful, until it finds of one that would work. Also, if any of the attack functions can’t find an attack to use, it automatically makes the monster move toward the target and/or toward a position where an attack might be possible.

“It’s nearest enemy with the lowest hit points” is specified by 'nelohp'. First of all, distances are measured in terms of how many turns it would take to walk there. There will be more information about understanding nelohp in the next section, but for now just understand that “nearest” is more important than “lowest hit points.” In other words, if the nearest enemy is three steps away then no enemies more than three steps away will be considered.

Subject Composition
The most complex part of writing these scripts is referring to specific entities in battle. After you get the hang of it though, it’s very intuitive and fairly easy.

General Subjects
A subject is a short string of characters that refers to a single entity in battle. It may refer to the exact entity, or it may specify a random individual from a group of entities. The basic structure of a subject looks like this:

distance + team [ + stat range + stat type ]

Stat range and stat type are in brackets because they are not required. However, if you specify one you must specify the other as well. Here’s a breakdown of what each of those things mean, and what the valid values are:

Distance
N = Nearest
F = Farthest
A = Adjacent
* = Whatever

Team
E = Enemy
A = Ally
* = Whatever

Stat Range
LO = Lowest
HI = Highest

Stat Type
HP = Hit points

Special Subjects
SL = Self

All you have to do is put the letters together, and case doesn’t matter. Here are some examples to help solidify it for you:

Nahihp = Nearest ally highest hit points
sl = Myself
*a = Any ally
n*hihp = The nearest whatever with the highest hit points
ae = Any adjacent enemy
Aelohp = Adjacent enemy with the lowest hit points
** = Some random lucky bastard

Specific Subjects
You can also specify specific nodes in battle by using a predetermined field created in the editor. For example, say there is a node in your battle that you want everyone to try to attack. You could say:

(attack &targetname=deathnode)

And in BED (the battle editor), you would have to give that node a .map key/value pair of “targetname=deathnode”. Also, if you have an APE gamevar or gamestring that holds the value you want to search for, you can use it as well. However, if you use a numeric variable, you must prefix it with a percent sign (“&uid=%next_uid”) or use a dollar sign prefix for a string variable (“&message=$node_message”).

Just for completeness’ sake, here is the general syntax for it:

&keyname=[%,$]value

Conditions
Conditions control the flow of your script. If there is a prime place for elusive errors and bugs to creep in, this is it. Conditions consist of operations, or functions, which are performed on a subject and result in an integer or boolean value. If the particular function in use results in an integer value, you must provide a comparison (more detail coming up). If the function is boolean, you don’t need to compare it to anything. The ' default' function, from the example script above, is a boolean function that always results in TRUE.

Conditional Functions
Here is the format for a conditional statement:

subject.function [ operator value[%] ]
or
gamevar:name [ operator value[%] ]

The subject is a string of characters as just specified in the last section. Next, there must be a dot/period. Then comes the function. Here is a list of the possible functions:

These five evaluate to an integer value…

Hp = Hitpoints
Naden = Number of adjacent enemies
Nadal = Number of adjacent allies
Nen = Total number of enemies
Nal = Total number of allies

And these are binary…

Isburn = Is subject burning?
Isnuts = nuts?
Ispois = poisoned?
Isfroz = frozen?
Isslow = slowed?
Ishast = hasted?
Iswink = winky?
Issnoo = snoozing?
Ispsys = a psy slave?

Isvuln = is subject vulnerable to attack from any enemy?
Isvis = is subject visible to any enemy?

Canburn = Am I capable of inflicting burn status on this subject?
Cannuts = Am I capable of inflicting nuts status on this subject?
Canpois = Am I capable of inflicting poison status on this subject?
Canfroz = Am I capable of inflicting freeze status on this subject?
Canslow = Am I capable of inflicting slow status on this subject?
Canhast = Am I capable of inflicting haste status on this subject? Is this actually still in the game?
Canwink = Am I capable of inflicting winky status on this subject?
Cansnoo = Am I capable of inflicting snooze? status on this subject? Is this actually still in the game?
Canpsys = Am I capable of inflicting psyslave status on this subject?

Canbeat = Am I capable of inflicting a beat attack on this subject?
Iamon
Isoccupied

Canhurt = Am I capable of using hurt mystech on subject?
Canheal = Am I capable of using heal mystech on subject?

CanFixburn = Am I capable of fixing burn status on subject?
CanFixnuts = Am I capable of fixing nuts status on subject?
CanFixpois = Am I capable of fixing poison status on subject?
CanFixfroz = Am I capable of fixing freeze status on subject?
CanFixwink = Am I capable of fixing winky status on subject?
CanFixsnoo = Am I capable of fixing snooze status on subject?
CanFixpsys = Am I capable of fixing psyslave status on subject?

Comparisons
If you’re using a boolean function, then that’s it. However, if you’re using one of the functions that result in a value, you will need to provide a comparison. For example:

sl.hp < 30%

(Note that the spaces on either side of the operator are currently required. This is a limitation that may or may not be fixed depending on time restraints and demand.)
This tests if the current entity’s hitpoints are less than 30% of it’s maximum. If it’s max hitpoints are 200 and it’s current hitpoints are 50, this will be TRUE. If it’s current hitpoints are 60, it will be false because 60 is 30% of 200, and thus is not less than 30%. If you wanted it to be true, you would need to use the “less than or equal to” operator (see below… that kind of thing shouldn’t ever really be a problem or source of stress, but you should be aware of it anyway).

Here is yet another table, this particular one being the list of comparison operators you can use:

< = is less than
( = is less than or equal to (Is this right? This is normally <=)
= = is equal to
) = is greater than or equal to (Is this right? This is normally >=)
> = is greater than
! = is not equal to

Multiple Conditions (Clauses)
Now we finally get to the point where conditions become a bit more powerful. You can put up to sixteen separate conditions, each of which would be called a clause, on a single line. Just separate each one with a comma, like so:

sl.hp < 30%, sl.canheal

This says “if my hitpoints are less than 30 percent of my max, and I have the ability to heal myself.” For the entire line, or statement, to be true, each individual clause must be true.

Using <OR> with Conditions
By default, each clause in a condition has an AND relationship with the other. That is, “A, B” is true only if A and B are both true. By putting

<OR>

as the first thing in a conditional, everything in that conditional will have an OR relationship. “<OR> A, B” is true if either A or B are true.

Actions
If the conditions are the brains of a script, the actions are the brawn. They just get it done. Each individual action must be in parentheses, and there can be only one action on a line. However, you can have up to sixteen individual actions inside a block. When you have multiple actions inside a block, they will be carried out in the order they appear in the block, and each one will not happen until the one before it completes. If you happen to need two separate actions to occur at exactly the same time, too bad. But that functionality could be added with a little effort if it becomes desirable so say something to the programmers if you find yourself needing it.

Functions
Here is the syntax for an action:

(function [parameter 1 [parameter 2] … [parameter N]])

Function is one of the following:
Beat (subject) (gdb item) = Hit target with beat attack, or move closer to target if beat is not possible
Range (subject) (gdb item) = Hit target with ranged attack, or move toward a spot where target is visible from
Attack (subject) (gdb item) = Attack target with anything, or move toward a spot where attack is possible
Invoke (sequence) = Invoke an APE sequence
Gamevar (var)(value) = Assigns a value to an APE gamevar
Sound (name) = Play a sound
Anim (name) = Play an animation
Goto (subject) = Teleport to a specific node
Spawn (GDB classname) (target node) (script name) = Used to call other objects into battle, e.g. Stone Sentinel boulders or Hive Queen drones.
Effect (subject) “effect string = Used to add hit points, poison, fire, etc to the subject.
Setinvuln (true,false) = Become invulnerable or not
Become (script name) = Switch to using a different AI script
Burn (subject) = Set subject on fire
Nuts (subject) = Make subject nuts
Pois (subject) = Poison subject
Froz (subject) = Freeze subject
Wink (subject) = Make subject winky
Snoo (subject) = Make subject snoozin
Psys (subject) = Make subject a psy slave
Slow (subject) = Slow subject
Hast (subject) = Haste subject
Hurt (subject) = Hurt subject
Heal (subject) = Heal subject
Fixburn (subject) = Put out fire on subject
Fixnuts (subject) = Make subject un-nuts
Fixpois (subject) = Un-poison subject
Fixfroz (subject) = Thaw subject
Fixwink (subject) = Unwinkify subject
Fixsnoo (subject) = Wake subject
Fixpsys (subject) = Make subject not a psy slave
Fixany (subject) = Undo any negative status modifiers if possible
Hide = Move to a place where the enemy cannot see
Pass = Skip this turn - do nothing
Moverandom = Just move to wherever

Example:

# Golem

sl.hp < 50%
{
(beat nelohp "GolemSpecial")
}

sl.naden > 0
{
4: (beat ae)
1: (beat nehihp "GolemSpecial")
}

default
{
(beat ne)
}

 

Random Actions
It’s hard to create unpredictable behavior without using some kind of random selection. If you want to have a monster select one action from a list of possible actions, prefix each action to be selected from with a number representing it’s probability of being selected, followed by a colon. For example:

5: (attack nelohp)
1: (heal sl)

This will cause the monster to usually attack nelohp, but 1 out of 5 times (randomly, not precisely) it will heal itself.

APE Gamevars
In order to ease interaction between APE and the Battle AI scripts, you can check and set the value of gamevars directly from the AI script. To set it, use the gamevar action, just as any other action, followed by the gamevar name, then a space, then the value to assign to it. You can also add and subtract values for counters. Prefix the number with ‘+’ to add the value, or ‘-’ to subtract.

(gamevar variable [+,-]value)

Nested Blocks
Now we get to the part of writing scripts that takes a little thought. You can put blocks of conditions inside conditions to streamline and simplify your scripts. The only thing that is different inside a nested block is that when the end of a nested block is reached, the script continues at the first condition after the block (with the exception of default block, which is explained below).

To help explain how to use nested blocks, I’m going to use a real example of something you might want to do with a script. Let’s say you want a monster to:
Attack, or move toward, it’s nearest enemy with the lowest hitpoints
Occasionally attack it’s nearest enemy with the highest hitpoints
When it’s health gets less than 1/3 of it’s max, it will try to get in a place where it can’t be attacked
After it gets to a place where it can’t be attacked, it will heal itself

You can do this like this:

sl.hp < 33%, sl.isvuln
{
(hide)
}
sl.hp < 33%, sl.canheal
{
(heal sl)
}
default
{
5: (attack nelohp)
1: (attack nehihp)
}

Or like this:

sl.hp < 33%
{
sl.isvuln
{
(hide)
}
sl.canheal
{
(heal sl)
}
}
default
{
5: (attack nelohp)
1: (attack nehihp)
}

In this example, the only advantage you get by nesting the blocks is that it’s a bit more comprehendible. In order to read the conditionals like a book, you have to learn to read the comma as the word “and”.

The first one is “If my health is low and I can be attacked, get to a place where I can’t be attacked. Or if my health is low and I can heal myself, then heal myself.”

The second one is “If my health is low, get to a place where I can’t be attacked, then heal myself if I can.”

But beyond comprehension, if you start writing large complex scripts you’ll find that it will be much easier to separate common conditions into a nested block rather than typing the same clause over and over for each block. Also, if you wanted to change the above example to 50% instead of 33%, it’s much easier to change at the top of a block than it is on every 5th line.

Special Behavior of Nested Default Blocks
As we learned in the section Conditions, the default block will always execute when it’s reached. This applies in nested blocks also. Check it out:

sl.naden > 1
{
sl.hp > 100
{
(attack ne)
}
sl.hp < 20
{
(hide)
}
}
sl.nadal > 1, nalohp.hp < 50%
{
(heal nalohp)
}
default
{
(pass)
}

This says
If I have more than one enemy near me, attack it if my health over 100 or hide if my health is below 20
Or, if I have more than one ally near me and it’s hitpoints are less than half, heal it.
Or, pass.

Now, let’s say that sl.naden > 1 and sl.nadal > 1, nadal.hp < 50% are true, but that sl.hp is 50. Neither of the blocks under sl.naden > 1 will be true and the script will just go on to sl.nadal > 1, nadal.hp < 50%. But what if we don’t want the script to do anything else if there are enemies around? We stick a default in there, and the script will stop at that point:

sl.naden > 1
{
sl.hp > 100
{
(attack ne)
}
sl.hp < 20
{
(hide)
}
default
{
(pass)
}
}
sl.nadal > 1, nadal.hp < 50%
{
(heal nadal)
}
default
{
(pass)
}

That's the end of the documentation that Laguna wrote. I may expand the documentation a little in the future. Laguna did a good job of writing this document, so I may never expand it.

For now pour over these examples:

# Alley Goon

sl.hp < 50%
{
(beat nelohp "AlleyGoonSpecial")
}

sl.naden > 0
{
4: (beat ae)
1: (beat nehihp "AlleyGoonSpecial")
}

default
{
(beat ne)
}

 


#
# Big Wimpa
#

gamevar:@did_protects = 0
{
(effect sl "prot_statuseffects=999")
(gamevar @did_protects 1)
(again)
}

sl.hp < 38%, gamevar:@wimpa_dont_hide = 0
{
(hide)
(gamevar @wimpa_taunt 0)
(gamevar @wimpa_dont_hide 1)
}

gamevar:@hasted_90 = 0, sl.hp < 90%
{
(effect sl "add_beatspeed=15")
(gamevar @hasted_90 1)
(again)
}

gamevar:@hasted_75 = 0, sl.hp < 75%
{
(effect sl "add_beatspeed=25")
(gamevar @hasted_75 1)
(again)
}

gamevar:@hasted_50 = 0, sl.hp < 50%
{
(effect sl "add_beatspeed=35")
(gamevar @hasted_50 1)
(again)
}

gamevar:@hasted_25 = 0, sl.hp < 25%
{
(effect sl "add_beatspeed=50")
(gamevar @hasted_25 1)
(again)
}

gamevar:@wimpa_taunt = 1
{
(range &uid=%@bootsnode "WimpaAvalanche")
(gamevar @wimpa_taunt 0)
(gamevar @wimpa_visible 0)
}

sl.naden > 0
{
(beat ae "WimpaBeat")
(gamevar @wimpa_visible 0)
(gamevar @wimpa_dont_hide 0)
}

sl.isvisible
{
gamevar:@wimpa_visible = 0
{
(gamevar @wimpa_visible 1)
(range ne "WimpaSnowball")
}
gamevar:@wimpa_visible = 1
{
(gamevar @wimpa_visible 0)
(hide)
}
}

default
{
# 20000:100 sets @bootsnode to the UID that boots is standing on
# Has to do this so he can attack the same node twice even if
# boots moves.

(invoke 20000:100)
(range &uid=%@bootsnode "WimpaTaunt")
(gamevar @wimpa_taunt 1)
}

 

 


# Detta

gamevar:@detta_stage = 0
{
1:(invoke 20000:700)
1:(invoke 20000:710)
}

gamevar:@detta_stage = 1
{
1:(invoke 20000:720)
1:(invoke 20000:730)
}

gamevar:@detta_stage = 2
{
1:(invoke 20000:740)
1:(invoke 20000:750)
}

gamevar:@detta_stage = 3
{
1:(invoke 20000:760)
1:(invoke 20000:770)
}

 

 

# Rictus

gamevar:@spawn ! 0, gamevar:@spawn_wait = 0
{
gamevar:@spawn = 1
{
(gamevar @spawn 0)
(gamevar @rictus_minion_type 1)
(gamevar @rictus_minions 2)
(spawn "Krapotron Brutalbot" 160 krapton_rictus_summon_krapbot_left)
(spawn "Krapotron Brutalbot" 159 krapton_rictus_summon_krapbot_right)
(again)
}
gamevar:@spawn = 2
{
(gamevar @spawn 0)
(gamevar @rictus_minion_type 2)
(gamevar @rictus_minions 2)
(spawn "Devil Slug" 160 krapton_rictus_summon_devilslug_left)
(spawn "Devil Slug" 159 krapton_rictus_summon_devilslug_right)
(again)
}
gamevar:@spawn = 3
{
(gamevar @spawn 0)
(gamevar @rictus_minion_type 3)
(gamevar @rictus_minions 2)
(spawn "Bad Person" 160 krapton_rictus_summon_badperson_left)
(spawn "Bad Person" 159 krapton_rictus_summon_badperson_right)
(again)
}
gamevar:@spawn = 4
{
(gamevar @spawn 0)
(gamevar @rictus_minion_type 4)
(gamevar @rictus_minions 2)
(spawn "El Pinto" 160 krapton_rictus_summon_elpinto_left)
(spawn "El Pinto" 159 krapton_rictus_summon_elpinto_right)
(again)
}
gamevar:@spawn = 5
{
(gamevar @spawn 0)
(gamevar @rictus_minion_type 5)
(gamevar @rictus_minions 2)
(spawn "Orange Roughy" 160 krapton_rictus_summon_goldfish_left)
(spawn "Orange Roughy" 159 krapton_rictus_summon_goldfish_right)
(again)
}
}

gamevar:@rictus_stage ! 2, sl.hp < 67%
{
(spawn "Cosmic Machine" 216 monster_null_left)
(spawn "Cosmic Machine" 227 monster_null_right)
(invoke 20000:500)
(gamevar @rictus_stage 2)
(gamevar @spawn_wait 0)
(gamevar @spawn_next 1)
(gamevar @spawn 1)
(again)
}

gamevar:@rictus_stage = 2, gamevar:@rictus_minions = 0, gamevar:@spawn_wait = 2
{
(invoke 20000:2000)
(gamevar @spawn @spawn_next)
(gamevar @spawn_wait 1)
(again)
}

sl.naden > 0
{
(gamevar @spawn_wait 0)
(beat ae "RictusBeat")
# 1: (range *ehihp "RictusRanged")
}

default
{
(range *ehihp "RictusRanged")
}

 

 

# Hive Queen
#
# Who fights: Boots, Grumpos, Rho
#
# Where: Heart of the Verilent Hive (hive.map)
#
# Cinematic/Context: Rho has just used her Analyze world skill to break into the Hive Queen’s chamber.
#
# Room Design: The Queen resides on one node at the top, with no adjacencies. Beneath her are two
# confront nodes where CyberSects spawn. These nodes have 1-way adjacencies to the three forward
# player positions. The three forward player positions are along a line between the the two spires,
# which are now NRG sources for both the Queen and the players. There is an OBSTRUCTION in the center
# of the room that is man-height. To each side of the pillar are flanking 'mid' position nodes. Mid
# nodes are immune to queen beat attacks, but are still in range of her laser. Each mid position is
# adjacent to the two forward positions in front of it as well as the single rear position. There is
# one rear player position where the entry hall meets the room, between the first spires. This position
# is safe from all queen attacks (beat and laser), and has adjacency with the two flanking mid positions.
#
# Combat Specifics: Aasdfas
#
# MysTech ONLY - no beat weapons work in this battle.
#
# If the queen taps these spires, then each fully charged spire will spawn/summon a new CyberSect to the
# two confront positions. She will not tap them again unless the previous two CyberSects are killed.
# When a player is standing on a node next to a spire, (s)he has the ability to draw NRG from that spire.

sl.nal = 0, gamevar:@rightCrystal = 0, gamevar:@leftCrystal = 0
{
(spawn "Cybersect" 279 "monster_hivequeen_summon_left" 20000:9000)
(invoke 20000:201)
(spawn "Cybersect" 278 "monster_hivequeen_summon_right" 20000:9001)
(invoke 20000:101)
}

gamevar:@fmocc = 1
{
2: (range &uid=%289 QueenBeatCenter)
1: (range &uid=%289 QueenBeam)
}

gamevar:@flocc = 1
{
2: (range &uid=%290 QueenBeatLeft)
1: (range &uid=%289 QueenBeam)
}

gamevar:@frocc = 1
{
2: (range &uid=%291 QueenBeatRight)
1: (range &uid=%289 QueenBeam)
}

<OR> gamevar:@rlocc = 1, gamevar:@rrocc = 1
{
2: (range &uid=%289 QueenBeam)
1: (pass)
}

default
{
(pass)
}