Jump to content


Photo

How to ensure your banters always run when you want them to.


28 replies to this topic

#1 Kulyok

Kulyok
  • Members
  • 5585 posts
  • Gender:Female
  • Location:Moscow, Russia

Posted 14 March 2007 - 01:31 AM

Many players play romances. Or have BG1 NPC Project installed. And from time to time, it happens that you press a button, and romance music plays, but nothing happens. And sometimes, Imoen or Xan or Ajantis will misfire their scenery talk. "<CHARNAME>, wow, is this a diamond?" - "(sigh) No, Imoen, a bounty notice."

Normally it happens when the player issues a command at the same time the NPC is about to speak his piece. This means that the conditions for the banter are met, the banter is ready to trigger, but it does not, because it was interrupted. This also means that it will very probably trigger next time the banter engine looks for a next banter or lovetalk to fire, and will probably look out of place.

(Sometimes it also happens in the case of very bad scripting, but this is not the point of this tutorial).

So, what to do?

Imagine that your NPC is called "MyNPC", has a script file MyNPC.baf and a banter file BMyNPC.d. Both are really text files, and you can open and edit them with Notepad, Crimson Editor or Context Editor, whichever you prefer.

Now, imagine you want to fire a banter on entering the Graveyard District in Athkatla. And you do not want it to trigger anywhere else, which can well happen, if the player issues a command at the same time your NPC is about to speak.

The trick is FIRST to set the variable for the banter in MyNPC.baf, THEN keep triggering the banter until it runs(again, in MyNPC.baf), and FINALLY, in your BMyNPC.d, increase the variable by one, so the banter will never ever run again. It works wonders.

(Bioware does it from time to time, too. Unfortunately, not always).

So, the code. First, your script file, MyNPC.baf:

IF
InParty(Myself) 
See(Player1)						 // ensures that your NPC is in party and sees Player1
AreaCheck("AR0800")		   // the banter happens in the Graveyard District of Athkatla
CombatCounter(0)
!See([ENEMY])					 // the banter will not run during combat
// You could add other conditions, depending on your banter.
Global("MyGraveyardBanter","GLOBAL",0) //"0" means that the banter hasn't happened, yet
THEN
RESPONSE #100
SetGlobal("MyGraveyardBanter","GLOBAL",1) //"1" means that it is time for the banter to happen, and for the next block in the script to start working...
END

IF
InParty(Myself) 
See(Player1)						 // ensures that your NPC is in party and sees Player1
Global("MyGraveyardBanter","GLOBAL",1) //"1" means that it is time for the banter to happen, so we trigger it
THEN
RESPONSE #100
Interact(Player1)				 //  calls forth a banter from BMyNPC.d
END

And now, your banter file, BMyNPC.d:

IF ~Global("MyGraveyardBanter","GLOBAL",1)~ MyGraveyardBanterStart 
// the condition in the banter must match the variable in the script precisely, or we are in trouble.
SAY ~Hey, <CHARNAME>, that's a graveyard! Let us lie down and die!~
++ ~Cool idea.~ DO ~SetGlobal("MyGraveyardBanter","GLOBAL",2)~ + MyGraveyardBanter1.1
++ ~Nah, first we must meet Bodhi.~ DO ~SetGlobal("MyGraveyardBanter","GLOBAL",2)~ + MyGraveyardBanter1.2
++ ~I don't care either way.~ DO ~SetGlobal("MyGraveyardBanter","GLOBAL",2)~ + MyGraveyardBanter1.2
END
// you must ensure that your variable is increased in each and every reply, so the condition Global("MyGraveyardBanter","GLOBAL",1) is never met again, and the banter never runs again.

IF ~~ MyGraveyardBanter1.1
SAY ~Finally!~
IF ~~ EXIT
END

IF ~~ MyGraveyardBanter1.2
SAY ~Aw, you're no fun at all...~
IF ~~ EXIT
END

If your banter is more complicated: say, you want it to trigger two minutes after you enter the graveyard, there is one extra step: setting a timer. So, your code will look like this:

Script file, MyNPC.baf:

