Ted's RPG Rant

A place to rant about RPG games, particularly the Temple of Elemental Evil. Co8 members get a free cookie for stopping by. Thats ONE cookie each, no seconds.

Wednesday, January 02, 2008

KotB Scripts part 2

Ok, todays tutorial is about Climb / Use Rope, which has a somewhat complicated scripting process that I still get muddled with myself on occasion. I am quite proud that we have added new skills to the game. It really allows people to customise their characters. Not long ago, someone posted a thing in the 'Temple of Elemental Evil' forum on Co8 (I think it was that forum, I am not going back to look) asking what skills to bother giving a cleric. It was a fair point - once you've got Concentration, what would you bother with? Spellcraft, just because? A bunch of cross-class stuff?

Hopefully in years to come we will be turning out modules with all the Knowledge skills implemented. For now we aren't, so a couple new skills give people the chance to at least have some more choice, something useful in-game to apply their skill points, and maybe a difficult choice or two, where an RP-choice has to be made between competing needs.

That reminds me - we really have to have a combat at some point straight off a rope, so people who take their armour off before climbing are at a disadvantage (as they should be).

But back to the scripts. Climb is implemented in two separate ways in KotB: one through a dialogue where you get to climb down a hole (I won't be bother with that one here), the other through using the rope item.

Rope has always been in the game at 12011. Whether it was ever meant to do anything, I have no idea, but its nice to have it already there. I've made a few alterations, I think I'll go through them one by one since this is a neglected part of my tutorials: making new spells and such (I am by no means an expert but I have done maybe 7 new spell effects for KotB). Essentially what we are doing is setting the rope up as a 'wand' that casts a specific spell, 'Use Item'. The reasons for this will be outlined as we go along.

The changes to the rope prototype (12011) don't kick in until col 50, the item flags. They now read:

OIF_EXPIRES_AFTER_USE OIF_NO_NPC_PICKUP OIF_USES_WAND_ANIM

Straightforward. Firstly, you use it then it expires - secondly, NPC followers don't loot it (added to pretty much all items by an early incarnation of Co8 to prevent weird follower looting) - thirdly, it uses the wand animation. This means when you use the rope, you do so with a little 'throwing' animation like flicking a wand: very pleasing effect! :-)

Chugging along we hit col 59, which says our 'wand' has one charge before expiring: so we get to use each rope once. Why does it expire? Well, it is assumed when you tie a rope and climb down it then it is left there. Keep in mind we haven't just implemented 'Climb' but 'Use Rope' as well. So when you climb down a rope, it checks to see whether you have successfully tied a slip-knot (or whatever its called) that can then be released from the bottom. If so, you get your rope back (a new one is created) - if not, your rope is left there, albeit that spot is flagged so in future you can access it again without using up another rope.

There's nothing more until col 168. This is where items keep their 'powers' or bonuses. Armour bonuses, shield bonuses, weapons being chaotic or holy, skill circumstance bonuses, you name it and it appears in here somewhere. Our bonus is simply that it is a Usable Item. This means (essentially) it appears in the radial menu: stick it in your inventory and you can 'use' it through the radial menu (I don't know what the 0 and 1 parameters do precisely, but suffice it to say they are necessary and I tried a lot of variations on this setup to finally get the rope to work like this). Scoot over to col 312 and we get our spell list, which has the 'spell' Use Item: this is set up as 'Domain Special' so you don't have to be a wizard or whatever to use it. When we use our 'wand' through the radial menu (to spell it out one more time) this spell effect gets cast. What we see, though, is we choose 'rope' through the radial menu and our little fella onscreen does a throwing animation and we get teleported to wherever climbing the rope takes us.

We'll have a look at this in a minute but first, you might be asking, why use a spell? Why not just use a san_use script attachment?

I know I've ranted about this before but its my blog so I will say it all again. We should be able to do it that way: the game would be massively enhanced if we could just add a simple san_use script to generic items without having to frig around with new spells etc. San_use it not broken, per se - we can use it on written items to create little dialogue boxes, as we saw in the 'creating notes' tutorial (http://rpg-rant.blogspot.com/2007/06/creating-notes.html). We can use it to make doors work on command, or not - I did it with the ladder in the Inn (which works perfectly) and the doors into the Thieves' Guild and Warehouse (which work clumsily, but Liv has come up with a better way that I will hopefuly get a chance to implement soon).

