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.

Tuesday, September 20, 2005

Proactive NPCs

Welcome to part 6 of the Dialogue writing tutorial, and what a pretentious name it has!

By 'Proactive', I mean situations in which the NPCs butt in and say their piece (or react) without the PC initiating it. We have seen that generally events have to be triggered by the PC (who is defined as the triggerer in many scripts, or will be the effective triggerer) and as far as dialogue goes, it is triggered by the player clicking on the NPC to initiate dialogue (or selecting talk from the radial menu).

Many of the proactive events are no different, they are still triggered by something the PC does, its just not as blatant as a click from the mouse. An obvious one is when you move near to an NPC such as the sentinel outside the ruined Tower, who speaks as soon as you get near (or any of the NPCs in the opening vignette, again they will start talking as soon as you go near them, you don't have to click on them and you don't get the option of just walking past).

Dialogue-by-click (if we can call it that) is activated by the san_dialog script we saw earlier. This script is accessed by the game when you clearly initiate dialogue, by clicking or selecting 'talk' on the radial menu. Proactive efforts are done by another method. This is the ubiquitous 'heartbeat' method you may have heard reference to.

The heartbeat is a script that fires every couple of seconds (no surprises for guessing where the name came from). It can do pretty much anything you like, but here we are going to consider its use to start a dialogue. Since Liv always does these things the best, lets look at Kent, her kiddy at the start. (His script is py00228kids_off.py)

for obj in game.obj_list_vicinity(attachee.location,OLC_PC):
__________if (obj.distance_to(attachee) <= 30 and game.global_vars[702] == 0 and critter_is_unconscious(obj) != 1):
______________if (obj.stat_level_get(stat_race) == race_halfling):
_______________________game.global_vars[702] = 1
_______________________attachee.turn_towards(obj)
_______________________obj.begin_dialog(attachee,500)
______________elif (obj.stat_level_get(stat_race) == race_halforc):
_______________________game.global_vars[702] = 1
_______________________attachee.turn_towards(obj)
_______________________obj.begin_dialog(attachee,600)
______________elif (obj.stat_level_get(stat_level_paladin) >= 1):
_______________________game.global_vars[702] = 1
_______________________attachee.turn_towards(obj)
_______________________obj.begin_dialog(attachee,200)
______________elif (obj.stat_level_get(stat_level_wizard) >= 1):
_______________________game.global_vars[702] = 1
_______________________attachee.turn_towards(obj)
_______________________obj.begin_dialog(attachee,300)
______________elif (obj.stat_level_get(stat_level_bard) >= 1):
_______________________game.global_vars[702] = 1
_______________________attachee.turn_towards(obj)
_______________________obj.begin_dialog(attachee,400)
______________else:
_______________________game.global_vars[702] = 1
_______________________attachee.turn_towards(obj)
_______________________obj.begin_dialog(attachee,100)

Well that looks complicated! I barely understand it myself. But it starts repeating itself pretty quickly so it won't take too long to see what it is all about.

First line:

for obj in game.obj_list_vicinity(attachee.location,OLC_PC):

Obviously words like 'for' and 'list' tell us we are going to do a loop. What we are checking for is a list of objects in the vicinity of the attachee's location (ie anything near where Kent is at that moment), and seeing if those objects are PCs. If they are, it moves on to the next line:

if (obj.distance_to(attachee) <= 30 and game.global_vars[702] == 0 and critter_is_unconscious(obj) != 1):

Note 'if' clauses always end in a colon.

In the event that a PC is detected, it checks:

Is s/he close? Actually 30 is pretty far, but remember Kent is MEANT to speak to you as soon as you appear.

Is the critter conscious? Note how it is phrased - is the 'critter_is_unconscious' flag for the object (the PC) NOT equal to 1. I am not sure why that is there: possibly you can come thru from a combat start like CE, NE or CN with noone in a state to talk. But if so, how did you win the combat? No idea, maybe Liv is just making sure or she played through an obscure situation where it happened.

And finally, game.global_vars[702] == 0. You will notice further down that when one of the choices is made, it sets this variable to 1. Once this variable is no longer 0, Kent will never butt in in this manner again. It makes sure he only does it once. (Kids can still butt in though, look further down in the file and you will see they can come and annoy you a fair bit).

