Jump to content

Post yer monster scripts here


Recommended Posts

I am way too busy to ever finish a scenario. I would like to believe otherwise, but BoE taught me the reality (and I had a lot of free time then).

 

But I can whip out some pretty cool monster scripts quickly. And since some people might be overwhelmed by the scripting language, I thought that I would make some of my ideas freely available as a public service -- and to have my ideas see the light of day. Feel free to use these in your scenario, as long as my name remains in the credits in the script header.

 

I have a few ideas for some monsters and hope to post one a week. The first one is a monster that I desparately wanted to do in BoE: the phase spider. Unfortunately, I could not do it for two reasons.

  • A monster cannot both web and be invisible.
  • I could not control monster movement to make the spider teleport.

Unfortunately, BoA does not allow me 1 or 2 either (you can select a destination for movement, but movement is controlled by the path-finding algorithms, and you cannot teleport non-player creatures) because of very legitimate design decisions on Jeff's part.

But that's okay, because with scripts I can fake it. For example, the spiders don't really teleport, I just use effects and cutscene calls to make it look like they do.

 

For those interested, download my arena and try them out. Completing this adventure will not increment your scenarios won or started, so you can experiment safely.

 

Phase Spider Example

 

Next week: Manual combat targetting for user-defined combat AI.

Link to comment
Share on other sites

I haven't looked at the coding for the available examples -- still wasting time playing the scenarios -- but on the basis of that play, I'd be interested in somebody tinkering with NPC combat scripts. The ones in VoDT and Small Rebellion seem to depend too much (solely?) on line-of-sight with hostiles when deciding where to move, so one frequently has to leave combat just to get an NPC to catch up with the PCs (and if the NPC had the bad luck to enter combat around a corner from the action, he or she is unlikely to join). In sustained sequences (say, the SR mission against the Empire's hidden fort), this can be a pain. They're also unimaginative about target selection, though that is easily adjusted.

 

EDIT: Of course, now that I actually read your entire post, I see that next week may deliver just this. Sorry.

Link to comment
Share on other sites

Quote:
Originally written by Boots:
EDIT: Of course, now that I actually read your entire post, I see that next week may deliver just this. Sorry.
Actually, I was planning to show how to intelligently assess combatant abilities so that archers don't waste time on the fighters (which select_target() usually returns) when they clearly should be killing the ice lance mage. Or the fighter healing cleric.

However, I will see about addressing your concern.
Link to comment
Share on other sites

Quote:
Originally written by Newtfeet:
You can control where the monster moves to but not what path it uses, and you also can't make it teleport.
I would think Jeff could add teleporting without too much trouble. Just have it designed to modify the internal x and y coordinates.
Link to comment
Share on other sites

*i, you should post your magekiller script. I've got some monster scripts I'm working on too.

 

In that magekiller script, monsters will target characters based on spell-level, which is a more simplistic way of going about it and doesn't take much more into account, but it's still great to have.

Link to comment
Share on other sites

Quote:
Originally written by Drakefyre:
In that magekiller script, monsters will target characters based on spell-level, which is a more simplistic way of going about it and doesn't take much more into account, but it's still great to have.
That's one way to do it, but for such scripts, we should ideally make them as general as possible. Use memory cells to customize priorities such as mage spells, priest spells, missile fire or melee skill. That way, script-shy people can just customize the memory-cell values.

At least that is what my next project is.
Link to comment
Share on other sites

All right, I've uploaded the magekiller script. What it does is it finds the PC with the highest mage or priest skill in the party and sets that PC as its target. It's not perfect as the AI in BoA is quite crude, but there is a definite favoring of going after mages/priests.

 

Let me know if my commenting in the code is sufficient.

http://stareye08.tripod.com/scripts/magekiller.txt

 

Yes, there are more general ways of doing this with other skills and I may code something to that effect. However, to utilize the char_with_highest() skill script you need to have it work on the party. I figured this was the simplist way to implement this.

 

I can envision a more advanced code (call it advkiller.txt) that could work as follows:

 

1) Select a target at random

2) Check visibility, status, and whether or not it is hostile to the creature.

3) Check the difference between strength and mage + priest skills.

4) Record the character number and find the difference.

5) Go on to the next creature and run the same check. If the difference exceeds that of the previous, rewrite this over the old character and record the difference. If not, move on to the next character.

6) Continue this cycle until some truncation ends the loop and picks a target. You could conceivably check every monster in the town and once you repeat, stop.

7) Set the target to the character with the highest difference of skill.

 

You could generalize this further using memory cells with that would record the skills you want to compare. The difference would be calculated as follows:

 