BUT too often we are reduced to making new spells to implement scripts that should be done directly through the san_use command. Its annoying, and its prohibitive, and I don't like it.

For the rope, we're stuck with it. So lets get back to it. We have wandered down a dungeon corridor and come to a hole: we want to climb down with our rope. We select rope through the radial menu and the animation fires, and behind the scenes our 'spell' is cast: now what?

Opening Spell783 - Use Item.py and we find the usual conglomerate of OnBeginSpellCast( spell ), OnSpellEffect( spell ) and OnEndSpellCast( spell ). These occur in every spell, and I won't go into it here.

The important bit is in the spell effect area: the whole script is fairly repetitive but I will go through it here so that if anyone ever wants to add a climbing point of their own, they can easily understand what to do. Here it is:

________if game.leader.map != 5038 and game.leader.map != 5069 and game.leader.map != 5001 and game.leader.map != 5066:
________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1001 )
________________create_item_in_inventory(12011,spell.caster)
________elif game.leader.map == 5001:
________________for obj in game.obj_list_vicinity(spell.caster.location,OLC_GENERIC):
________________________if obj.name == 12797:
________________________________npc = find_npc_near(spell.caster,14144)
________________________________if (npc != OBJ_HANDLE_NULL) and (spell.caster.distance_to(npc) < 40):
________________________________________spell.caster.begin_dialog(npc, 350)
________________________________________create_item_in_inventory(12011,spell.caster)
________________________________________npc.turn_towards(spell.caster)
________________________________else:
________________________________________if (spell.caster.distance_to(obj) > 9):
________________________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1002 )
________________________________________________create_item_in_inventory(12011,spell.caster)
________________________________________else:
________________________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1006 )
________________________________________________game.timevent_add(teleportrope, (), 1300 )
________else:
________________for obj in game.obj_list_vicinity(spell.caster.location,OLC_GENERIC):
________________________# rope down
________________________if obj.name == 12797:
________________________________spell.caster.turn_towards(obj)
________________________________if (spell.caster.distance_to(obj) > 9):
________________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1002 )
________________________________________create_item_in_inventory(12011,spell.caster)
________________________________else:
________________________________________if (game.leader.map == 5038 and game.global_flags[54] == 1) or (game.leader.map == 5001 and game.global_flags[53] == 1):
________________________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1009 )
________________________________________________create_item_in_inventory(12011,spell.caster)
________________________________________else:
________________________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1006 )
________________________________________game.timevent_add(teleportrope, (), 1300 )
________________________# rope up
________________________elif obj.name == 12793:
________________________________spell.caster.turn_towards(obj)
________________________________if spell.caster.item_find(12792) == OBJ_HANDLE_NULL:
________________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1007 )
________________________________________create_item_in_inventory(12011,spell.caster)
________________________________elif (spell.caster.distance_to(obj) > 9):
________________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1002 )
________________________________________create_item_in_inventory(12011,spell.caster)
________________________________else:
________________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1006 )
________________________________________game.timevent_add(teleportrope, (), 1300 )
________________________________________create_item_in_inventory(12011,spell.caster)
________________________else:
________________________________spell.caster.float_mesfile_line( 'mes\\narrative.mes', 1001 )
________________________________create_item_in_inventory(12011,spell.caster)
________spell.spell_end( spell.id )

What a mouthful! Firstly, going through the 'sections', these can be divided up into:

________if game.leader.map != ####...

________elif game.leader.map == 5001:

________else: # rope down

______________# rope up

______________else:

Only five sections: thats easier.

