Karl Jan Clinckspoor

My personal site

Publications

Education

Blog Posts and Tags

View My GitHub Profile

10 April 2022

Exult combat

by Karl Jan Clinckspoor

I decided to peek into the source code of Exult and figure out how combat works. Here’s the result of quite a few hours of code reading, note writing, source code manipulation and running some simulations. I hope this brings you a bit of satisfaction, and I’m certain you’ll read some parts and think “Oh, so that’s why this happens!”

A few disclaimers before we start:

With that out the way, let’s start.

How combat works

Flowchart

Other fun stuff

Combat_trace and show_hits

You can enable a debug feature in Exult called combat_trace to print out some additional information about what’s going on in combat to what’s called standard out, or stdout. If you run exult from a console, it’ll print out to that, or it’ll be in the file called stdout.txt in the Exult folder. To enable this, modify or include these lines in exult.cfg:

<debug>
    <trace>
        <combat>
            yes
        </combat>
    </trace>
</debug>

If you want, you can also enable the option called show_hits in the game’s options. This shows how many hit points an npc lost, and how many are remaining.

How long it takes to “charge up” an attack

Dexterity controls how quickly an actor can attack. A dex value of 30 means they can attack every other turn. If the npc’s dex is 29, the actor only attacks after 3 initial turns, and then the accumulated dex_points lets it attack every other turn. Only after 59 turns its excess points are spent and the npc will have to build up points for 3 turns again, and attack on turn 62. If the npc’s dex is 15, this means it will always attack once every 3 turns. A dex attribute of 1 is comical. The npc will stare at the target for quite a while until they attempt to raise their arms to attack.

I’ve… spent way more time trying to illustrate this relatively inconsequential aspect of combat than I should have. You can skip to the next section of you want. Here’s a few graphs:

How many attacks can be performed given a fixed number of turns. Notice how, for short battles, the number of attacks is the same for relatively wide ranges of dex values. Only for really long battles do we see a clear trend favoring higher dex values.

Number of attacks for n turns

Here we can see how many turns are required to perform a specific number of attacks. Notice the logarithmic scale in the y axis. For absurdly low dex values, it takes ages to perform an attack. Note also how 30 dex is favored, especially for shorter battles.

Number of turns for n attacks

Last we can see a visualization of attack frequency and the role of the leftover dex_points on each turn. Here, an attack being performed in a turn is indicated by a peak. The closer the peaks, the faster are the attacks coming. Notice how sometimes there’s 3 turn gaps, then 2 turn gaps, then 1 turn gaps and finally at 30 dex, there are no gaps — an attack goes through every other turn.

Hit frequency visualization for 30 turns

You might be wondering how long a turn lasts. To test this, I went to the Trinsic stables and modified the Avatar’s stats to 1 STR and varying DEX values. Then, I modified the game engine to print out the current state and the game’s tick number at that instant (1 tick is 1 millisecond since the game started running). I then attacked the horse and noted down how long between I started “charging up” and I finally performed the attack animation. The speed the game runs depends on the set fps, so I did vary the fps values also. Here’s the results.

First, we see how long it takes to start a strike. This is calculated from the moment I double click the horse to induce the attack (and initiative starts building up) to the moment the Avatar decides to hit the enemy. As expected, this time depends on the fps and roughly half the fps leads to a delay twice as long. I’ve measured these a few times each and noticed there’s a variation in how long it takes to hit the target, and this increases at lower dex and lower fps. Nevertheless, the highest standard deviation I found was 80 ticks, or 0.08 seconds. Imperceptible in normal circumstances.

Hit timings

If we take the timing at 30 dex and consider this 2 turns, we can rescale this plot and compare to our previous result. This is what we get. Note how the actual number of turns to attack varies between fps. I don’t know the exact cause of this, but I guess my assumptions might be too simplistic.

Measured and theoretical number of turns

A visualization of hit frequency for two melee enemies and different game difficulties.

For combatants with equal combat stats, the attacker is always slightly biased towards hitting. To better visualize how this works, and to show how bias affects the probabilities, I’ve prepared some hit probability maps containing theoretical values of hit probabilities, from game difficulty -3 to +3. These consider you’re attacking (i.e. party member) a random monster.

We can see when difficult is 0, the 50% hit probability is slightly offset downwards, indicating the green-yellowish area (>50% hit prob) is bigger. Then, as you go from -3 to +3 (easy to hard), the yellow region shrinks considerably. Since the bias is symmetrical, if you want to know how probably it is for a monster to attack your party, you just need to consider the opposite sign graphs (i.e. a monster to-hit map at the hardest difficulty is the -3 map, and so on.)

Examples of foes you can safely interact until you enter combat

You can calmly walk around and talk to npcs with hostile alignments provided none of you enter the combat state. The instant you press ‘C’ however, you’ll attack. For example, Iriale Silvermist, located in the Fellowship retreat, is Evil but peaceful (for some time), as well as some npcs in New Magincia (Robin, Battles and Leavell).

You can also do some shenanigans and try to change the Avatar’s alignment to Evil, either with Exult Studio (I failed at that) or modifying the variable using a debugger, and see if you can complete the game.

Combat schedule states and attack modes that aren’t used

The parry and stunned states aren’t used anywhere in the code. Neither is the attack mode flank or defend. berserk is the same as random, but prevents npc from fleeing.

Health-based actions

How enemies use spells

Enemies don’t require a spellbook to cast spells. Rather, they have spells are weapons they can fire a limited number of at enemies. If you manage to get one, you can use it too. This makes spellcasters especially dangerous, since they have no mana requirements. When killed, these items are removed from them so you can’t access them natually. However, I distinctly remember a few jesters around the castle of the white dragon that spawned with death bolts or something like that, and which I looted and used for a bit. For example, Aram-dol has two spells equipped, one with 99 uses and another with 2. Good luck surviving 99 casts of some spell. If you have Exult Studio equipped, you can open an npc’s inventory gump and see the spells. Here’s an example of a mage located at an island to the south of Trinsic.

Mage inventory

Experience and levels

An npc’s level is calculated by 1 + log2(total exp / 50) (see actors.h:490). If you want to calculate the exp for a specific level, you can use 25 * 2 ^ level. Every level gives the npc 3 training points. I prepared two graphs with this info:

Experience and level

Experience and level, log scale

How to compile exult myself and make modifications?

I’ll preface and say compiling exult was a bit of a pain, because I can’t use Visual Studio (disk space concerns). If you can, then it’s by far the easiest method to compile Exult. I managed to build it in Windows (msys2), in WSL (so technically Linux, but under Windows), in Windows (Visual Studio) and Linux.

First, follow the instructions in README.win32, README.MacOSX or INSTALL (Linux). If you can’t get it to work, send me a message and I’ll try to help you. I have all my steps recorded, but there’s a few aspects I don’t understand, so I won’t post here, lest it confuses people.

tags: games - ultima - c++