diff = (skill1 + skill2) - (skill3 + skill4)

 

You should probably set some null number such as -1. Any cell with a -1 in it will be ignored.

Link to comment
Share on other sites

Ah. Much Better.

 

That is what I really wanted to do. Now my phase spiders teleport for real. They get in a hit, and if you manage to hit them back, they teleport to safety. In fact, I used memory cells to define a lot of "safety locations" so that you can have them teleporting all over the place.

 

To fight it out with the spiders, try this one room scenario . Programmers might also want to look at the room to see how I use the memory cells.

 

If you are just interested in the script , here is the source code . For intermediate/low-level programmers, note how I used a lot of different states for combat. There is a defensive webbing state, and a melee attack state. I also have transition states for moving between the two using set_state_continue().

Link to comment
Share on other sites

Quote:
Originally written by *i:
All right, I've uploaded the magekiller script.
I am not trying to disparage your coding skills *i (script looks fine to me and -- more importantly -- it is easy for beginners to read). However, now that I have read your magekiller script I wanted to point out a quick tip to everyone on optimizing code (as scripts can slow the game down).

The magekiller script has the following code fragment (UBB forces me to use ~ to represent space indentations):


randtarg = get_ran(1,0,1);
if (randtarg == 0) {
~~~if (char_with_highest_skill(11) >= 0) {
~~~~~~caster = char_with_highest_skill(11);
~~~} else {
~~~~~~caster = char_with_highest_skill(12);
~~~}
}

if (randtarg > 0) {
~~~if (char_with_highest_skill(12) >= 0) {
~~~~~~caster = char_with_highest_skill(12);
~~~} else {
~~~~~~caster = char_with_highest_skill(11);
~~~}
}


This is a lot of branch checking, which can be expensive (especially on Pentiums). Moreover, the second if group looks almost like the first, just with different numbers. Using a little mathematics, you can collapse this to a single if-else statement.


randtarg = get_ran(1,0,1);
if (char_with_highest_skill(11 + rantarg) >= 0) {
~~~caster = char_with_highest_skill(11 + rantarg);
} else {
~~~rantarg = 1 - rantarg;
~~~caster = char_with_highest_skill(11 + rantarg);
}


Of course, some people may find this a little harder to read (which is a legitimate concern if we are sharing scripts). However, it is more efficient.

Edit: And if you really want it tigher, you can edit it down to this.


randtarg = get_ran(1,0,1);
if (char_with_highest_skill(11 + rantarg) < 0) {
~~~rantarg = 1 - rantarg;
}
caster = char_with_highest_skill(11 + rantarg);
Link to comment
Share on other sites

Thanks. I coded this up quite quickly and didn't take the time to optimize since I figured it would only run through once per cycle and not loop continuously; therefore I didn't take that extra step. It's not like I'm programming my radiation hydrodynamics code here. laugh

 

However, I like your suggestion and will implement it.

Link to comment
Share on other sites

  • 2 weeks later...

Ok. I am Back

 

I said I would have the next installment in a week. It has been over a week, but real work got in the way. Plus, some of the stuff that I wanted to do turned out to be a lot harder than I thought, or virtually impossible. I have certainly started to discover the boundaries of AvernumScript now. Its nice, but it is missing some very desirable (and simple to implement functions). But more on that later.

 

I have a lot of goodies this week, so I will keep each one to an individual post. As I said last time, this week I planned to play around with target acquisition and combat AI. All of these scripts are for enemies, not for party NPCs. Next week I will implement familiars and pets, and that will address some of Boot's comments at the beginning of this thread.

 

Throughout all of these script postings I will be working with a slightly modified form of basicnpc. The modified script that I am using is available here .

 

What's the difference you ask? I changed the AI for responding to who_hit_me(). If your think about it, there is very little reason to put who_hit_me() after target selection. The only way this will be called if no one is in range, but someone hit you. While possible, this is rare.

 

So, it makes more sense to react to being hit before standard target selection, at least if the target is in within melee range.

However, if you do this, you must check that the who_hit_me() character is alive. It is possible that you were hit, but your buddy dispatched him before the script was called. If you do not check for this fact, target acquisition will put you in an infinite loop.

 

I still retain the old code for who_hit_me() after standard target selection. This is for hunting down people who hit you but are now out of range.

 

Anway, the code is not much more complicated than the standard basicnpc, and it will provide the basis for my next several posts.

Link to comment
Share on other sites

Next: Modified Magekiller

 

*i was good enough to post a magekiller script in this thread. Short, sweet and to the point. However, it has two minor issues in it.

 

  • It cannot be used by NPCs that are allied with the party.
  • It cannot target allies that are not in the party. This includes wizard allies that might be following the party through a custom script.