Section one checks what map you are on. Only certain maps have rope points: a list of maps is checked, and if you are not on any of them, then a floatline fires (# 1001, {Not here!}) and a rope is created in the the 'spell.caster's' inventory - remember, the rope expires on use, so any time it is not used properly, it has to be replaced. (This is the reason why I haven't implemented silk rope, if anyone has been keeping track - people would be legitimately aggrieved if they spend the money on a silk rope for its +2 bonus, try it somewhere likely-looking, get the 'not here!' floatline, and have their nice silk rope replaced with a normal one. A more complicated script could check for it, but I haven't done it yet :^) ).

If you are on a viable map, then it moves on to section two and checks if you are on map 5001. This is the Keep Outer Bailey map and it is singled out for a reason: if you try to use your rope to climb into the well, and there is a watchman around, he will quite legitmiately ask what the heck you are doing and prevent you. Good for him. Also, this will be probably the only map where there will be multiple rope points: the current one is the well, there is a rope-up point planned for climbing up onto the wall (mentioned by one character late in the Demo but not yet done), and a rope-across point that I won't go into here.

Lets ignore the rest of the 5001 option and move straight onto the Rope Down option. The Rope Up and Rope Down sections are prefaced by the money line - the one that makes it all work:

for obj in game.obj_list_vicinity(spell.caster.location,OLC_GENERIC):

Climbing works by searching for a 'rope point' object, a generic item (12000s) which can be one of 3 types:

{12791}{Rope Across Hook}
{12792}{Grapple}
{12793}{Rope Up Hook}
...
{12797}{Rope Down Hook}

I threw the grapple (or 'grappling hook' if you prefer) in there because thats about to come up and I may as well show it now.

So.... having quickly assessed what generic items are lying around nearby (usually very few) the game next tests if there is a Rope Down hook. If so, the distance is calculated: you can only use your rope if you are right near your rope hook (ie near the hole, or whatever the hook is attached to). If you are too far away (further than 9 - thats pretty close) then you get another floatline:

{1002}{Move closer!}

I might speak a little more about what exactly these rope hooks are. Model-wise, they are leather armour - yup, a piece of leather armour lying there. Popping the prototypes of 12793 and 12797, we find they are both OF_CLICK_THROUGH. So you never get to touch them or pick them up (or be able to feel them there). thats not much... I must have added the OF_DONTDRAW manually in the mobs. Now here's something I never mentioned before (but have encountered a few times) - if you add something manually in the mobs, because of the way flags are set up in the mobs, it will OVERRRIDE the prototype flags, so you have to add the flags all over again. The mob saves a flag register for every type of flag you select for it, so if you just have OF_DONT_DRAW yiour flag register for the Object flagsd might look something like:

0000 0000 0010 0000

so you have zeroed out the OF_CLICK_THROUGH! Now, this may not be a big deal in this case (I doubt you can click on something that is not drawn), but be aware of it: I added a NO_TRANSFER item flag to a mob, and this negated the DRAW_WHEN_PARENTED flag in the prototype which meant instead of drawing the item in the NPCs hands, it drew it at his feet, and when he moved around, it just stayed there on the ground rotating to match him! Damn weird and a lot of stressing trying to figure it out.

While I am warning you about this, I will issue the same warning for the script override tool. If you use this on a mob to add a heartbeat, where the prototype has, say, a san_dialog attachment, you have to add the san_dialog all over again in the script override tool. Obviously saves them in the mobs the same way.

Ok... lets say we are indeed climbing down somewhere and the game has found the Rope Down hook and determined we are in range. Next it does a by-map-number flag check to see if you have used this hole (or whatever) before and left a rope dangling there, because you failed your Use Rope check when you got to the bottom (think Sam and Frodo in the Emyn Muil, of course). Since there are a finite number of ropes in the game, we can't have you running out because you keep leaving them everywhere and rolling 1s on your Use Rope check.

If the relevant flag is checked, it says:

{1009}{Climbing the rope tied here!}

and replaces your rope. If not it just says {1006}{Using rope!}

Then comes the actual moving of the party, via a timed event, teleportrope(). It is done as a timed event to give you a moment to read whatever floatline appears and to see your little 'throw the rope' animation, which really is quite effective.

Before I speak about teleportrope(), lets do Rope Up.

The main difference is, after Rope Up checks if there is a rope hanging there, it then checks for a grappling hook: unless you've tied a rope from the top, you won't go far without a grappling hook! If you don't have one, then it says:

{1007}{Needs grapple!}

Otherwise Rope Up functions exactly like Rope Down. Note it adds a rope to your pack - you never lose a rope climbing up, and don't have to make a Use Rope check. We also have a Rope Across hook but as I said above it isn't use yet.

So... straightforward, really. Now comes the tricky part.

We have to do the Use Rope check AND the check to see if you have fallen and sustained damage AFTER the map change. This is a problem: we can't trigger a time event to happen after we cross the map, since there is nothing there on the new map to trigger it: no 'attachee', if you will. Our attachment points have been generic items, which don't get to fire heartbeats or san_new_maps. Even if our rope hooks were OF_DONTDRAWn critters, rather than generic items, they still wouldn't be there on the new map and the scripts wouldn't cross.

We have 3 options:

1) Put OF_OFFed or DONTDRAWn critters at the landing points, set a flag each time the rope is used, and have these critters have a first_heartbeat file that checks for that flag and if present, does the necessaries. I didn't do this (too clunky, all the critters everywhere).