IF
InParty(Myself) 
AreaCheck("AR0800")			// the timer sets if the party is in the Graveyard District of Athkatla and NPC is in party, no other conditions
Global("MyGraveyardBanter","GLOBAL",0) //"0" means that the banter hasn't happened, and the timer hasn't been set, yet
THEN
RESPONSE #100
RealSetGlobalTimer("MyGraveyardTimer","GLOBAL",120) // We set a timer for two minutes
SetGlobal("MyGraveyardBanter","GLOBAL",1) //"1" now means that the timer is set. Now we're waiting until it expires...
END

IF
InParty(Myself) 
See(Player1)						 // ensures that your NPC is in party and sees Player1
AreaCheck("AR0800")			// the banter happens in the Graveyard District of Athkatla
CombatCounter(0)
!See([ENEMY])					  // the banter will not run during combat
RealGlobalTimerExpired("MyGraveyardTimer","GLOBAL") // The timer has expired, two minutes have passed
Global("MyGraveyardBanter","GLOBAL",1)  // The timer has been set
THEN
RESPONSE #100
SetGlobal("MyGraveyardBanter","GLOBAL",2)  // The timer has now expired, it is actually time to run the banter
END

IF
InParty(Myself) 
See(Player1)						 // ensures that your NPC is in party and sees Player1
Global("MyGraveyardBanter","GLOBAL",2)  // Time to trigger the banter
THEN
RESPONSE #100
Interact(Player1)				 //  calls forth a banter from BMyNPC.d
END

Banter file, BMyNPC.d:

IF ~Global("MyGraveyardBanter","GLOBAL",2)~ MyGraveyardBanterStart 
// the condition in the banter must match the variable in the script precisely, or we are in trouble.
SAY ~Hey, <CHARNAME>, that's a graveyard! I've just noticed!~
++ ~Slow, aren't you?~ DO ~SetGlobal("MyGraveyardBanter","GLOBAL",3)~ + MyGraveyardBanter1.1
++ ~Really? Oh! A graveyard!~ DO ~SetGlobal("MyGraveyardBanter","GLOBAL",3)~ + MyGraveyardBanter1.2
++ ~Huh? What?~ DO ~SetGlobal("MyGraveyardBanter","GLOBAL",3)~ + MyGraveyardBanter1.2
END
// you must ensure that your variable is increased in each and every reply, so the condition Global("MyGraveyardBanter","GLOBAL",1) is never met again, and the banter never runs again.

IF ~~ MyGraveyardBanter1.1
SAY ~So much for trying to keep the conversation going, I see.~
IF ~~ EXIT
END

IF ~~ MyGraveyardBanter1.2
SAY ~Do you think the time is right to lie down and die? Ahhh... never mind.~
IF ~~ EXIT
END

And now, the most interesting part: romances and friendships. Same thing, only instead of one variable, we've got many.

Script file, MyNPC.baf:

//This block makes sure that a romance begins, if Player1 meets the requirements: here she should be a female human
IF 
InParty(Myself)
Gender(Player1,FEMALE) 
Race(Player1,HUMAN) //the conditions for romance match
Global("MyNPCLoveTalk","GLOBAL",0) //makes sure this block runs only once: afterwards MyNPCLoveTalk sets to 1
THEN
RESPONSE #100
RealSetGlobalTimer("MyNPCRomanceTimer","GLOBAL",3000)
SetGlobal("MyNPCRomanceActive","GLOBAL",1)
SetGlobal("MyNPCLoveTalk","GLOBAL",1)
END

IF
InParty(Myself)
See(Player1) // MyNPC is in party and sees Player1
RealGlobalTimerExpired("MyRomanceTimer","GLOBAL") // the timer has expired, so it is time for a new lovetalk
Global("MyNPCRomanceActive","GLOBAL",1) // romance is still active
!AreaType(DUNGEON) // no romancing underground
CombatCounter(0)
!See([ENEMY]) // no romancing during combat
OR(4) // let us pretend we have only four lovetalks. You could have as many as you like, of course.
Global("MyNPCLoveTalk","GLOBAL",1)
Global("MyNPCLoveTalk","GLOBAL",3)
Global("MyNPCLoveTalk","GLOBAL",5)
Global("MyNPCLoveTalk","GLOBAL",7) // "1,3,5,7" means - "between lovetalks", "2,4,6,8" means "it is time for a lovetalk"
THEN
RESPONSE #100
IncrementGlobal("MyNPCLoveTalk","GLOBAL",1) // the variable increases by one, telling that it is time for a lovetalk
END

