Creating non-linear quests - scripting

Hi,

I set out to create a quest the progress and ending of which would depend on multiple choices and actions. How do I go about that? Unfortunately, while I know quests from the main campaign and TMoB well enough to understand vaguely what is what in the scripts, I know nothing about writing the scripts myself - and writing scripts seems inevitable if I want to have multiple endings. Do you know any tutorials on that? On writing scripts that have several mutually exclusive states, putting choices that lead player to different actions and options, etc.?

Examples of things Iā€™d like to do:

  1. Have two factions fighting each other and give the player a choice to side with one of them or neither (like with the fire giants and Tholapsyx or inter-demon fights in Ammon Jerroā€™s Haven).
  2. A riddle: either or both like the ones in the Neverwinter Archives or, even better, like the one in the Mysteries of Westgate with player having to spell out a word.
  3. High/low influence on companions triggering certain options.
  4. High/low influence on NPCs (like with Nasher) that decides the ending of the quest and who or what the player will have to fight, depending on who she/he supported more often during the quest.
  5. Basic dialogue action options (for example - save someone or let them die).
  6. Changing appearance of a location based on the playerā€™s choices (for example player chose to side with NPC A and when she/he gets back to a certain location, someoneā€™s standing there / thereā€™s a light / an object appears, etc.)

Do you have ony pointers? What are the basic commands that Iā€™d have to use here? How do I juggle that? Would it be easier to put all different things that can influence the ending in one script or should I write a separate script for every point that creates nonlinearity and then another one that rules them all?

Anything you can think of that might help me?

Cheers,
Rimavelle

3 Likes

hi Rimavelle

tl;dr the point below is ā€¦ use variables

basics:

scripts are fired by events. Just about every type of object in the game has a set of events; a door-object has for example an OnOpen event, so if you write a script for door(s) when it/they open and slot the scriptname on doorā€™s/doorsā€™ instances or blueprints (and your compiled script is an accessible resource ofc ā€“ ie. in the module folder or its Campaign folder or in an associated .hak, etc) then when said door(s) open(s) that script fires.

[that didnā€™t come out right hehe]

note: Items donā€™t use events and rely instead on tag-based scripting ā€¦ one of the events is, eg. OnItemAcquired

Dialogs can have ActionScripts and ConditionalScripts on the dialog-nodes.

ā€¦ it shouldnā€™t be too hard to find tutorials on how to script in nwn1/2 (theyā€™re largely the same language with mostly the same game-mechanics)

google ā€œnwn lexiconā€
and @Tarot_Redhand has pdf-tutorials for scripting, i believe; search the vault for ā€œTRā€™s Basicsā€

 
moving on to what youā€™re actually talking about ā†’

To do all that stuff ā€¦ i imagine youā€™re going to be setting and getting a whole bunch of local_variables. Eg,

    // PC gives Beorn a fish
    SetLocalInt(oPC, "GaveBeornFish", TRUE);

or store the var on Beorn:

    // PC gives Beorn a fish
    SetLocalInt(oBeorn, "GotFish", TRUE);

that would probly be done in a dialogā€™s actionscript. Youā€™ll have to learn to define objects like ā€˜oPCā€™ and ā€˜oBeornā€™ ā€“ by using OBJECT_SELF when Beorn is the owner of a dialog, or GetObjectByTag("beorn") ā€“ there are many ways to define an object and such decisions are up to you.

If you want to keep a tally of the PCā€™s influence with Beorn, thatā€™d be done in another variable that is incremented/decremented based on the PCā€™s actions and dialog. And perhaps stored on the module-object via a custom function:

// changes PC's current influence with a creature or organization
void Influence(string sCreatureOrOrganization, int iInfluenceChange)
{
    object oModule = GetModule();
    int iInfluenceCurrent = GetLocalInt(oModule, sCreatureOrOrganization);
    SetLocalInt(oModule, sCreatureOrOrganization, iInfluenceCurrent + iInfluenceChange);
}

Then you can write dialogs with conditionalscripts with nodes that show or donā€™t show based on PCā€™s influence with Beorn ā€¦ etc