2) Use Spellslinger's Persistant Data mod to handle things across the map change, since that is pretty much what it is designed for. I didn't do that either (don't really understand how it works) but eventually this is probably how it will be done: the most elegant and least disruptive method.

3) The current method: manufacture an NPC and attach him to the party temporarily, then fire all the scripts off his san_new_map attachment.

Hence we find this in the teleportrope() script:

def teleportrope():
________if game.leader.map == 5038:
________________rope_item = game.obj_create(14591, game.leader.location + 1)
________________game.leader.ai_follower_add(rope_item)
________________game.fade_and_teleport( 0,0,0,5039,492,479 )
________elif game.leader.map == 5066:
________________rope_item = game.obj_create(14591, game.leader.location + 1)
________________game.leader.ai_follower_add(rope_item)
________________game.fade_and_teleport( 0,0,0,5069,503,388 )
________elif game.leader.map == 5001:
________________rope_item = game.obj_create(14591, game.leader.location + 1)
________________game.leader.ai_follower_add(rope_item)
________________game.fade_and_teleport( 0,0,0,5038,505,469 )
________return

We can see that the sole difference between the various sections is where the teleport lands you: otherwise in each, a critter is created and attached to the party as an ai follower (like a commanded undead or pet). This may beg the question, what if the party is full? Well, one of these days I will write a san_heartbeat addition for the Watchmen to tell people not to fill their party (and more to the point, to check if the party is already full). If it is, they will subtly tell the player there is a problem to be fixed. The thing is, the two instances of co-operative combat in the game are done by adding the person you are fighting alongside to the party - again, this may change if we can ever figure out how to check factions and do it that way (or if anyone figures out any other way to do it) but for now we just surreptitiously add the person being defended to the party. So you can well see keeping one slot in the ai section open has a couple of uses, and will just have to be done (there are 10 slots anyway. Even if all your characters have familiars / pets, thats still only 8 - and thats pretty unlikely).

Anyway, lets assume the teleport has taken place and you are on the new map. Now your critter's san_new_map fires, lets see what it does (its in py00520RopeUse.py):

def san_new_map( attachee, triggerer ):
________if game.global_vars[36] == 1:
________________game.global_vars[36] = 0
________________falls(attachee, triggerer)
________elif game.global_vars[36] == 4:
________________game.global_vars[36] = 0
________________climbs_sans( attachee )
________elif (attachee.map == 5069):
________________climbsup(attachee)
________else:
________________climbsdown(attachee)
________game.leader.ai_follower_remove(attachee)
________attachee.destroy()
________return RUN_DEFAULT

The first lot - checking for variable 36 - is from the 'climb via dialogue' option we mentioned at the very beginning of the tutorial but never went back and looked at. I'm not going to look at it now either - the purpose of this tutorial is to explaion how rope works so people can add fully functioning rope hooks if they want. So lets skip to the two subroutines climbsup(attachee) and climbsdown(attachee): and note after these run, the game returns, removes the ai follower critter, and destroys it (not removing it before destroying will give you one of those annoying 'blue circle in my party' moments).

