Introduction[]
This is intended to be a short guide that will allow the novice "skritter" to get into existing talk skrits and make some modest modifications. This will allow you to set up NPCs to use the different conversations created in Siege U: 208A - Non-Player Characters. You can choose to play different conversations based on quest states, Boolean variable states, inventory items, and more.
What You Need For This Tutorial[]
- Dungeon Siege
- Siege Editor
- Siege Editor Manual (siege_editor_manual.htm comes bundled with Siege Editor.)
- A passing familiarity with programming logic, including: what variables are, and how if/else blocks work.
What This Tutorial Assumes You Have Already Learned[]
- Siege U: 208A - Non-Player Characters: This will teach you how to create conversations for Non-Player Characters (NPCs)
Overview - Why Do Talk Skrits Exist?[]
Talk skrits tell an NPC what to do when the player talks to them. The skrit tells the NPC how to animate, which conversation to choose, and when to start talking. Since it is a skrit, we can also do more advanced things like delete items, give items to the player, change Boolean values, etc.
What A Talk Skrit Looks Like[]
This is what talk skrits look like, in brief. Don't let this scare you too much; you won't have to worry about (or change) most of what you find in a talk skrit.
/* <opening comments; usually giving the purpose of the skrit> */ <variable definitions> startup state STARTUP$ { } ////////////////////////////////////////////////////////////// // explanation of event xxx event xxx$(...) { <this code will be executed when the event happens> } ////////////////////////////////////////////////////////////// // Do more stuff state xxx$ { transition { ... } event OnEnterState$ { ... } } ////////////////////////////////////////////////////////////// // Here's the good stuff state Talk$ { transition { -> Exiting$: OnWorldMessage( WE_REQ_TALK_END ) = { <this tells the skrit what to do when the player is done talking to the NPC> } } trigger OnWorldMessage$( WE_ANIM_DONE ) { <this block is used to keep the NPC animating> } event OnEnterState$ { <here is all of the logic for choosing the conversation> TryAnim$('talk'); } } ////////////////////////////////////////////////////////////// // finish up state Exiting$ { ... }
As you can see, talk skrits are divided into different states. State Talk$ is probably the only thing that you will want to change. In fact, all changes will occur inside event OnEnterState$. This is where you can run logical checks on Boolean variables, quest states, inventory items, etc. and choose a conversation based on the results.
Here is an example of a state Talk$ block from job_talk_ibsen.skrit. This is a simple example, where the NPC has two possible conversations. If the Boolean value "talked_to_overseer" is true, the first conversation is chosen. If it is not true, then the second conversation is chosen, and the Boolean is set to true, ensuring that the first conversation will be chosen in the future.
////////////////////////////////////////////////////////////////////////////// // Here's the good stuff state Talk$ { transition { -> Exiting$: OnWorldMessage( WE_REQ_TALK_END ) = { if( m_Go$.Mind.ActorAutoFidgets ) { StartFidgetIfRequired$(); } } } trigger OnWorldMessage$( WE_ANIM_DONE ) { TryAnim$('talk'); } event OnEnterState$ { // This is the part that we really care about for our example: if ( GameAuditor.GetDb.GetBool( "talked_to_overseer" ) ) { m_Go$.GetConversation.RSSelectConversation( "conversation_ibsen_2" ); } else { GameAuditor.GetDb.SetBool( "talked_to_overseer", true ); m_Go$.GetConversation.RSSelectConversation( "conversation_ibsen" ); } m_Go$.GetConversation.RSActivateDialogue(); TryAnim$('talk'); } }
Methods For Choosing Conversations[]
The primary goal (that we are concerned with) inside of the talk skrit is to choose the appropriate conversation. This is done via two functions:
m_Go$.GetConversation.RSSelectConversation( "conversation_name" ); m_Go$.GetConversation.RSActivateDialogue();
RSSelectConversation chooses which conversation the NPC will use. Calling RSSelectConversation without supplying a conversation name will cause it to default to the first conversation specified in the [conversation] block in the actor.gas file for the NPC. When RSActivateDialogue is called, the NPC will start talking. The most common criteria for deciding what the NPC should say are Boolean variable values, quest states, and inventory items.
Boolean Values[]
Boolean variables can be named anything, and are useful for keeping track of whether or not the party has done certain actions. Boolean variables can be set via a "set bool" trigger, and are often wired with other triggers near quest-specific areas in the game. Boolean values can also be accessed with these functions:
GameAuditor.GetDb.GetBool( "variable_name" ); GameAuditor.GetDb.SetBool( "variable_name", true/false );
Running GetBool on an uninitialized boolean returns a "false" value. Otherwise it returns the value of the variable.
Here is an example from job_talk_sanctuary.skrit:
else if ( GameAuditor.GetDb.GetBool( "turn_world_red" )) { m_Go$.GetConversation.RSSelectConversation( "conversation_keeper_red" ); }
Quest States[]
Perhaps the most common use of talk skrits is to cause NPCs to say different things at different stages of a quest. See Siege U: 209 - Quests for an explanation of quests and their stages.
Victory.GetQuestOrder( "quest_name" ) Victory.IsQuestActive( "quest_name" ) Victory.IsQuestCompleted( "quest_name" )
GetQuestOrder will return the stage of the quest as an integer. IsQuestActive and IsQuestCompleted will return true or false. This is an example from job_talk_sanctuary_keeper.skrit:
else if ( Victory.IsQuestCompleted( "quest_flooded_sanctuary" )) { m_Go$.GetConversation.RSSelectConversation( "conversation_keeper_complete" ); }
This is a fictional example for GetQuestOrder:
else if (Victory.GetQuestOrder( "bobs_neat_quest" ) == 2) { m_Go$.GetConversation.RSSelectConversation( "conversation_bob_quest_part_2" ); }
Inventory Items[]
It can be very useful to check and see if the party has a particular item in their inventory. Here is the function:
UIPartyManager.DoesActivePartyHaveTemplate( "item_template_name", m_Target$ )
This function returns the GOID of the object, unless the object is not found, in which case it returns a value equal to Goid.InValidGoid. The GOID is useful if you want to directly manipulate the object. If you only care whether or not the party has the item, you can compare the result of the function to Goid.InValidGoid; if the party doesn't have the item, the comparison will return true.
Here is an example that incorporates checking for an inventory item along with a bit of variable manipulation (example taken from job_talk_ardun.skrit):
int books$ = 0; if ( Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("book_glb_lore_fedwyrr_1", m_Target$)) { books$ += 1; } if ( Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("book_glb_lore_fedwyrr_2", m_Target$)) { books$ += 1; }
In this example, the comparison is done with the "not equal" ( != ) operator, so that the result will be true if the party does have the item. (We are checking to see if the result is valid.) This particular NPC is used for a quest where the player has to collect books. This skrit counts how many of the relevant books the player has.
Custom Buttons[]
If you have a custom button available for the player to press (refer to Siege U: 208A), it will have a value associated with it. This value is the name of a Boolean variable. (Since this is true, the value associated with each button should be unique within your map. Otherwise you could have one NPC's button affect another NPC's conversations. The way to check and see if they pressed the button is to test that Boolean variable. If they pressed the button, it will have a "true" value. You will have to manually set the value back to "false" after testing the variable. If you don't do this, then every subsequent conversation with the NPC will act as though the player had just pressed the button.
Here is an example from job_talk_guard_captain_el.skrit of a check to see if a button has been pressed, and then setting the bool back to false afterwards. The crazy name for the Boolean variable (d_0x03200da1) is a naming convention that we came up with internally - when an NPC has a button for "directions", we would use "d_" and the SCID of the actor for the Boolean value, thus keeping it unique within the map. This naming convention is certainly not mandatory, but since SCIDs are unique within a map, this made it easy for us to keep the Boolean values unique.
if( GameAuditor.GetDb.GetBool( "d_0x03200da1" ) ) { m_Go$.GetConversation.RSSelectConversation( "zconversation_directions" ); GameAuditor.GetDb.SetBool( "d_0x03200da1", false ); }
Giving And Taking Items From The Player[]
It is possible to drop an item for the player using skrit. This is useful as a reward for finishing a quest. Explaining the skrit involved is beyond the scope of this document, but you can still cut and paste using an example, and inserting the template name of the item that you wish to drop for the player. The following is an example of dropping an item near the player. Just replace "template_name_of_object" with an actual template name.
SiegePos SpawnPos$; GoCloneReq cloneReq$ = MakeGoCloneReq( "template_name_of_object" ); SpawnPos$ = m_Target$.go.placement.position; cloneReq$.StartingPos = SpawnPos$; If( m_Target$.Go.Hasmind ) { if(AIQuery.FindSpotRelativeToSource(m_Target$.Go, .25, .5, 2.0, m_Target$.Go.Mind.TempPos1)) { cloneReq$.StartingPos = m_Target$.Go.Mind.TempPos1; } } cloneReq$.SnapToTerrain = true; GoDb.SCloneGo( cloneReq$ );
The following is an example of dropping gold. Just replace the "9999" at the end with the amount that you actually want to drop.
SiegePos SpawnPos$; GoCloneReq cloneReq$ = MakeGoCloneReq( "gold" ); SpawnPos$ = m_Target$.go.placement.position; cloneReq$.StartingPos = SpawnPos$; If( m_Target$.Go.Hasmind ) { if(AIQuery.FindSpotRelativeToSource(m_Target$.Go, 1, 1.5, 2.0, m_Target$.Go.Mind.TempPos1)) { cloneReq$.StartingPos = m_Target$.Go.Mind.TempPos1; } } cloneReq$.SnapToTerrain = true; Goid gold$ = GoDb.SCloneGo( cloneReq$ ); gold$.Go.Aspect.SetGoldValue(9999);
It is also possible to delete an item out of the player's inventory. This can be done when an NPC mentions that they are "taking" an item from the player. The following is an example of deleting something out of the player's inventory. Just replace "template_name_of_object" with an actual template name.
goid temp_object$ = UIPartyManager.DoesActivePartyHaveTemplate("template_name_of_object", m_Target$); temp_object$.Go.Parent.Mind.SDoJob(MakeJobReq(JAT_DROP, JQ_ACTION, QP_FRONT, AO_REFLEX, temp_object$)); PostWorldMessage(WE_REQ_DELETE, temp_object$, temp_object$, .1);
Advanced Examples[]
Here is an example (taken from job_talk_guard_captain_cr.skrit):
else if ( (Victory.GetQuestOrder( "quest_zabar_hovart" ) == 4 ) && (Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("stone_glb_gavel_01", m_Target$)) )
For the first check to return true, the player would have to be on step four of the quest named "quest_zabar_hovart". For the second check to be true, the party would have to have the item with template name "stone_glb_gavel_01" in their inventory. The logical "and" operator ( && ) is used here to make sure that both comparisons are true before moving on.
This example is from job_talk_sanctuary.skrit:
if ( ( Victory.IsQuestCompleted( "quest_flooded_sanctuary" )) && ( !GameAuditor.GetDb.GetBool( "sanctuary_keeper_gave_reward" )) )
This will return true only when the quest is completed, and the reward has not been given. The first line is fairly simple; it will return true when the quest is completed. However, the second line has a "!" in front of it, which is the logical "not" in skrit (just like C). Thus, the second comparison would return true if the bool is not true. The logical "and" is used to tie these comparisons together, just as in the example above.
This final example is a series of checks found in job_talk_kelti.skrit. This is one of the longer talk skrits, and serves as an example of what kind of checking is possible within a skrit. For completeness, the entire "event OnEnterState" block is included here. This example will have comments throughout, explaining what is going on:
event OnEnterState$ { #only( game ) [[ if( GameAuditor.GetDb.GetBool( "d_0x032007b5" ) ) { m_Go$.GetConversation.RSSelectConversation( "zconversation_directions" ); GameAuditor.GetDb.SetBool( "d_0x032007b5", false ); }
This is checking a Boolean variable, then if it's true, setting the bool back to false after choosing the conversation.
else if ( GameAuditor.GetDb.GetBool( "turn_world_red" ) ) { m_Go$.GetConversation.RSSelectConversation( "conversation_kelti_red" ); }
Again, checking a Boolean variable:
else if ( Victory.IsQuestCompleted( "quest_townstones" )) { m_Go$.GetConversation.RSSelectConversation( "conversation_hub_complete" ); }
This checks to see if a quest is done:
else if ((!Victory.IsQuestActive("quest_elddim")) && (!Victory.IsQuestActive("quest_townstones")) && (Goid.InValidGoid == UIPartyManager.DoesActivePartyHaveTemplate("stone_glb_elddim_01", m_Target$))) { m_Go$.GetConversation.RSSelectConversation( "conversation_kelti" ); }
This gets a bit more complicated. It is checking to see if both quests are not active (note the "!" operators). It is also checking to see if the party does not have a specific item (it is comparing against Goid.InValidGoid).
else if ((Victory.IsQuestActive("quest_elddim")) && (Goid.InValidGoid == UIPartyManager.DoesActivePartyHaveTemplate("stone_glb_elddim_01", m_Target$))) { m_Go$.GetConversation.RSSelectConversation( "conversation_kelti_local" ); }
This is checking to see if the quest is active, and the party does not have a specific item:
else if ( ( Victory.IsQuestActive( "quest_elddim" )) && (Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("stone_glb_elddim_01", m_Target$)) && (Goid.InValidGoid == UIPartyManager.DoesActivePartyHaveTemplate("ring_utraean", m_Target$))) {
Now the skrit is checking to see if the quest is active, the party does have "stone_glb_elddim_01", and the party does not have "ring_utraean".
m_Go$.GetConversation.RSSelectConversation( "conversation_kelti_quest" ); if ( !GameAuditor.GetDb.GetBool( "kelti_conversation_quest_one" ) ) { GameAuditor.GetDb.SetBool( "kelti_conversation_quest_one", true ); }
Checking to see if a bool is false (note the "!"):
else { if ( !GameAuditor.GetDb.GetBool( "kelti_conversation_quest_two" ) ) {
Again, checking to see if a bool is false:
GameAuditor.GetDb.SetBool( "kelti_conversation_quest_two", true ); // The second time that this conversation gets played, // this item is supposed to be given to the player SiegePos SpawnPos$; GoCloneReq cloneReq$ = MakeGoCloneReq( "gen_ring_utraean" ); SpawnPos$ = m_Target$.go.placement.position; cloneReq$.StartingPos = SpawnPos$; if( m_Target$.Go.Hasmind ) { if(AIQuery.FindSpotRelativeToSource(m_Target$.Go, .25, .5, 2.0, m_Target$.Go.Mind.TempPos1)) { cloneReq$.StartingPos = m_Target$.Go.Mind.TempPos1; } } cloneReq$.SnapToTerrain = true; GoDb.SCloneGo( cloneReq$ ); } } } else if ( Victory.IsQuestActive( "goto_crystwind" ) || ( Victory.IsQuestCompleted( "goto_crystwind" ) && ( Victory.IsQuestActive( "goto_elddim" ) || Victory.IsQuestActive( "goto_elddimb" )))) {
This is the first time we see the "||" operator used. It is a logical "or", just like in C. So this check will be true if the "goto_crystwind" quest is active. It will also be true if "goto_crystwind" is completed and either "goto_elddim" or "goto_elddimb" is active. When dealing with several comparisons like this, it can be helpful to insert white space and parenthesis to make it more readable.
if( Victory.IsQuestActive( "goto_elddim" ) ) { Victory.RSCompletedQuest( "goto_elddim", m_Target$ ); }
Checking to see if a quest is active:
if( Victory.IsQuestActive( "goto_elddimb" ) ) { Victory.RSCompletedQuest( "goto_elddimb", m_Target$ ); }
Checking to see if a quest is active:
m_Go$.GetConversation.RSSelectConversation( "conversation_kelti_goto" ); give_item$( m_Target$ ); } else if ( ( !Victory.IsQuestActive( "quest_elddim" )) && (Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("stone_glb_elddim_01", m_Target$)) && (Goid.InValidGoid == UIPartyManager.DoesActivePartyHaveTemplate("ring_utraean", m_Target$))) {
This is checking three things: "quest_elddim" is not active, the party does have "stone_glb_elddim", and the party does not have "ring_utraean".
m_Go$.GetConversation.RSSelectConversation( "conversation_kelti_stone" ); if ( !GameAuditor.GetDb.GetBool( "kelti_conversation_quest_one" ) ) { GameAuditor.GetDb.SetBool( "kelti_conversation_quest_one", true ); } else if ( !GameAuditor.GetDb.GetBool( "kelti_conversation_quest_two" ) ) { GameAuditor.GetDb.SetBool( "kelti_conversation_quest_two", true ); } else if ( !GameAuditor.GetDb.GetBool( "kelti_conversation_quest_three" ) ) { GameAuditor.GetDb.SetBool( "kelti_conversation_quest_three", true ); } else if ( !GameAuditor.GetDb.GetBool( "kelti_conversation_quest_four" ) ) { GameAuditor.GetDb.SetBool( "kelti_conversation_quest_four", true ); // The fourth time that this conversation gets played, // this item is supposed to be given to the player
Here are several checks of Boolean variables, all with the "!" operator, so they are checking to see if the variable is false.
SiegePos SpawnPos$; GoCloneReq cloneReq$ = MakeGoCloneReq( "gen_ring_utraean" ); SpawnPos$ = m_Target$.go.placement.position; cloneReq$.StartingPos = SpawnPos$; if( m_Target$.Go.Hasmind ) { if(AIQuery.FindSpotRelativeToSource(m_Target$.Go, .25, .5, 2.0, m_Target$.Go.Mind.TempPos1)) { cloneReq$.StartingPos = m_Target$.Go.Mind.TempPos1; } } cloneReq$.SnapToTerrain = true; GoDb.SCloneGo( cloneReq$ ); } else { m_Go$.GetConversation.RSSelectConversation("conversation_kelti_goto"); } } else if ( ( !Victory.IsQuestActive( "quest_elddim" )) && (Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("stone_glb_elddim_01", m_Target$)) && (Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("ring_utraean", m_Target$))) { m_Go$.GetConversation.RSSelectConversation( "conversation_kelti_stone_ring" ); }
This checks to see if the quest is not active, and the party does have both items:
else if ( ( Victory.IsQuestActive( "quest_elddim" )) && (Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("stone_glb_elddim_01", m_Target$)) && (Goid.InValidGoid != UIPartyManager.DoesActivePartyHaveTemplate("ring_utraean", m_Target$ ))) { Victory.RSCompletedQuest( "quest_elddim", m_Target$ ); // finish quest, since they actually have the ring already m_Go$.GetConversation.RSSelectConversation( "conversation_kelti_stone_ring" ); }
This is checking to see if the quest is active, and the party does have both items:
else { if (!Victory.IsQuestActive("quest_elddim")) {report.generic("No quest_elddim for you!\n");} if (Victory.IsQuestActive("quest_elddim")) {report.generic("You have quest_elddim!\n"); } Report.Error( "TELL SARAH: Kelti doesn't have a conversation set up for this condition! Sarah will need to know what quests you have, what states they are in, and what quest items you have (or don't have)." ); m_Go$.GetConversation.RSSelectConversation("conversation_kelti"); }
This "else" block is only here for legacy reasons. If you follow the logic above, you will see that this block should never even be reached. However, during development (when not all logic is in place), blocks like this can be very handy to print out error messages that will only be seen if there is a problem.
TryAnim$('talk'); m_Go$.GetConversation.RSActivateDialogue(); ]] }
Conclusion[]
We have covered what talk skrits are and how they function. We have covered several methods of playing different conversations (Boolean variables, quests states, and inventory items), custom conversation buttons, giving/taking items from the player, and some advanced functionality. All of these will help you with adding in functional conversations to your mod and assist you with setting up quests in Siege U: 209 - Quests.
Appendix A: Tips[]
Most of the syntax should look familiar to those who have been exposed to C++. An exception that you may have to worry about is this block:
#only( game ) [[ ... ]]
This block is used to insulate code from Siege Editor. Since the game engine is actually running in the editor, skrits get loaded and interpreted when you run the editor. Since it is the editor, however, not all functions are accessible, so this just tells the editor to ignore that block (the game will still read it like it normally does). If you are using functions that look inside the target's inventory, or you are getting errors about invalid functions when you load the editor, try insulating the offending code in this block.
Skrit allows you to do some amazing things. Take a look around the *.skrit files and if you see something you like, try copying and pasting it into your own skrit file.