// checks if PC has enough influence with a creature or organization
int HasInfluence(string sCreatureOrOrganization, int iInfluenceRequired)
{
    return GetLocalInt(GetModule(), sCreatureOrOrganization) >= iInfluenceRequired;
}

 
in practice whatā€™s above is very simplistic. Multiplayer modules add complications, and Campaigns with multiple modules might need to use global_vars instead of local_vars ā€¦

 
but

variables donā€™t have to be used always. Journal entries can be set and tested against and used much like locals/globals.

And rewards might be given to the PC. For example, Beorn might grant the PC a unique staff when heā€™s given a fish ā€¦ the staff can be flagged Plot (canā€™t be sold) and Cursed (note: cursed does not mean cursed, it means that its possessor canā€™t get rid of it) ā€¦ then a subsequent script could check if PC has the staff, if he does then he gave Beorn a fish ā€¦

etc.

 
(the deep end of a very big lake)

probably handled best in an areaā€™s OnEnter script. Test against one (or more) of your variables, journal states, etc and create and destroy creatures, placeables, and effects (etc) as you desire,

Or even have two different areas, and store a waypointā€™s Tag as a string_variable that the party gets sent to when a specific event fires ( remember scripting stuff is typically about events so you just have to learn what events the executable allows us to intercept aka hook )

// set "beorn_state" in his dialog (eg.) and run this when the decisions matter for the plot
switch (GetLocalInt(GetModule(), "beorn_state"))
{
    default:
    case 0: // none
        break;

    case 1: // gave beorn a fish
        // set other vars and/or do whatever
        break;

    case 2: // fish done, gave beorn an acorn
        // set other vars and/or do whatever
        break;

    case 3: // fish and acorn done, gave beorn his staff back
        // set other vars and/or do whatever
        break;
}

again, you might want to use global_vars (or journal entries) if youā€™re designing a multi-module Campaign,

 
 
 
i hope that made a bit of general sense ā€¦

4 Likes

noting your emphasis on non-linear quests ā€¦

non-linear scripting gets very complicated fast. Handling all possible cases when only 3 variables are involved could look something like this

int a = ; // define these
int b = ;
int c = ;

if (a && b && c)
{}
else if (a && b && !c)
{}
else if (a && !b && !c)
{}
else if (!a && !b && !c)
{}
else if (!a && b && c)
{}
else if (!a && !b && c)
{}
else // ok am tired now ...

but you get the idea

 

advice (technically it can be done either way) : use individual scripts to set vars or sets of related vars ā€¦ then have larger scripts that check whatever vars are relevant and decides an outcome(s) *

* if the plot is short and simple, thereā€™s likely only one ā€˜masterā€™ checker script; but if you got a sort of round-robin variable tournament going on, then youā€™d likely want to funnel variables down through several decision-making scripts, at various points in the plot ā€¦

4 Likes

kevL_s has already answered most of your questions, but I just thought to add a bit regarding:

In my fourth module A Strange World IV - The traveler I have riddles that use the function DisplayInputBox. Itā€™s really handy and lets the player write the riddle answer. I then have a few scripts (itā€™s a bit complicated) that checks for what the player writes by using custom tokens. Hereā€™s some of the scripts I use for this:

//displaybox_show

void main()
{
object oPC = GetPCSpeaker();
int nMessageStrRef = -1;
string sMessage = "";
string sOkCB = "gui_checkanswer";
string sCancelCB = "gui_noanswer";
int bShowCancel = TRUE;
string sScreenName = "";	//string sScreenName = ""
int nOkStrRef = 181744;
string sOkString = "";
int nCancelStrRef = 181745;
string sCancelString = "";
string sDefaultString = ""; //default text in the message boxā€¦

DisplayInputBox(oPC, nMessageStrRef, sMessage, sOkCB, sCancelCB, bShowCancel, sScreenName, nOkStrRef, sOkString, nCancelStrRef, sCancelString, sDefaultString);

	SetGUIObjectHidden(oPC, "SCREEN_STRINGINPUT_MESSAGEBOX", "messageboxlb", TRUE);
	SetGUIObjectHidden(oPC, "SCREEN_STRINGINPUT_MESSAGEBOX", "messageboxlb_center", FALSE);

	sMessage = ("Your answer is:");
	SetGUIObjectText(oPC, "SCREEN_STRINGINPUT_MESSAGEBOX", "messagetext_center", -1, sMessage);


}