def climbsup(attachee):
________t = 1
________for clown in game.leader.group_list():
________________roll = game.random_range(1,20)
________________effort = clown.skill_level_get(attachee, skill_climb)
________________result = roll + effort
________________if result >= 5:
________________________t = t+500
________________________game.timevent_add(comment4, (clown), t )
________________else:
________________________ring1 = clown.item_worn_at(6)
________________________ring2 = clown.item_worn_at(7)
________________________if ring1.name == 6650 or ring2.name == 6650:
________________________________t = t+500
________________________________game.timevent_add(comment5, (clown), t )
________________________elif clown.stat_level_get(stat_level_monk) >= 4:
________________________________t = t+500
________________________________game.timevent_add(comment7, (clown), t )
#_______________________elif clown.type == obj_t_npc:
#_______________________________for m, n in flying_squad.items():
#_______________________________________if clown.name == n:
#_______________________________________________t = t+500
#_______________________________________________game.timevent_add(comment2, (clown), t )
#_______________________________________else:
#_______________________________________________t = t+500
#_______________________________________________game.timevent_add(comment6, (clown), t )
________________________else:
________________________________t = t+500
________________________________game.timevent_add(comment6, (clown), t )
________return

Sooooo... firstly, there is the little command t = 1. This is essential: t is going to be a counter, going ahead by 500 (milliseconds) to stagger the reports for each member of the party. 'for clown in game.leader.group_list()' checks the whole party (including ai followers) and reports what happens, and these reports are staggered by 500ms (1/2 a second) so they cascade on the screen rather than all come up at once. So you can read them, if you want to.

Now, here's the important part... if you DON'T set t as something like 1, but just assume the computer will assume that t is 0 (which I might add I think it SHOULD) then the whole thing doesn't work. Don't ask me why. You have to set t as something.

O - and why do I call them clowns? Because I wrote the 'jump down without rope' dialogue climb thing first, and copied the rest off that. Only a clown would jump down without climbing.

So, within the 'for...' routine, each member of the party has to make their climb check. This is done via a simple random 1-20 roll, and comparing it to DC 5. If you make it, then a time event fires, timed according to the currrent value in t, stating that you climbed the rope succesfully, t advances by 500 for the next party member, and thats that.

Why DC 5? Because climbing a rope is DC 10, and you have to screw up by 5 or more to actually fall. If you screw up by less than 5, you just don't progress, though two fails in a row also make you fall. To be honest, I should probably implement that, but for the moment I have this dumbed-down version whereby you either fail catastrophically and fall, or you just succeed. And even if you fall, you only fall once, then you are assumed to have made the climb. I figured no-one wanted to see their fighter in full plate, making his checks at -8 or whatever, take 6d6 damage just from failing half a dozen checks in a row before succeeding. Thats silly: characters could die that way without the player having any input, and thats just plain bad.

What if you do fall? Just take 1d6 damage? (All distances in KotB atm are 10 feet). Noooo... what about 4th level monks, or characters wearing a new ring of feather fall? Both of these are implemented at this point. Note there is also a new ring of climbing, but this just adds +5 to your climbing skill and doesn't have to be checked for right now. Item slots 6 & 7 are your left and right hands, of course (for rings, not weapons or shields). After this a monk level above 4 is checked for. (Yes I am also going to have to add a check for Half_knight's new Wings of Pain, but believe me, I don't mind! 8^D)

Next, there is a bit that is currently remarked-out with '#': the check for the flying squad. The flying squad are those familiars that can fly - hawks and doves and bats and such. Since we check for the whole group and not just the 8-man party, we have to keep in mind that some familiars etc can fly and wouldn't fall. Makes me think I better check for those who have a decent climb score and make sure it is in the prototype (familiar spiders and such).

Its remarked out because I have not tested it yet - I don't use summonings or familiars at the best of times, never have. Don't know why, just don't. Anyway, the flaying squad has to have been previously defined: it is, looking like this:

#flying_squad = {
#small_air_elemental: 14380,
#dire_bat: 14390,
#fiendish_bat: 14407
#}

Not finished, obviously! Not tested either, but I do seem to remember at least getting it into a format that didn't break the .py file. Anyways, for anyone wanting to know how to handle arrays like this, I thought I would throw it in. A working version is found in pc_start.py (dealing with all the possible weapon feats that people could have that would lead to them getting a weapon when they start the game).