If all these conditions are met, it goes on to the next line passage. This is a series of conditions that establishes where the dialogue will start. Here we see why Liv specifically went for a list of the PCs and not just checking if there was A PC (or the leader, game.party[0], or whatever). Its so all the PCs could be checked for to see (in order):

if there is a halfling

if there is a half-orc

if there is a paladin

if there is a wizard

if there is a bard.

If that is meant to be from least to most likely - so you get to see obscurer lines first - then I would have put the bard ahead of the wizard myself. If none of these are present then you get the default line.

The lines of dialogue that start, then, are dependant on what is there.

attachee.turn_towards(obj)
obj.begin_dialog(attachee,400)


It is not just 'start at line 1' as we have been doing with simpler examples. Rather, we have the following lines depending on the PC examined:

{500}{Hey, a new kid! Where are your parents? What grade are you in?}{Hey, a new kid! Where are your parents? What grade are you in?}{}{}{}{} halfling
{600}{[looks frightened] Um... uh... Are you a giant? You won't eat me will you??}{[looks frightened] Um... uh... Are you a giant? You won't eat me will you??}{}{}{}{} half-orc
{200}{Wow! Are you a real knight!}{Wow! Are you a real knight!}{}{}{}{} paladin
{300}{Wow! Are you a wizard!}{Wow! Are you a witch!}{}{}{}{} wizard
{400}{Wow! Are you in a traveling band!}{Wow! Are you in a traveling band!}{}{}{}{} bard

Wonder why Liv didn't use question marks? O well ;-)

So that was all pretty straight forward: if the party goes near Kent (and they will, he is positioned so they will ;-)) then his heartbeat file will trigger an appropriate line of dialogue.

Note that if we didn't care if the party never spoke to Kent again after this, we could avoid using san_dialog all together: Kent would just show a floating line when you clicked on him (like one of the pirates wandering Nulb) but will still have his dialogue triggered by san_heartbeat. The two are completely independant.

Why do some critters have floating lines to cover a lack of dialogue and others (like chickens) don't? It depends whether they are flagged as 'mute' in their object critter flags. If they are, they show nothing: if not, they will show a generic dialogue line.

One more example, something much simpler: the goon who screams 'intruders' in my mod. Now, this is messy and complicated as only a mod can be: the goons are dlg file 00286, but they work off heartbeat py00288 (because I wanted him to speak first, but he was too far away - I should have used Liv's vicinity of 30 thing perhaps) and also contain a dialogue line for the daemon who gets summoned (to save having a whole new dlg file for it - Liv likewise stores other things in the kids' file.) Note that because the goons have their san_dialog set as 00286, when their heartbeat script fires for a dialogue (we are about to see this) it fires dialogue from file 00286 even though it is py file 00288. Interesting! I wouldn't have thought it would work like that, but it does!

The heartbeat is like this:

def san_heartbeat( attachee, triggerer ):
________if (not game.combat_is_active()):
________________for obj in game.obj_list_vicinity(attachee.location,OLC_PC):
________________________if (is_safe_to_talk(attachee,obj)):
________________________________obj.begin_dialog(attachee,10)
________________________________game.new_sid = 0
return RUN_DEFAULT

First it checks there is no combat going on (this is common - you shouldn't speak during combat unless it is something very specific, such as Lareth's offer to surrender. To have an NPC want to initiate chit-chat with you while you are hitting them over the head with your flail is silly.) Then it checks for something in the vicinity, a la Kent. It checks again dialogue is possible, starts the dialogue at line 10, and finally changes the heartbeat from 00288 to 0 - the heartbeat has done its job so it is now scrapped (they can have a performance effect, so you only use them if you have to and try not to leave them running). This saves setting a flag as Liv did (well, a variable). Game_new.sid, as you will know from what Agetian posted about this, means the sid (script id, in this case 00288) is reset to a new one, in this case 0 (nothing). It could have been set to something else for some other critter, even as I am using the different py files for the Goons, the baddie and the Daemon. Note that even though Agetian hsa just explained this to us, Liv was using it aeons ago - for instance, she disabled this command in Shenshock's script to make him work better. Always one step ahead ;-)

So the heartbeat for dialogue is easy, isn't it. Since this fires the instant we walk onto the map, we could probably skip the combat and is_safe_to_talk bits since there has been no time for anything to happen. Its that easy.