We are going to solve these problems, but in doing so our script is going to become much more complicated. This introduces us to the world of "engineering trade offs". If neither of the two points above apply to you, then use *i's script; it is much more efficient. But if you care about them, then you should play with mine and see if the slowdown is acceptable. In the end, the choice is up to you, the scenario designer.

I am going to present two modified mage killers, each pointing out features of AvernumScript. The first mage killer has several important features to consider when manually choosing targets.

 

  • I keep target acquistion as a separate state from the START_STATE. This is not necessary, but it makes the code easier to read. Note that I do it by checking if there are any targets at all in range, and then dropping into state 4 to manually chose a target. This is a trick that I will do a lot. If you use set_state_continue(), it will not be that expensive compared to the rest of your script.
  • I find a target by looping through all characters 0 through 119. These are all the characters there are in a town, as outlined in the docs.
  • I always check that a character can be seen and is hostile. If you do not do these things in manual target selection, your monsters may run out the room to attack their friends.
  • I have a weird way of measuring mage strength. I don't consider just spell level; I consider a mixture of spell level and bonus. A character with one spell level lower than another, but twice the bonus, is going to do a lot more damage in his spells, and thus be more dangerous. Hence my metric is somewhat justified by a measurement of average damage the spells can deliver. The complete details are in the script, as well as instructions for changing it if you disagree.
  • If we do not find any mage or priest, we always revert to the basicnpc strategy. In all manual target selection, you should always revert to the basicnpc behavior. Even if you think you have every case covered. This means less bugs.

Notice how much more complicated this is than *i's. This is something we must always consider when we write these things. We can make them really complicated, but is it worth it? If you need an allied magekiller, then yes. Otherwise, it is less clear.

One of the issues in this new script is that we created a new problem that did not occur in *i's script. To see what I mean, put a character with this script in a room and watch what happens when your party enters. More than likely your front line fighter got targetted. HUH? How did that happen after all that work?

 