IF
InParty(Myself) 
See(Player1)						 // ensures that your NPC is in party and sees Player1
RealGlobalTimerExpired("MyRomanceTimer","GLOBAL") // this is actually important: this script block will only be fully checked when the timer has expired, so the performance wouldn't slow down
OR(4)
Global("MyNPCLoveTalk","GLOBAL",2)
Global("MyNPCLoveTalk","GLOBAL",4)
Global("MyNPCLoveTalk","GLOBAL",6)
Global("MyNPCLoveTalk","GLOBAL",8)
THEN
RESPONSE #100
Interact(Player1)
END

Banter file, BMyNPC.d: an example of a lovetalk's beginning

IF ~Global("MyNPCLoveTalk","GLOBAL",2)~ MyLovetalk1Starts
SAY ~Hey, <CHARNAME>, why don't we...~
++ ~NO!~ DO ~IncrementGlobal("MyNPCLoveTalk","GLOBAL",1) RealSetGlobalTimer("MyRomanceTimer","GLOBAL",3000)~ + MyLovetalk1.1
++ ~Ye-e-es?~ DO ~IncrementGlobal("MyNPCLoveTalk","GLOBAL",1) RealSetGlobalTimer("MyRomanceTimer","GLOBAL",3000)~ + MyLovetalk1.2
++ ~Oh, do go away.~ DO ~IncrementGlobal("MyNPCLoveTalk","GLOBAL",1) RealSetGlobalTimer("MyRomanceTimer","GLOBAL",3000)~ + MyLovetalk1.3
END

So, this is all. Your banters, friend- and lovetalks will now run smoothly, flawlessly, and without delay - if you check and re-check that the variables in your script file match the variables in your banter file perfectly.

Enjoy, and if you have any questions, feel free to ask.



20/03/2007 Hopefully the final edit. Currently tested on Xan BG2, Coran/Branwen/Shar-Teel/Xan BG1. Once again, I'd like to thank everyone who commented, participated and contributed. :)

Edited by Kulyok, 20 March 2007 - 12:38 AM.


#2 jastey

jastey
  • Gibberlings
  • 5135 posts
  • Gender:Female

Posted 15 March 2007 - 05:53 AM

Just one question: Your method does not prevent the banter to happen in other places, say "global" is set, but the dialogue didn't trigger, so the next time the engine searches for a dialogue in the dlg file the not-yet-triggered dialogue gets triggered.

This could be prevent either by adding an area check to the dialogue trigger, or by resetting the variable to zero if the area is left:

IF
InParty(Myself)
!AreaCheck("AR0800") // the banter happens in the Graveyard District of Athkatla
Global("MyGraveyardBanter","GLOBAL",1) //"1" in this case means that the banter hasn't happened, yet
THEN
RESPONSE #100
SetGlobal("MyGraveyardBanter","GLOBAL",0) //ensures the dialogue doesn't get triggered instead of another one
END

Hope this makes sense.

Edited by jastey, 15 March 2007 - 05:53 AM.


#3 Kulyok

Kulyok
  • Members
  • 5585 posts
  • Gender:Female
  • Location:Moscow, Russia

Posted 15 March 2007 - 06:18 AM

Your method does not prevent the banter to happen in other places, say "global" is set, but the dialogue didn't trigger, so the next time the engine searches for a dialogue in the dlg file the not-yet-triggered dialogue gets triggered.


That's what we were discussing with cmorgan the other night, yes. I am still in doubt whether the second block should have extra conditions, like an area check, or not: going to another area or initiating a PID will likely not happen between the two points, because the NPC script takes priority. I've been testing it, trying to get to another area or talk to Xan, and Xan's banter fired first. But, certainly, I believe that you raise a valid point.

#4 Kulyok

Kulyok
  • Members
  • 5585 posts
  • Gender:Female
  • Location:Moscow, Russia