What other proactive stuff is there? Well, there are the butt-ins. These are when an NPC starts talking even though you are in dialogue with another. Some examples are Monier and Sunom (if a female PC is chatting to Monier, Sunom butts in resenting it) or the back and forth between Lila and her hubby in my mod. Of course Elmo and Otis chatting away to each other when they first catch up is the same principle, or Riana and her sister reacting to each other, or switching from Elmo to Prince Thrommel if you find him in Elmo's presence (or numerous other NPCs who react at that moment - Zert has the floating line, "quick kill him!" or something). This actually happens a lot in the game though not necessarily a lot in play if you take my meaning.

Lets see what these calls are, ummm, called in the .py files:

py00005 Calmert to Terjon: switch_to_terjon( npc, pc ):

py00005 Calmert to Spugnoir: look_spugnoir( attachee, triggerer ):

py00017 Meleny interrupts Mona: buttin( attachee, triggerer, line):

py00084 Sunom to Monier: argue( attachee, triggerer, line):

py00091 Elmo to Otis: make_otis_talk( attachee, triggerer, line):

py00091 Elmo to Thrommel: switch_to_thrommel( attachee, triggerer):

py00100 Riana to Jenelda: together_again( attachee, triggerer ):

py00108 Bertram to Preston: buttin( attachee, triggerer, line): (butt-in, Bertram?)

py00109 Dala to Dick: make_dick_talk( attachee, triggerer, line):

py00112 Murfles to Y'Dey: buttin( attachee, triggerer, line):

py00121 Tower Sentinel to Lareth: talk_lareth( attachee, triggerer, line):

py00170 Taki to Ashrem: switch_to_ashrem(npc,pc,line,alternate_line): (that looks complicated!)

Etc. I have shown these at length in order to demonstrate there is no single way of doing this, even within a .py script (like Elmo's or Calmert's) there might be more than one thing defined. But they all look pretty similar in action.

I used Monier and Sunom's carry-on as my template by and large because it goes back and forth a few times and is nice and familiar. Lets look at it in detail as a script and see how it is applied. (From py00085).

def argue( attachee, triggerer, line):
________npc = find_npc_near(attachee,8006)
________if (npc != OBJ_HANDLE_NULL):
________________triggerer.begin_dialog(npc,line)
________________npc.turn_towards(attachee)
________________attachee.turn_towards(npc)
________else:
________________triggerer.begin_dialog(attachee,10)
________return SKIP_DEFAULT