So what happens if any of these conditions are met? Well, a floatline appears explaining what happened: here are some examples.

{100}{Fell safely due to Monk abilities!}
{101}{Fell 10 feet, taking damage!}
{102}{Climbed the rope but fell, taking damage!}
{103}{Climbed the rope safely!}
{104}{Climbed down safely!}
{107}{Flew down safely!}
{108}{Fell but unharmed due to Monk abilities!}
{109}{Fell but unharmed due to Ring of Feather Fall!}
{110}{Fell while climbing, taking damage!}

Some are for the 'climb by dialogue' option of climbing without the rope. The DC is 10 then instead of 5. Where appropriate there is damage, and if that happens, you are assumed to have fallen so you wind up prone. (Thats in 'climbsdown' but not in climbs up, since if you fell while climbing up, it is assumed you got back up and climbed up again). Adding damage looks like this:

clown.damage(OBJ_HANDLE_NULL,D20DT_UNSPECIFIED,dice_new("1d6"),D20DAP_NORMAL)

while making the character prone looks like this:

clown.condition_add_with_args( 'prone', 0, 0)

Another thing we find in climbsdown is the 'get rope check', called thusly:

________t = t+500
________game.timevent_add(get_rope, (attachee), t )

which is the very last thing done. This check sees if you can 'recall' your rope and not leave it tied there. It assumes you tied it with a slip knot (running knot?) that can be detached somehow from below: this is a DC 15 check of the Use Rope skill.

def get_rope(attachee):
________w = 1
________for roper in game.party:
________________roll = game.random_range(1,20)
________________effort = roper.skill_level_get(attachee, skill_use_rope)
________________result = roll + effort
________________if result >= 15:
________________________w = w+500
________________________game.timevent_add(comment8, (roper), w )
________________________create_item_in_inventory(12011,roper)
________________________if game.leader.map == 5066:
________________________________game.global_flags[47] = 0
________________________elif game.leader.map == 5038:
________________________________game.global_flags[53] = 0
________________________return
________w = w+500
________flagging()
________game.timevent_add(comment9, (roper), w )
________return

Note the resetting of the flags that check if you have left the rope there: you don't get a choice to leave it there for later, its assumed you always try and take it with you. Deal with it.

Now, this is a really dumbed down little routine: it just runs the Use Rope skill of everyone in the party. Its a bit like Appraise or Survival - you don't have to worry about who is getting checked, because everyone does. If you succeed, you get a rope in your inventory and the flag that says a rope is there is set to 0 (in case you were climbing down a rope that was left behind before). That can make for a weird moment, if your rope and grapple are always in the thief's hands and its the druid who makes his Use Rope check - you have to go hunting for where the rope is. Small price to pay. If you fail, a small routine called 'flagging()' is run, to set the appropriate flag that leaves a rope at the spot.

Then, it hits return. There's been a few of those. Where are we returning back to?

We go right back to the san_new_map() call: once all these subroutines have run, our little interloper removes himself from the group and detroys himself. Job done: you have changed maps to whereever you climbed to, got the rope back if you had the skill, taken damage if you fell. Finished!

So, finally, how do you add a rope point in the game then? Well...

1) You add the Rope Up or Rope Down hook at the point where you want it to function (where you want to climb from, obviously - where you want a character using his rope to get a response).

2) You add the map number to the first line of the Spell783 - Use Item.py script (this is just a little performance thing - by checking immediately whether there is a viable use for the Rope on that map, rather than going through the whole 'look for a nearby object' routine, it should improve the performance on any map where there are objects around but not rope points. Just a little thing, but considering we are modding, and mods invariably come with performance issues, well, every little bit helps).

3) You add the arrival bit (where the rope is going to take you) to the teleportrope() subroutine in Spell783 - Use Item.py.

4) You pop py00520RopeUse.py and alter the get_rope() and flagging() subroutine with a new flag to allow for the presence of the rope, if someone leaves it there. Then go back and factor this in to the Rope Up section of Spell783 - Use Item.py.

A little bit fiddly, but emminently satisfying once you get it going :-)

0 Comments:

Post a Comment

<< Home