Scripting for Proto Values
Well I stayed up very late working on KotB last night, but I very quickly got sidetracked into writing this blog. There's nothing in here that I have discovered myself (though I did pen all the KotB scripts I am using as examples) but rather it is a compendium of things I have had to go look for when scripting for this or that, which I now present so you folks won't have to hunt so hard 8^D
So, lets say you want to recognise an object, or an NPC, not just by their number but according to some setting in the protos.tab. Here's some examples of how.
Firstly, let me say, you can't always get what you want. For instance, picking critters by faction: damn that would be useful. But we can't. When u try to read the faction flag, you get a changing number (certainly NOT what is in col 154 of ProtoEd) and if you try to script for it, it crashes. The reason for this (so I am told) has to do with how the .dll interprets that particular flag, and it doesn't just read it as an integer. Indeed, there is nothing in the .dll to output the contents of the flag (obj_f_npc_faction (363) for memory, it was a while back when I tried this) in a way we can understand, or so I was reliably informed when I pestered some gurus like Ag, S-S and C-Blue to help with this, so we can't just read it and say, "ahhh, yonder critter is faction X, lets manually remove him from the fight". I mean we CAN manually remove critters from a fight, but not based on faction.
Never fear! There are many things we CAN read. Some we already have seen, such as the flags: I wrote a whole tutorial on flags somewhere. Here's a quick recap from KotB:
________y = (attachee.critter_flags_get() & OCF_MECHANICAL)
________if attachee.map == 5033: ## office
________________if ((is_daytime() != 1) and (game.global_vars[19] == 1) and (y != 0)):
________________________attachee.object_flag_unset(OF_OFF)
________________________game.leader.begin_dialog( attachee, 600 )
We read the specific critter flag by logically ANDing it with the whole flag byte: if the OCF_MECHANICAL bit is set, it returns a value of 1 to y, otherwise it returns a 0. This way I can have 2 different mobs for a single NPC in the same room and access them independantly. Under the right circumstances (nighttime, and the trigger variable [19] having been set) then the mob flagged OCF_MECHANICAL (which I presume is a leftover from Arcanum and means nothing in ToEE, but in any case is simply a handy 'nothing' flag that the game still recognises) will get its OF_OFF flag unset (ie it will switch on instead of OFF) and it will kick in. Of course, there is an accompanying thing so that the mob WITHOUT the OCF_MECHANICAL flag will then switch off.
Easy, and you can do it for any of the flags: object flags (OF), item flags (OIF), NPC flags (ONF) etc.
What about some of them other columns in protos.tab? Well, lets go through in no particular order...
NAME: Ok, this is a simple but important one: the item name. Item name is col 22 of ProtoEd: it can therefore differ from the prototype number. An example of this in action is find_npc_near: lets have a look at the script whereby Otis checks to see if Elmo is nearby:
def make_elmo_talk( attachee, triggerer, line):
________npc = find_npc_near(attachee,8000)
________if (npc != OBJ_HANDLE_NULL):
________________triggerer.begin_dialog(npc,line)
________________npc.turn_towards(attachee)
________________attachee.turn_towards(npc)
________else:
________________triggerer.begin_dialog(attachee,410)
________return SKIP_DEFAULT
We've seen this before so I won't dwell on it: suffice it to say, the game looks for Elmo's NAME (8000) not his prototype number (14013) or even his IDed number (col 23, which in this case is 20008 - the line in Description.mes where the game gets the word "Elmo" so it can call him that after you have met him, instead of "drunk").
So how exactly do you read the name for moments like this? Well we could go thru Utilities.py and deconstruct the "find_npc_near" command that just got called, but I will leave you to do that for yourself (checking out the innards of that file is well worth doing for any modder). For now, I will simply say it is attachee.name == xxxxx. For instance:
________if attachee.name == 14696:
________________etc...
Easy :-) And you can see how important it is that everything have a name, which is why Darmagon went through and added a name (based on protoype # if not already set) to everything in the protos.tab - so spells, scripts doing things by name would always find a handle.
MATERIAL: No, not the material thing for meshes, I mean the material an object is made from. Want your Warp Wood spell to only warp wood? Of course you do ;^) Here is another little thing from KotB that shows how to find a wooden item: in this case, it is going by slot, so you get a 2-for-1, how to find an item by material AND by slot :-)
def unequip( slot, npc):
________for npc in game.party:
________________item = npc.item_worn_at(slot)
________________if item != OBJ_HANDLE_NULL:
________________________if (item.obj_get_int(obj_f_material) != mat_wood):
________________________________holder = game.obj_create(1004, npc.location)
________________________________holder.item_get(item)
________________________________npc.item_get(item)
________________________________holder.destroy()
________return
Item slot numbers can be found in ToEEWB: I will post them here though for better tutorial bang-for-your-buck.
0 = helmet
1 = necklace
2 = gloves
3 = primary weapon
4 = secondary weapon
5 = armour
6 = primary ring
7 = secondary ring
8 = boots
9 = ammo
10 = cloak
11 = shield
12 = robe
13 = bracers
14 = bardic item
15 = lockpicks
So to check for an item worn at this or that slot, just check:
npc.item_worn_at(slot)
where 'npc' and 'slot' get replaced by the appropriate handles. For instance, lets say you want to add a script so Terjon checks if u actually walk up to him wearing his dad's ultra-masculine pendant thingy (prototype 6139) and if so, says something about it without waiting for u to raise the issue. The pendant is flagged
OIF_WEAR_RING_SECONDARY OIF_WEAR_RING_PRIMARY OIF_WEAR_RING_SECONDARY OIF_WEAR_NECKLACE
I have never understood those "flag it multiple times" things, but Protos.tab does it a lot. Anyways, lets interpret this to mean "it can be warn in either ring slot or the necklace slot" because I think that is what it is probably trying to say. So we would check all 3 slots:
________item1 = triggerer.item_worn_at(1)
________item2 = triggerer.item_worn_at(6)
________item3 = triggerer.item_worn_at(7)
then test by item name (which for some reason is 3004):
________if (item1.name == 3004) or (item2.name == 3004) or (item3.name == 3004):
________________triggerer.begin_dialog( attachee, 990 )
And then have some appropriate dialogue at 990. (note, there may be a more elegant way of doing it than checking them individually like that, but thats ok :-)
Getting back to the material flag, we are checking the internal object flags again (we have used some of these before also) and in the case of material it is, to repeat it:
item.obj_get_int(obj_f_material) != mat_wood
where 'item' could be 'attachee' or 'triggerer' or something else depending on the circumstance. Mat_wood could of course be mat_metal, or mat_cloth, or mat_flesh, mat_glass, mat_force etc.
Do we use this to check whether we are talking to a living creature? Well we could (sort of - living or dead creature would be more accurate, but then you can't 'talk' to a dead thing, except in very special exceptions like Clarisse ;) but a better way would be by checking object type. This is the very first column in protos.tab.
OBJ_TYPE:
________for pc in game.party:
________________if pc.type == obj_t_npc:
________________________return 0
This checks to see whether a member of the party is a PC or NPC follower. Again, this could be attachee.type or triggerer.type or whatever. And it could be obj_t_npc, or obj_t_armor like the pendant, or obj_t_scroll or obj_t_generic (all the items in the 12000s) or obj_t_weapon or whatever.
Here is another quick example of obj.type and flags (portal flags this time - they don't show up too often) in action: the Knock spell.
________if target.obj.type == obj_t_portal:
________________if ( target.obj.portal_flags_get() & OPF_LOCKED ):
________________________target.obj.float_mesfile_line( 'mes\\spell.mes', 30004 )
________________________target.obj.portal_flag_unset( OPF_LOCKED )
SIZE: Back to NPCs. Need to fine-tune your specific NPC rather than just by whether it IS one or not? Well, size is good :-) Easy, too:
________if attachee.get_size < STAT_SIZE_LARGE:
Nice how they go sequentially: you don't have to say '== small or == medium or == diminutive' etc, just '< LARGE'. A few things go that way - quest outcomes ('if game.quests[19].state < qs_botched') for instance - but it is not always easy to see the progression, so normally I just do it the long way.
Still worrying about whether it is dead or not?
________if critter_is_unconscious(attachee) != 1:
want him not only conscious but on his feet?
________and not attachee.d20_query(Q_Prone):
ok, I am getting carried away ;-) those are not prototype settings. Lets try to stay focused.
But what if we are still looking for a more specific type of NPC than just one who is smaller than LARGE? Race is easy enough, I have talked about that before:
pc.stat_level_get(stat_race) == race_Halfling
pc.stat_level_get(stat_race) == race_Human
pc.stat_level_get(stat_race) == race_Elf
pc.stat_level_get(stat_race) == race_Dwarf
pc.stat_level_get(stat_race) == race_Gnome etc
but we also have the CATEGORY value in col 163 (not to be confused with the one in col 30, from category.mes):
________obj.is_category_type( mc_type_humanoid )
where 'obj' could again be attachee or whatever handle you grab it by. 'mc_type_humanoid' could be mc_type_animal or mc_type_outsider or mc_type_undead or mc_type_giant or whatever.
SUBTYPE, on the other hand is this:
________obj.is_category_subtype( mc_subtype_fire )
which is not that surprising ;-)
ALIGNMENT you already know:
________pc.stat_level_get(stat_alignment) == LAWFUL_GOOD // individuals
________game.party_alignment == NEUTRAL_EVIL or game.party_alignment == CHAOTIC_EVIL or game.party_alignment == CHAOTIC_NEUTRAL or game.party_alignment == LAWFUL_EVIL // parties
but here is a method for checking alignment as a flag (easier in some circumstances):
________alignment = target_item.obj.critter_get_alignment()
________if (alignment & ALIGNMENT_EVIL) or ( (alignment & ALIGNMENT_NEUTRAL) and not (alignment & ALIGNMENT_GOOD) ):
________________etc...
Note it is checking any evil, any good etc. You can also check the Law - Chaos line:
________alignment = target_item.obj.critter_get_alignment()
________if (alignment & ALIGNMENT_CHAOTIC) or ( (alignment & ALIGNMENT_CHAOTIC) and not (alignment & ALIGNMENT_LAWFUL) ):
________________etc...
You know DEITY:
________pc.stat_level_get( stat_deity ) == 1 // Boccob
and ATTRIBUTES:
________pc.stat_level_get(stat_strength) >= 10
and GENDER:
________pc.stat_level_get( stat_gender ) == gender_male (or gender_female)
and CLASS:
________pc.stat_level_get(stat_level_cleric) >= 1
but you won't have come across HIT DICE before (not in my tutorials, anyways!):
________if obj.hit_dice_num <= 10:
What about PORTRAITS? Yep, we can even read and change them (I can't for the life of me think of many moments to do so, but I guess it might be fun to change the portrait of a character to all bloodied and battered if he is yanked out of combat by falling to low HP):
________attachee.obj_set_int( obj_f_critter_portrait, 6500 )
(That one's from 'Flaming Sphere', which I assume was done by Darmagon and is just a masterpiece of scripting). To read a portrait, I would assume it would be:
________x = attachee.obj_get_int( obj_f_critter_portrait)
but that one is untested.
How about SKILLS? Well you should be able to read them already:
________pc.skill_level_get(npc, skill_bluff) >= 7
But what about manipulating them? Here is how Darmagon implemented Analyse Dweomer - a +20 on Spellcraft so you would recognise anything going on:
________spell_obj.item_condition_add_with_args('Skill Circumstance Bonus', skill_spellcraft, 20)
This powerful command can be used to add other bonuses besides circumstance ones, as Darmargon also discovered - but they don't come off in a hurry, so use at your peril!
________game.party[0].condition_add_with_args('Shield Bonus',8,0) // has the effect of raising the armor class of the left-most party member by 8!
As for checking for FEATS:
________if (not target.has_feat( feat_evasion )) and (not target.has_feat( feat_improved_evasion )):
where, for the last time, 'target' could be 'attachee' or 'triggerer' or however else you define it.
Well, that just leaves ROTATION, col 31 :-) We'll end this with a little script that gets quite a workout in KotB: it causes the NPC to turn and face the other way, whatever their initial rotation.
def away(attachee):
________x = attachee.rotation
________x = x+3
________if x >= 6:
________________x = x - 5.99
________attachee.rotation = x
________return