Posted 15 March 2007 - 06:30 AM

Since it takes millisecond(s) between the first block and the second one, I believe that
1) the chance of PC doing something in this period is really, really low;
2) the chance of any conditions in block 1 changing is just as low.

So, to answer your question, I think that it is indeed wiser to strip the second block of all extra conditions, though I guess it is a purely cosmetic change(see (2) ). I'll edit the original post.

#5 jastey

jastey
  • Gibberlings
  • 5135 posts
  • Gender:Female

Posted 15 March 2007 - 06:58 AM

Since it takes millisecond(s) between the first block and the second one

It also takes as long (short) if the dialogue is triggered via e.g. "StartDialogNoSet" directly from the first block, without splitting it into two, making it also very unrealistic for the player to hinder the dialogue from triggering. :D

I just noticed that the firstly addressed problem ("This means that the conditions for the banter are met, the banter is ready to trigger, but it does not, because it was interrupted. This also means that it will very probably trigger next time the banter engine looks for a next banter or lovetalk to fire, and will probably look out of place") is not fully solved by your approach, since the variable gets set and might clock up dialogues, if the script can't run properly (I imagine an area talk not triggering because the player immediately leaves the area again. This is where the additional area check would come in.)

I do appreciate your approach very much, I just felt like pointing to where I think it's not perfect yet. ;) :)

#6 Kulyok

Kulyok
  • Members
  • 5585 posts
  • Gender:Female
  • Location:Moscow, Russia

Posted 15 March 2007 - 07:05 AM

That's right. So, that's not purely cosmetic, sorry. :)

(I do think that StartDialogueNoSet takes more time to set than a variable change, for some reason).

So... do you think we've solved the problem, now that the second block runs immediately no matter what?

#7 jastey

jastey
  • Gibberlings
  • 5135 posts
  • Gender:Female

Posted 15 March 2007 - 07:14 AM

So... do you think we've solved the problem, now that the second block runs immediately no matter what?

I notice you removed the area check from the talk trigger-script block. This would definitely mean the dialogue would trigger immediately, leaving a clean trigger. There could be the rare case of the dialogue trigger "too late" (if thinking of my example), but the common problem of "my triggered dialogues are one behind because one wasn't triggered properly) should be solved, yes. Thank you. :)

#8 cmorgan

cmorgan

    journeyman investigator

  • Gibberlings
  • 6913 posts
  • Gender:Male
  • Location:Glencoe, IL, USA

Posted 15 March 2007 - 07:28 AM

I am following this closely, and it is helping me understand;
I understand the wish to strip the second block down to minimum and place the setup block on the harder-to-match conditions, and it has one big modding advantage: if we (or a user) sets that value manually, we automatically trigger that dialogue.
That's why we set the dialogue file variable directly
SetGlobal("MyNPCLoveTalk","GLOBAL",3)
instead of using IncrementGlobal("MyNPCLoveTalk","GLOBAL",1); it "hardcodes" the dialogues in sequence.