Then the gui_checkanswer and gui_noanswer scripts look like this:

//gui_noanswer
#include "custom_token_inc"

void main()
{

	object oPC = GetFirstPC(FALSE);
		
	SetCustomTokenEx(1002, "no answer");
	//SetLocalString(GetModule(), "CUSTOM24", sPlayerAnswer); //To be able to call this inside a script and just not just in a conversation by <CUSTOM1001>.
	//According to 4760 one should use numbers above 1000 to be safe that it's not already used by the engine, but I read in NWNlexicon that number 24 wasn't used.

	SetGlobalInt("riddleanswer", 1);
	AssignCommand(oPC, ClearAllActions());
	SetCutsceneMode(oPC);
	AssignCommand(GetNearestObjectByTag("npc_frank"), ActionStartConversation(oPC, "c_v_elfriddle"));

}
//gui_checkanswer
#include "custom_token_inc"

void main(string sPlayerAnswer)
{
	object oPC = GetFirstPC(FALSE);
		
	if (sPlayerAnswer == " ")	// <-- keep one space between quotes
	{
		SetCustomTokenEx(1002, "no answer");
	}
	
	else
	{
		SetCustomTokenEx(1002, sPlayerAnswer);
	}
	
	SetGlobalInt("riddleanswer", 1);
	AssignCommand(oPC, ClearAllActions());
	SetCutsceneMode(oPC);
	AssignCommand(GetNearestObjectByTag("npc_frank"), ActionStartConversation(oPC, "c_v_elfriddle"));	
}

The include script custom_token_inc:

// Custom Token Library script - custom_token_inc
//::////////////////////////////////////////////////////////////////
 
// void SetCustomTokenEx(int iToken,  string sValue)
// Sets the custom token identified by the number in iToken to the
// string value specified in sValue. Also duplicates the value as a
// local string stored on the module object so the GetCustomTokenEx
// function can retrieve it at any time.
void SetCustomTokenEx(int iToken, string sValue);
 
// string GetCustomTokenEx(int iToken)
// Retrieves the current value of the custom token identified by the
// number in iToken by reading the local string variable containing
// the duplicated token value which was stored on the module object
// by the SetCustomTokenEx function. If the custom token was set using
// the default SetCustomToken function instead of SetCustomTokenEx,
// this function may return an incorrect or blank string.
string GetCustomTokenEx(int iToken);
 
void SetCustomTokenEx(int iToken, string sValue)
{ 
    if (iToken <= 0) 
        return;
 
    // Change the custom token variable and duplicate it.
    SetCustomToken(iToken, sValue);
    SetLocalString(GetModule(), "CUSTOM" + IntToString(iToken), sValue);
}
 
string GetCustomTokenEx(int iToken)
{ 
    if (iToken <= 0)
        return "";
 
    // Return the content of the module variable being used to duplicate
    // the token variable.
    return GetLocalString(GetModule(), "CUSTOM" +IntToString(iToken));
}

Just to be clear: I didnā€™t write these scripts on my own, but I had kevL_s, travus, 4760 and others help me with them (I donā€™t actually recall now exactly what is my own code and what the others have done, but the main gist of it is I canā€™t take credit for these). In any case, they work really well.
Then in the dialogue you have to use something like this for it to work:
You think the word is: <CUSTOM1002>? Let's see if that is correct...

When it comes to scripting kevL_s, travus, Lance_Botelle, my brother and 4760 has taught me everything I now know.
And @kevL_s is the standout here. Heā€™s a very good teacher, IMHO. Very clear and to the point.

2 Likes

Wow, thatā€™s a helpful community for you! Thank you so much! I have to digest what you wrote, check out the tutorials, and determine what is within my capabilities and what isnā€™t - and then check my priorities and maybe lower my ambitions a bit for now :P.

Iā€™ll probably get back to you with some specifics after I try writing a script and make it actually work (successfully or not.)

Thanks again, you really made my day! I wasnā€™t expecting this much of a response :).

3 Likes

All very good comments from the wise people here but I canā€™t script so Iā€™d do the whole thing with conversations the scripts are already there in the actions tab and checks can be done in the conditions tab you just fill in the blanks.