Simple enough? Again, attachee is Monier, triggerer is the PC, and line is the line number of the dialogue you are GOING to (Sunom's).

Now: first of all, it looks for Sunom and makes sure she is there (the party might have snuck in, killed her, beat Monier unconscious then came back later. Stranger things have happened! Or they might have charmed Monier and be holding this conversation, not in the weavers' house, but out on the parapet of Burne's tower or some other obscure place.) Finding her is simple enough, she is id #8006 so we start with

npc = find_npc_near(attachee,8006)

As you remember from above this defines her presence as a variable called npc. The actual finding bit is the next line: having established this concept of 'npc' being near the attachee (Monier) it is either true or false. So the comparative line reads

if (npc != OBJ_HANDLE_NULL):

Common enough proposition: 'null' means no, basically; the search has returned a null result. That is, Sunom is NOT there. So, here we are asking if the proposition 'npc' (Sunom's presence) is NOT equal (!=) to not happening! Confused? Damn double negatives! But thats the way the game operates, you will get used to it pretty quickly.

What happens if it DOES return a null verdict - if Sunom is absent? Jump ahead to 'else', and we see that what happens is

triggerer.begin_dialog(attachee,10)

Note it is the attachee line 10, Monier's: his dlg line 10 is the copout:

{10}{I cannot talk now. Please come back later.}{I cannot talk now. Please come back later.}{}{}{}{}

Its good to have a copout line though! They usually do: you might want an NPC to butt in if you have a particular NPC follower in the party, (like Elmo with Otis) but because of the vagaries of the pathfinding, you often initiate dialogue on big maps while your NPCs are standing around far away wondering what to do. Initiating dialogue across sectors like that looks ridiculous and may well cause a crash. So NO SKIPPING THESE BITS.

Anyways, assuming Sunom IS there, the 'obj_handle_null' thing will return false (not equal, !=) and on we go. Next line:

triggerer.begin_dialog(npc,line)

It will go to the npc line number specified - this is the switching bit, because we have defined npc as Sunom.

npc.turn_towards(attachee)
attachee.turn_towards(npc)


This makes Sunom (the npc) and Monier (the attachee) towards each other, so instead of facing the PC when they yellll at each other, they face each other. Nice touch, but I bet you don't notice ;-)

Now, how is it fired in Monier's dialogue? Lets see:

{20}{[no response]}{I was not leering!}{}{}{}{}
{21}{And I...}{}{1}{}{0}{argue(npc,pc,40)}
{30}{[no response]}{Sunom, you are overreacting! I would never...}{}{}{}{}
{31}{I think...}{}{1}{}{}{argue(npc,pc,50)}
{40}{[no response]}{Sunom, mind your manners! She is a customer here!}{}{}{}{}
{41}{Now wait just a...}{}{8}{}{0}{argue(npc,pc,60)}

So between each line it goes off to Sunom, then she sends it back again with a similar script of her own (identical, actually).

The actual call of the script comes from the PC's line, because if you fired it from Monier's his would disappear before you could read it and probably they would wizz back and forth so fast you wouldn't even notice ;-) It means you have to say some little thing or have a 'continue' line in the PC's part but that can't really be helped.

It says argue(npc,pc,40). Now, watch carefully: this calls the defined thing argue( attachee, triggerer, line):. So hear 'npc' refers to the attachee, Monier, NOT to Sunom who then gets defined as 'npc' in the script seperately. Don't get them mixed up or you will be sending people to the wrong lines!

BUT the line number is still defined as '40' so that will be SUNOM's line number. It will go to line 40 of Sunom's dialogue: lets look at it.

{40}{[no response]}{Please, your eyes nearly bugged out of your skull! And you winked!}{}{}{}{}
{41}{Excuse. . .}{}{1}{}{0}{argue(npc,pc,30)}
{50}{[no response]}{Oh shut up, you hussy! Leave my husband alone!}{}{}{}{make_hate( npc, pc )}
{51}{Hussy? Who are. . .}{}{8}{}{0}{argue(npc,pc,40)}
{52}{Hussy? Me not. . .}{}{-7}{}{0}{argue(npc,pc,40)}
{60}{[no response]}{She is no customer! And don't tell me what to do, Moneir! I have half a mind to tell my father about this!}{}{}{}{}
{61}{Well, half a mind is about. . .}{}{8}{}{0}{argue(npc,pc,50)}

The 'no response' bit for males is of course because you only get this when you start a conversation with Monier and a female PC.

That's about it. Its pretty straightforward. :-)

To soldier on to the next installment, click HERE.

8 Comments:

At 6:39 pm, Anonymous Anonymous said...

Hey Nice job on the Site ! The reason I stopped at yours is to get ideas for the one I'm putting up called,Back Pain, which naturally covers back pain depression and similar areas. I liked the way yours was laid out and appreciate some of the ideas. ---Jack---

 
At 9:41 pm, Anonymous Anonymous said...

wow look @ taht homie sum jack in da box appreceates sum of ur ideas. aint u teh happyest blogger on earth now?
back pain = pain in teh ass, rite? wut a fitting naem for Jacks blog.

now bak 2 da topic. cant say i know wut this tutorial was about, but my grampa is very proactive. he eats those pills to keep him liek thta.

 
At 9:43 pm, Blogger ShiningTed said...

He should try Metamucil.

I am gonna delete damn spammers in future, in fact I have already started.

 
At 3:13 pm, Anonymous Anonymous said...

Great tutorial Ted! I actually learned some stuff I hadn't already figured out from this one. Thanks for sharing your knowledge and experience.

 
At 1:51 am, Anonymous Anonymous said...

Hey Ted, this is a cool blog! Coincidentally, I've got an interesting site about dung beetles - check it out HERE!

:)

You should post a thread at Co8 documenting your spam replies here. They're actually very entertaining.

 
At 9:49 am, Anonymous Anonymous said...

the beatles is dung band, tahts 4 sho!

 
At 11:06 am, Blogger ShiningTed said...

"Its been a hard day's night,
and I've been working like a dung beatle..."

Its a strange world where you make a blog to tell funny stories about ToEE, then post on the ToEE forum with funny stories from your blog. :-/

But don't worry about the spammers, I am gonna delete them as soon as I see them in future.

 
At 10:21 pm, Blogger ShiningTed said...

Yeah u r right, shoulda done it a while back.

 

Post a Comment

<< Home