but I am still hung up on the "emergency brake".
Area-dependent code is easier to deal with (Imoen's Tarnesh talk, for example), as are the "single shot" dialogues. Unfortunately, we can't just roll back every variable, because the series of blocks would roll the darned thing back to the beginning.

To test this, folks, I think what we will have to do is purposefully miscode and "hang" a variable.
I love deadlines. I love the whooshing noise they make as they go by. - Douglas Adams

#9 cmorgan

cmorgan

    journeyman investigator

  • Gibberlings
  • 6913 posts
  • Gender:Male
  • Location:Glencoe, IL, USA

Posted 15 March 2007 - 07:32 AM

Oh bother. I'm headed back to work: I understand now. The minimalist block will try to run immediately. This would work for area dependent talks.

Can someone (re-for-the-millionth-time-'cause-I'm-teh-STOOPID)explain if this would clear materials, or just keep hanging?
I am assuming it would clean out the hanging dialogues as a kind of auto-PID clickfest...
I love deadlines. I love the whooshing noise they make as they go by. - Douglas Adams

#10 berelinde

berelinde

    The Typo Queen

  • Gibberlings
  • 8594 posts
  • Location:New Jersey, USA

Posted 15 March 2007 - 07:37 AM

My argument is that it makes no difference if Imoen gives her diamond talk on the Coastway or if she gives it somewhere else, provided it's close enough *in time* to finding it for the player to remember. The less variables we put on that second block, the closer together those things will happen.

We've had a number of reports that Coran was missing because the players were jumping around with the console and just never strolled up to the right bridge. So we know people are using the console to travel.

Hypothetical situation: you pick up the diamond, hit pause, and then, while the game is paused, console to the FAI. Congratulations. You've just hung a dialogue, if it's got an area check. *But* if the area check is on the finding part and the talking part doesn't have one, no big deal. She'll talk when you land at the FAI, and the player should know what she's talking about, since it was just seconds ago.

If it isn't obvious from my post, I'm very much in favor of losing the area check on the second block.
Must. Write. Faster.

cmorgan: "None of us get old around here, just more proficient at doing more stuff with less braincells!"

berelinde's mods
TolkienAcrossTheWater website
TolkienAcrossTheWater Forum

#11 Kulyok

Kulyok
  • Members
  • 5585 posts
  • Gender:Female
  • Location:Moscow, Russia

Posted 15 March 2007 - 08:00 AM

My argument is that it makes no difference if Imoen gives her diamond talk on the Coastway or if she gives it somewhere else, provided it's close enough *in time* to finding it for the player to remember. The less variables we put on that second block, the closer together those things will happen.


Yeah, cmorgan, berelinde's post sums it up brilliantly. :)

So - we're losing any and all checks on the second block.

#12 cmorgan

cmorgan

    journeyman investigator

  • Gibberlings
  • 6913 posts
  • Gender:Male
  • Location:Glencoe, IL, USA

Posted 15 March 2007 - 11:54 AM

Three points:
#1, This discussion might be clipped to a secondary "comment" post like the Tutu State of the Union thread, to keep it clear of confusion (I don't have moderator stuff here, so someone else will need to do it)

#2, I know this is unlikely, but it has happened - don't we still need to stop Block#2 in the event the dialoguing actor dies or is not in area when it fires, because if the engine calls for say, Ajantis' dialogue when he is not present,
then *crash* goes Mr. Computer? Or at least the dialogue exits and does not complete the actions, casuing the dialogue to loop. We just fixed this from happening several places by adding the InMyArea checks to CHAINs in D - do we need to do this to all dialogues as well to prevent this from occurring?

A full example of this code in an operating mod so that I know I understand:

BAF>
/* Available: Garrick Carnival Comments*/
IF
InParty(Myself)
Global("X#GAFW4900","GLOBAL",0)
AreaCheck("FW4900")
CombatCounter(0)
!See([ENEMY])
InMyArea(Player1)
!StateCheck(Myself,CD_STATE_NOTVALID)
!StateCheck(Player1,CD_STATE_NOTVALID)
THEN
RESPONSE #100
SetGlobal("X#GAFW4900","GLOBAL",1)
END

/* Initiate: Garrick Carnival */
IF
InParty(Myself)
Global("X#GAFW4900","GLOBAL",1)
THEN
RESPONSE #100
StartDialogueNoSet(Player1)
END
RELATED D>
APPEND ~_GARRIJ~

/* Carnival Comments */
IF WEIGHT #-2 ~Global("X#GAFW4900","GLOBAL",1)~ THEN BEGIN GAFW4900
SAY @0
++ @1 DO ~SetGlobal("X#GAFW4900","GLOBAL",2)~ + CarnivalNo
++ @2 DO ~SetGlobal("X#GAFW4900","GLOBAL",2)~ + CarnivalYes
++ @3 DO ~SetGlobal("X#GAFW4900","GLOBAL",2)~ + CarnivalNo
END

The gain using this system is that the separation of the SDNS or Interact() or other dialogue command from the initiating block provides sets it so the dialogue *will* fire once it is set. The only "fails" here are
  • if the encounter is not activated by BCS, in which case the whole deal is skipped completely until conditions are met so
  • using this system any talks you want to remain directly dependent on a specific action need an area check or timer to stop them *activating (block 1, not *initiating, block 2)* (very rare condition - I can see few instances where a talk could be that dependent - even a DreamTalk can make sense later on) and
  • Dialogue is the driving force behind setting timers, closing variables, and in the case of chained dependencies (FriendTalk1>2>3>4 et al) also the actual method of toggling on and off the backup timer.
This does not pose problems for modders who want variable timers on their romances/friendtalks, because they can take advantage of WeiDU's ability to parse modder-created variables, where you can set up a change in the D file at install time:

In TP2>
BEGIN ~My Romance [Minimum 30 Minutes between LoveTalks]~
SUBCOMPONENT ~My Romance~
OUTER_SPRINT "MyRomance_TimeWanted" "1800"
COPY ~myMod\myDFile_tmp.D~ ~myMod\myDFile.D~
		EVALUATE_BUFFER
COMPILE ~myMod\myDFile.D~

BEGIN ~My Romance [Minimum 45 Minutes between LoveTalks]~
SUBCOMPONENT ~My Romance~
OUTER_SPRINT "MyRomance_TimeWanted" "2700"
COPY ~myMod\myDFile_tmp.D~ ~myMod\myDFile.D~
		EVALUATE_BUFFER
COMPILE ~myMod\myDFile.D~
and code in the D file
IF ~Global("MyNPCLoveTalk","GLOBAL",2)~ MyLovetalk1Starts
SAY ~Hey, <CHARNAME>, why don't we...~
++ ~NO!~ DO ~SetGlobal("MyRomanceCheck","GLOBAL",0) SetGlobal("MyNPCLoveTalk","GLOBAL",3) RealSetGlobalTimer("MyRomanceTimer","GLOBAL",%MyRomance_TimeWanted%)~ + MyLovetalk1.1
++ ~Ye-e-es?~ DO ~SetGlobal("MyRomanceCheck","GLOBAL",0) SetGlobal("MyNPCLoveTalk","GLOBAL",3) RealSetGlobalTimer("MyRomanceTimer","GLOBAL",%MyRomance_TimeWanted%)~ + MyLovetalk1.2
++ ~Oh, do go away.~ DO ~SetGlobal("MyRomanceCheck","GLOBAL",0) SetGlobal("MyNPCLoveTalk","GLOBAL",3) RealSetGlobalTimer("MyRomanceTimer","GLOBAL",%MyRomance_TimeWanted%)~ + MyLovetalk1.3
END

Edited by cmorgan, 15 March 2007 - 12:02 PM.

I love deadlines. I love the whooshing noise they make as they go by. - Douglas Adams

#13 Kulyok

Kulyok
  • Members
  • 5585 posts
  • Gender:Female
  • Location:Moscow, Russia

Posted 15 March 2007 - 11:59 AM

I guess variable timers are better off in a separate thread - I, for one, would like to have access to a quick tutorial on them, and I am definitely not qualified enough to provide one. :)

#14 Kulyok

Kulyok
  • Members
  • 5585 posts
  • Gender:Female
  • Location:Moscow, Russia

Posted 15 March 2007 - 12:19 PM

#2, I know this is unlikely, but it has happened - don't we still need to stop Block#2 in the event the dialoguing actor dies or is not in area when it fires, because if the engine calls for say, Ajantis' dialogue when he is not present,
then *crash* goes Mr. Computer? Or at least the dialogue exits and does not complete the actions, casuing the dialogue to loop. We just fixed this from happening several places by adding the InMyArea checks to CHAINs in D - do we need to do this to all dialogues as well to prevent this from occurring?


A safety catch, yes. InParty/InMyArea/CD_STATE_NOTVALID, then?

#15 Nythrun

Nythrun

    Long since out to pasture

  • Modders
  • 1761 posts
  • Gender:Female

Posted 15 March 2007 - 12:34 PM

For what it's worth (read: not so much, cause Elly is real silly) it takes around 1/20 of a second to process adding or updating a variable in .gam on my system.
"You tell lies, too."
"Not I." The witch laughed; her laughter was clear and yet unpleasant. "I used to as a child, I confess. But I soon found the truth more disconcerting."



Reply to this topic



  


0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users