Kill damsel in distress ga_global_int ā€œdamsel_distressā€ ā€œ1ā€
Save damsel in dstress ga_global_int ā€œdamsel_distressā€ ā€œ2ā€
Distress damsel even more and bugger off ga_global_int ā€œdamsel_distressā€ ā€œ3ā€

Then just use the conditions tab for global_ints in the conversation with the damselā€™s husband and you have all your options. Theyā€™ll all have similar names but with gc not ga.

When you start a fight with whatever faction during a conversation set another int then everybody knows what you did later when it comes to the conversation conditions.

You can also spawn ( ga_creat_obj ) / destroy ( ga_destroy ) an npc via the actions tab in another area at the same time as you have a chat, put a speak trigger nearby the spawn point and assign it to the spawned npc and when you walk past theyā€™ll start a conversation. If you did whatever doesnā€™t need them theyā€™ll never arrive and thereā€™ll be no conversation ever.

Back to the damselā€™s husband, heā€™s got a new girlfriend now standing next to him and owning the conversation trigger. He couldnā€™t care less about his wife but was just asking you to help because his mother in law was listening ( she got destroyed ga_destroy ) ! If you turn up with his wife she can now have a fight with the girlfriend.

Anything you do in a conversation can also have a journal entry then use the conditions to check what was actually done according to the journal number.

3 Likes

@Rimavelle If you want to look at those tutorials I made here are the links to the project pages. One thing to remember is that I made them for NwN. In a small number of cases there are differences to NwN 2.

TRā€™s Basics - Variables, Types and Functions (TBC)
TRā€™s Basics - Boolean Algebra
TRā€™s Basics - Decisions
TRā€™s Basics - Mostly Operators

TR

4 Likes

@Tsongo, thanks! Iā€™ll probably start with that and in the future Iā€™ll gradually try to learn some scripting.

@Tarot_Redhand, thank you! Thatā€™s exactly what I was looking for!

2 Likes

@Rimavelle

Good to hear you are delving into scripting. :slight_smile:

I have written a couple of NWN2 tutorials that may also help you move in the right direction. Itā€™s along the lines of the info you have probably already received, but in case you would like some extra ā€¦

SCRIPTING: (Covers event hooks, variables, journal entries, etc.)

https://neverwintervault.org/project/nwn2/other/scripting-tutorial-beginners-nwn2

XML: (Covers GUI interfaces, input and results.)

https://neverwintervault.org/project/nwn2/other/gui/nwn2-xml-gui-coding-beginners

If you have any questions above those already raised, just ask. :slight_smile:

EDIT: Also, sometimes looking over other peopleā€™s work can help you to understand what to do ā€¦ I have a number of factions and lots of various puzzles (including word puzzle doors) in my module, which you can take a closer look at here:-

https://neverwintervault.org/project/nwn2/module/scroll

The above tutorials start from a very basic (friendly) level and help you along at a steady pace.

You can also read about some of my own NWN2 designs for complex plots and puzzles in my blog.

2 Likes

Rimavelleā€¦ Another useful ready made one is gp_talk_object. Go to any placeable properties, look at the scripts, select on used and open it ( arrow thing ), type in gp, scroll down to the talk object one and select it.

Now follow the instructions, make it usable, plot ( so it canā€™t be wrecked ) and give it a conversation. It is now scripted to make choices, explode or whatever according to your conversation.

There will be times when you need something more than what is already available and you canā€™t pre plan or work it in via a conversation. Thatā€™s when itā€™s vital to know how to script or in my caseā€¦

Come here being annoying and asking for help, sorry all you scripting wizards, but you have saved my modules and sanity :crazy_face:

1 Like

I agree with @Tsongo that the gp_talk_object is a very useable tool. Itā€™s sometimes a lot easier to do stuff through conversations and stock scriptsā€¦but unfortunately there are times when you absolutely need to do your own scripts too. And if you want to go further down the rabbit whole you begin messing with XML scripts (which is quite confusing. If you find normal scripting hard, like I still do, XML is quite a lot worse and, at least to me, quite a bit more illogical), which @Lance_Botelle is very skilled at.

