Journal UpdateSome really cool stuff happened last night. Non-scripters may not care very much...

This week has been mostly about AI, although I've done a lot of work on areas, loot, encounters, etc. But the AI was the real focus. And last night I really got somewhere with it.
Some back story:
I have a library of AI functions I'm building, and they're very useful and allow me to do what I want to do in terms of boss-mob battles and targeting specific PC characters. I have a lot of functions like: DoSpellAttackOnclass, DoSpellAttackOnLowestWill, etc. These functions allow me to really tweak the AI for a boss mob battle and make it more challenging than the default AI.
The function calls don't always succeed. It is expected that they will fail because of certain checks (that can be turned on/off with optional boolean parameters). When they fail, we simply want to run the next function call until we get something that works, or we fun out of scripted attacks.
For instance, one parameter that is default=TRUE in all of these calls is the call to IgnoreIncapacitated. This means that if the target character is not a threat in some way, like they're Charmed/Feared/Petrified, then the call returns immediately and fails, because it considers the target invalid. This is great for preventing the monster from wasting a spell on an enemy that isn't a threat.
Other ways the function can fail is if it fails to find a target at all. Example: DoSpellAttackOnclass(): If the function can't find that class-type represented in the PC party, it fails (again, expected behavior) and we can move on to the next function call.
The way I code the AI script for a boss mob then is to setup all the function calls I want, and then the first one that succeeds is the one that gets performed.
But the problem with this is that all these calls in a script are statically ordered. And that's not good. That means the AI is predictable and will do the same calls in the same order every time, and we don't want that (in most cases). Only in certain cases do I want the AI to do something every time (in the case of my Necromancer, I wanted him to cast False Life first, Fear second, and then
randomly cast from among his 4-5 most harmful spells remaining, and to target smartly).
So the part missing with all of this is the ability to easily randomize function calls. To do randomization of function calls required a huge if/else statement, and inside each statement I'd order the function calls manually. Then I'd roll a random number equal to the number of if/else statements I had created, and based on the number rolled, run that if statement.
That's repetitive, makes the script way longer than it needs to be, and it's just a bloody waste of time.
What I really needed was a way to define all the function calls I wanted to use
once, and then
shuffle those function calls in a random manner. Thus, each combat round, I get a new shuffle order and start running through the function calls to find the first one that succeeds, and perform the action.
I knew I needed an array. To my great delight, I had found that someone had already created
data strutures for NWN1. What was great was that these data structure functions copy straight over to NWN2 without any changes!
Once I had an array I knew I was home free. Randomizing the function calls was possible.
To do it, I simply create an array of sequential numbers equal to the number of AI function calls I have setup for the mob. So I end up with a simple number array like so:
// I'm going to script 6 different AI attacks
1,2,3,4,5,6
Then I run the
Fisher-Yates shuffle algorithm (in-place on the array). Each round, I get a new shuffle, so I end up with something like this:
4,6,1,3,2,5
With that shuffled array of numbers, I can easily run down my list of functions, and the one that matches the integer gets run.
So if I have the six function attacks setup, given the shuffle I just created, the 4th function runs first. If it fails (due to invalid target), then the 6th function runs next, and so on, until I have a successful function call to perform an attack action, or I've run through all the values.
If all of them fail, the script falls back to the default AI. Which is fine, because in scripting the AI for a boss mob, I don't care what the mob does after all of my options have run out. I'm generally going to script the encounter to do all the best attacks. When all those actions/spells have run out, by all means - the default AI should take over and just attack.
I took this script last night and applied it to a copy of my necromancer, and then hopped into the area and watched several times. It was pure joy. He always cast False Life first, Fear second, but then he started randomly casting from the 4-5 best spell attacks I had setup. Sometimes I would do the encounter solo, sometimes with party members, just to make sure it was attacking correctly.
As an example, one of the calls I had setup was DoSpellAttackOnclass(class_DIVINE_CASTER, SPELL_MAGIC_MISSLE). I want the mob to hit the cleric with magic missile, obviously. When the cleric was in my party, she got nailed. When she wasn't, someone else got hit with it only after all the other scripted attacks when off and the AI was left to its default scripts.
There's still a lot of work left to do. There's a lot more DoSpellAttacka and DoAttack functions left to write (I haven't started work on melee attacks yet, and I want to be able to script combat feat usage). But this is a good start and it is exactly what I needed for the first boss-mob encounter, since he's a caster.
I'm jacked. I have AI!
What I like is that this makes the encounter so much better. It doesn't make it significantly harder to the point of impossibility, but it makes it more challening. Without the AI, you can pretty much just tear into the encounter with the party of 3 (at this time in the first act) and you probably won't break a sweat. But with the AI installed on the boss mob, I find you really need to use all the disposables in your inventory and be buffed up. Otherwise, right off the bat someone is feared, someone is likely to be charmed and if Ghoul Touch (with all 3 of its lame checks) lands, you might be hurting.... And that's kinda neat, IMO.
I can just imagine this in a higher level encounter, which is what I plan to work on next, to make sure the AI really works well at higher level. What I want to test next is a high level encounter against wizard/priests, and to script Sanctuary/Etherial Jaunt usage. I want the NPC's to cast those spells and then buff up first. But if you have True Sight and attack, I want the AI to shift over on the attacked NPC and start launching their best attacks instead. Now that... to me would be really cool. And I know it will work.