The problem is your fighter entered the room first, and the bad guy saw him, but did not see the mage. So, he targetted the fighter as an alternative, which is proper behaviour for the script (*i's script does not check line of sight, and so does not have this problem). Unfortunately, the script will not reselect a target until the fighter dies, and you certainly will not let that happen. Hence the mage is never targetted.

 

To solve this problem, we need a script that forces the character to reacquire a target every few rounds. That is what we do in the second mage killer . Notice that if we have a mage, we never stay on it until it is dead. However, if we do not have a mage, we look for one every few rounds.

 

When you play this script, you will discover that this addition makes a big difference. So the moral is that initial assessment is not always as important as continual reassessment. This is something to keep in mind when writing your own targetting AI.

Link to comment
Share on other sites

A Truly Complicated Example: AdvancedNPC

 

Truly generic target assessment turns out to be much more complicated that I thought. A lot of things you take for granted in programming languages don't exist in AvernumScript. This is not really Jeff's fault. These things aren't easy and he had to make some design decisions. But in the end it limits what you can do.

 

However, if you really know how to program, there is a lot of dark code magic that you can summon to make these programs do what you want to do. That is what I have done in advancednpc.txt . This is a fairly generic AI that allows the user to control everything through memory cells.

 

In this AI I break everything into five categories.

  • Closest targets
  • Melee targets
  • Archer targets
  • Mage targets
  • Priest targets

To use the script, just put the selection strategy in Cell 4, and fill out how you feel about each of the categories in Cells 5-9. The instructions are in the comments.

You will notice that this script has a lot of states. That is because, as advanced programmers will see, I am really using the states as functions. However, they are not the same as function. Functions return to where you called them; they don't start the state over; and they have return values. I fake all this using variables. It's a lot like programming in assembly at times.

 

I am not going to say much more about this script as it is really only for the very advanced programmer (and if you are really advanced, I feel ashamed at showing you my hacks). I consider it a failure as it is a lot of code to do very little. Try it and see. As the last example taught us, chosing a target is not always as important as reassessing the situtation, running away, or yelling for help.

 

What this script did teach me was a lot of limitations in AvernumScript. It also inspired me to look for other innovative ways to do target selection. And that is what I talk about next.

Link to comment
Share on other sites

Back to Basics: Bodyguards

 

These scripts got really complicated, and it was clear that if I continued along this track, I wasn't going to really add anything useful to the script community. So what I really needed was other ways for us to choose targets, beyond just assessment of strength.

 

One obvious way that jumps out is to have characters choose targets for each other. Your characters act as a party, why can't the hostiles? All we need is a way for the monsters to talk to each. And we have that. It is called messaging.

 

To illustrate a simple messaging example, I have two scripts.

  • guardednpc.txt . This is an important NPC like an official that is surrounded by guards. When he is hit, he calls out to the guards to strike down the offender.
  • bodyguard.txt . This is a guard for the guardednpc that chooses targets based on messages sent to it.

Clearly, we can do much more complicated set ups than this. However, as the last example demonstrates, it is best to keep these scripts simple when we can.

At first glance, the scripts seem to be very simple. When the guardednpc is hit, it sends the character number of the creature that hit it as a message. When the guards receive the message, they make that their target. But there are some things to worry about.

 

The first issue is that messaging is quite primitive. All you can do it send and receive a number. The receiver cannot even ask who sent the message. In fact, as terrains or even items can send messages, it does not make sense to ask who sent a message.

 

So, we have to make sure everyone is in the same group and we only send messages in the group. There is currently no way in the editor to set groups (this is a script function). So to make it easier on the non-programmer, we use memory cell 4 to store the group and initialize this value in the INITIAL_STATE. That way we don't send messages to people who are not guarding the guardednpc, and any message the bodyguards receive they will assume is a cry for help.

 

This means, however, that you have to be careful when you start mixing creatures that use messages. They may start to read each others messages and get confused. To avoid this, always send messages to groups, and avoid using the broadcast functions (which send to everyone). Sometimes this cannot be helped, but it is generally good practice.

 

Next, notice that we have to be prepared to receive messages in any state other than the initial and dead one. If we are in a state that does not receive messages, then the message that round is lost forever. This also means that we need to avoid the set_state_continue() that I am always using. set_state_continue() changes state without deleting the message, while set_state() deletes it (because it exits the script). So set_state_continue would cause us to restart the message response process and put us in an infinite loop. Using set_state() is okay; if the monster has AP, it will call the script back up again. It is just less efficient as it stops the script and then starts it back up.

 

Finally, there is one more issue addressed in this script. If the guardednpc keeps getting hit (the bodyguards aren't doing a very good job) he may keep shouting out different targets to the bodyguards. This would cause the bodyguards to hop about doing damage to a lot of targets and not killing anyone. Maybe you want this behaviour. In case you do not, in cell 5 of the bodyguard, we set a tolerance value. This determines whether a bodyguard will respond to additional target requests beyond the first. Again, the documentation is in the comments.

 

Finally, as a homework exercise, I assign the following. smile Add a message to the DEAD_STATE of guardednpc so that it can let the guards know it died. Then have the guards do something in response.

Link to comment
Share on other sites

A Bonus Script: Mind Duel

 

The last script this week doesn't really have all that much to do with targetting, but it is another really neat example, so I wanted to post it.

 

This example is motivated by Avernum 2, specifically what it was lacking. Those of us who played Exile 2 where really let down by the final battle. Garzahd is a demon, oh my. Not a big deal when it is easy to give all 4 characters level 3 repel spirit. If you want a real battle with Garzahd, try Exile 2. You all know what I mean. I present the return of Mind Duel to Avernum.

 

Making Mind Duel characters is really easy; we just use ideas from the magekiller scripts. That is what I have done in minddueler.txt . What is much more interesting, however, is giving that ability to the party.

 

To do this, we have to use a custom item. Aesthetically, I would prefer a custom ability, but custom abilities cannot target foes. Fortunately, items can using special abilities 219 and 220. In my script for battle arena I present the code for a mind duel item. It works pretty well and utilizes a d20 style willpower versus willpower check. And I stole the idea for demonic corruption from the Conan d20 RPG.

 

There are a couple of issues with this script however arising from limitations in AvernumScript. Some of these I feel are bugs (but then Jeff is probably starting to get tired of by bug reports).

  • There is no get_energy() function. This is one of the greatest oversights in all of AvernumScript. This means I cannot prevent the character from draining creatures with no energy points! I hope Jeff adds this function. In the meantime, the script does at least keep the character from draining non-spell casters.
  • Weapons of ability 219 cost no AP to use. So you can use them as many times as you want. I stop this (inelegantly) by putting in a flag that is reset each turn. This may be a feature and not a bug (to make items with multiple targets), but it seems strange to me.
  • There is no unary negation in AvernumScript. Try y = -x in AvernumScript. It won't work. Instead, you have to fake negation by subtracting from 0 as in y = 0 - x.

If Jeff ever adds get_energy(), we will be able to do a lot more with this. For example, we can make real Exile 2 style Garzahds (immune to everything, but die when all energy points are drained).

If you want to play with the mindduel, I have yet another one room scenario set up for it. Pick up the orb and try it out.

Well that's this week. Depending on work or so, I may be back in a week with my next topic.

 

Next Week(ish): Familiars and Pets. I will also show you how to make leashes and dog whistles to have the pets do cool tricks. The stuff I plan to do will also indirectly answer Boot's question at the beginning of this thread.

Link to comment
Share on other sites

Thank you again for your patience with us code-clumsy types: these things are marvels of clarity. And thanks further for starting the get_energy() campaign. I'd been trying and failing to fake it without fully realising that that was what I was trying to fake. It would be very, very good.

 

Am I right that, were it possible to call deduct_ap from scripts other than creature scripts, the absence of AP cost for ability 219 wouldn't be as serious? Not that I'd know whether such a change would be less invasive for JV to make than adding a fixed AP cost to the ability, but presumably certain designers might prefer to have the freedom to vary the cost.

Link to comment
Share on other sites

Very nice work, despite the fact that you misspelled 'voodoo'.

 

I was planning on doing some of this (like the bodyguards and an adapted magekiller script), but you've beaten me to it. Now I'll have to work on something that you won't get to for a while. I'm thinking of working on a creature script that binds a monster to a certain type of terrain (ie fish in the water, birds in the sky, etc.).

Link to comment
Share on other sites

Quote:
Originally written by Boots:

Am I right that, were it possible to call deduct_ap from scripts other than creature scripts, the absence of AP cost for ability 219 wouldn't be as serious? Not that I'd know whether such a change would be less invasive for JV to make than adding a fixed AP cost to the ability, but presumably certain designers might prefer to have the freedom to vary the cost.
That is correct. I cannot imagine that is too difficult to add to the language, but he would have to call deduct_ap() something else. He does not want to erase the old call (for compatibility), and he needs a way for his interpreter to recognize the right calls as they will have a different number of arguments.

However, this all depends on how difficult it is to add functions to AvernumScript in general. If he has a proper symbol look-up table, it shouldn't be too hard, but then I don't know how he chose to design this.

There are other modifications I would like made to AvernumScript like unary minus and the not operator (!) that are much harder and I doubt those will be added.
Link to comment
Share on other sites

Quote:
Originally written by Drakefyre:
I was planning on doing some of this (like the bodyguards and an adapted magekiller script), but you've beaten me to it. Now I'll have to work on something that you won't get to for a while. I'm thinking of working on a creature script that binds a monster to a certain type of terrain (ie fish in the water, birds in the sky, etc.).
Then I will lay off that idea (but to be honest, I had not thought of it). My plans for the next few scripts after familiars are:

  • A spawning pool that uses very complex messaging to control all its creatures as slaves (it does coordinated target acquisition and sends the creatures there). This will use a combination of tricks from my target acquisition and familiar examples.
  • A bunch of interesting mirror terrains like a mirror of life trapping, and a mirror of opposition (if Jeff fixes give_char_item() so I can duplicate party items).

You should be safe for a while. wink
Link to comment
Share on other sites

Quote:
Originally written by Drakefyre:
Well, it's not hard to fake the not operator with excessive use of DeMorgan's laws.
True. I was thinking that you cannot do it in unary instances such as !char_ok(x), but I guess in all those cases you just do (char_ok(x) == FALSE). So maybe that's not too bad.

It would be nice, however, to have !x available for making x = 1 if 0 and 0 if anything else. Right now that requires an if-branch.
Link to comment
Share on other sites

Quote:
Originally written by Walker White:
True. I was thinking that you cannot do it in unary instances such as !char_ok(x), but I guess in all those cases you just do (char_ok(x) == FALSE). So maybe that's not too bad.

It would be nice, however, to have !x available for making x = 1 if 0 and 0 if anything else. Right now that requires an if-branch.
No, just do what you said: (x == FALSE)
You don't need to put that in an if-branch. It's equivalent to (!x), if that did what you want it to.
Link to comment
Share on other sites

Originally written by Walker White:

Code:
 x = x ^ xx = x * 0 ^ ( x - 1 ) 
, x should be 1 if it was 1 or 0 before, and 0 if it was any other number before. I'm too tired to think of how to get rid of the 1...

 

Can you use min to get the smaller of two values? If so,

 

x = min .5,x *paranthesis omitted due to strange html error*

 

before the two others should leave it as x if it was smaller than .5, or changing it to .5 if it was anything larger, such as 1.

 

Probably bugridden, but if it works, you should have changed the x successfully in just three mathematical operations without any if-clause. smile

Link to comment
Share on other sites

Guest
This topic is now closed to further replies.
×
×
  • Create New...