Donā€™t be a afraid to ask here on the forums. Iā€™m here almost every day and there are quite a lot of people very skilled at scripting that can help you. If itā€™s basic stuff, even I can be of help.

2 Likes

Thanks again for all responses <3.

Iā€™ve been working on my quest, using actions and conditions in my conversations and itā€™s definitely a good start! However, I ran into a problem: Iā€™m using an NWN1 style of conversation and I canā€™t freeze the PC until the conversation ends (which means that the player can still move during the conversation and stop it - and basically crash the entire quest that way.) ga_party_freeze doesnā€™t work. How do I go about that? How do I stop the PC from moving for the entire conversation?

In the dialogā€™s properties, set the ā€œMultiplayer Cutsceneā€ flag to TRUE.

2 Likes

Thank you!

1 Like

Wow travus ! I didnā€™t know that, nice fix.

2 Likes

I always use a small custom script with the function SetCutsceneMode set to TRUE. I think the flag for Multiplayer Cutscene might perhaps do the same thing. There are other functions also like freezing the legs of a charater. I had that done in my arm wrestling scene in ASW4.

EDIT: Here is the small script for preventing the PC to move without the whole cutscene thing:

void main()
{

object oPC = GetFirstPC();

effect FREEZE = EffectCutsceneImmobilize();

ApplyEffectToObject(DURATION_TYPE_PERMANENT, FREEZE, oPC);
}

As usual, if I remember correctly, I think travus helped me with this script too.

EDIT2: Then to remove the immobilization you have to run a script similar to this:

#include "nw_i0_spells"

void main()
{

object oPC = GetFirstPC();

effect FREEZE = EffectCutsceneImmobilize();

RemoveSpecificEffect(EFFECT_TYPE_CUTSCENEIMMOBILIZE, oPC);


}
1 Like

Thanks! <3

Another day, another problem - my quest is based on ā€œside with one, piss off the otherā€ scenario and letā€™s say we have Candidate1 and Candidate2. With one dialogue line I wanted to do two things:

  1. Spawn hostile Candidate1 in front of the PC to have a final battle with them.
  2. Spawn friendly Candidate2 in another area for the final talk and rewards.

Or in the other scenario, do the opposite:

  1. Spawn hostile Candidate2 in front of the PC to have a final battle with them.
  2. Spawn friendly Candidate1 in another area for the final talk and rewards.

Since I wanted them to have custom names, I created an area not linked to the story in any way. I decided Candidate1 will be c_dryad and Candidate2 - n_moire. So I created two versions of both candidates (c_dryad, faction: hostile; c_dryad2, faction: merchant - and the same with n_moire) and then used ga_create_obj in conversation, pointing the appropriate waypoints. But it didnā€™t work. Is it because said creatures already exist in another area?

Any idea how to do it, hopefully without scripting? Or maybe with a copy-paste type of scripting where I can just create 4 templates and change names and factions, leaving other things the same?

1 Like

I can look into it for you tomorrow (if noone else gets to it before then). I would probably use a custom script for this, but maybe thatā€™s just me. I find it easier when there are quite specific things you want done, to do a custom script instead of reliying on stock scripts.

2 Likes

Alright, so hereā€™s my script. It is untested. I hope it will work for you. Iā€™m sure travus or kevL_s will correct me if Iā€™ve made any mistakes. :smiley:

#include "nw_i0_generic"

void main()
{

/*
I believe that if you have an area with two instances of both the NPCs, the ones that are hostile
will probably fight the others, which might then end up with dead NPCs before things are about to take
place. Therefore I have in this script only one of each (faction Commoner or something non-hostile 
for both), and instead change their faction when someone is to attack the PC. The script assumes the 
NPCs are already in the world just in a different area. They are therefore jumped to waypoints or the 
PC. If they are NOT in the world we need to spawn them instead and that requires different scripting. 

/andgalf
*/

object oPC = GetPCSpeaker();

object oCandidate1 = GetObjectByTag("c_dryad"); //I hope these are the correct tags for the NPCs. If not, change them.
object oCandidate2 = GetObjectByTag("n_moire");

object oWaypoint = GetObjectByTag("thewaypointtag"); 
//This needs to be changed to the tag where the PC is to have final talk and reward.

//On the node of where this scene is to take place 
//do a ga_global_int and set either "c1hostilec2friend" or "c1friendc2hostile")


	if(GetGlobalInt("c1hostilec2friend"))
	{

		AssignCommand(oCandidate1, ClearAllActions());
		AssignCommand(oCandidate1, ActionJumpToObject(oPC));

		AssignCommand(oCandidate2, ClearAllActions());
		AssignCommand(oCandidate2,JumpToObject(oWaypoint));
	
		ChangeToStandardFaction(oCandidate1, STANDARD_FACTION_HOSTILE);
		SetIsTemporaryEnemy(oPC, oCandidate1);
		AssignCommand(oCandidate1, ActionAttack(oPC));
		AssignCommand(oCandidate1, DetermineCombatRound(oPC));
		

	}

	else if(GetGlobalInt("c1friendc2hostile"))
	{

		AssignCommand(oCandidate2, ClearAllActions());
		AssignCommand(oCandidate2, ActionJumpToObject(oPC));

		AssignCommand(oCandidate1, ClearAllActions());
		AssignCommand(oCandidate1,JumpToObject(oWaypoint));
	
		ChangeToStandardFaction(oCandidate2, STANDARD_FACTION_HOSTILE);
		SetIsTemporaryEnemy(oPC, oCandidate2);
		AssignCommand(oCandidate2, ActionAttack(oPC));
		AssignCommand(oCandidate2, DetermineCombatRound(oPC));


	}


}
1 Like

Just in case this is what happened, to create an object you use itā€™s template not tag ( near bottom of properties ). Make blueprints of anything you want to spawn and check itā€™s template number to be sure you get the right thing as sometimes 0 is added to it. This is shown as resref in the blueprints window ( black writing on all templates so you canā€™t miss them ) and template in the creatureā€™s properties.

You can also turn anything hostile in a conversation with ga_faction_join and put itā€™s tag in the first box and $HOSTILE in the second box so the creatures can be in the area already, no need to spawn.

So you can have the player talking with candidate 1 annoying them and on the last line of the conversation they go hostile. Or the other one in the distance candidate 2 goes hostile etc.

Iā€™m pretty sure you can change things to hostile this way in any area so the PC will walk into hell when they return somewhere, but canā€™t remember.

Hereā€™s an awesome script from Claudius33 that turns three tags hostile in any area so you can have as many creatures as you want going mad so long as they share the same three tags. It works just like a normal conversation script you fill in the blanks with the three tags.

The second one is for making anything hostile if you did something to upset them and altered the journal accordingly. eg. Youā€™ve upset the orcs now ! Put it on any creatureā€™s ā€œon perceptionā€ script. So now you have the ability to turn entire towns/ gangs etc. against the PC according to his/ her actions.

//Script by Claudius33// to set hostile the tagged creatures. Many thanks for making people in my mod angry!

void  SetHostile(string sTag)
{
	int n = 0;
	object oHostile = GetObjectByTag(sTag);
	while (GetIsObjectValid(oHostile))
	{
		ChangeToStandardFaction(oHostile, STANDARD_FACTION_HOSTILE);
		n++;
		oHostile = GetObjectByTag(sTag, n);
	}
}

void main(string sT1, string sT2="", string sT3="")
{
	if (sT1 != "") SetHostile(sT1);
	if (sT2 != "") SetHostile(sT2);
	if (sT3 != "") SetHostile(sT3);
}

//second script for turning anything hostile according to the journal. the nint< is
 because it's checking to see if you've reached the point of no return. eg. journal
 entry "you annoyed the orcs" is number 3 they will attack, if number 2 is "you have
 to speak to the orc chief" then no attacking yet. This is good because you could 
sneak away without being spotted by anything hostile.

void main()
{
object oPC=GetLastPerceived();
object oNPC=OBJECT_SELF;
if(!GetIsPC(oPC))return;
int nInt;
nInt=GetLocalInt(oPC,"NW_JOURNAL_ENTRYjournal tag");
if(nInt< whatever your number is for attack)
return;
{
ChangeToStandardFaction(oNPC,STANDARD_FACTION_HOSTILE);
AssignCommand(oNPC,ActionAttack(oPC));
}
1 Like