Modding:Conversations: Difference between revisions

From Caves of Qud Wiki
Jump to navigation Jump to search
Line 52: Line 52:


This event is passed the sorted choices and gets one last chance to pass back additional choices / resort / or whatever it wants to do.  We suggest setting the response to a <code>new List<ConversationChoice>(Choices)</code> before mutating it as changes made to the original "Choices" here will persist throught the current conversation, but the choices used are not the Node.Choices, it is the response from this event's <code>Event.GetObjectParameter("Choices") as List<ConversationChoice></code>.
This event is passed the sorted choices and gets one last chance to pass back additional choices / resort / or whatever it wants to do.  We suggest setting the response to a <code>new List<ConversationChoice>(Choices)</code> before mutating it as changes made to the original "Choices" here will persist throught the current conversation, but the choices used are not the Node.Choices, it is the response from this event's <code>Event.GetObjectParameter("Choices") as List<ConversationChoice></code>.
==== Peek at result nodes ConversationChoice.Goto(Speaker, peekOnly: true) ====
We use this information to mark if the choice is visited or not, but each choice will have "goto" called with peekOnly to ask to resolve the node it will visit.


==== Building the message ====
==== Building the message ====

Revision as of 18:00, 4 June 2019


Conversations are loaded as templates from Conversations.xml - The base conversations can not be extended or modified through the XML, only overwritten. However there are events that will allow dynamically editing conversations as they happen, and mod authors can define their own conversation templates in their xml.

Conversation Object Parts

In order to be conversable, an object should use the ConversationScript part and attach a ConversationID which references a conversation template from Conversations.xml.

Snippet of ObjectBlueprints.xml that links Mehmet's Conversation to "JoppaMehmet"

  <object Name="Mehmet" Inherits="NPC">
    <part Name="ConversationScript" ConversationID="JoppaMehmet" />
  </object>

ConversationScript configuration

Skipping over a small section of pre-conversation / checks / etc handled inside the "ConversationScript" part (for now, documentation TODO here). ConversationScript should definitely be used because it checks all sorts of parameters about whether or not the player is capable of speaking with the object. Telepathy and other special conversation choices are considered when ConversationScript is the way into the ConversationUI.HaveConversation()

XRL.UI.ConversationUI.HaveConversation()

This is the main loop that handles the conversation start / end and setup. First, it deep copies the Conversation object as an original template, allowing mods and parts responding to the following events to mutate the nodes as much as they want.

Staring Conversation: Pick Start Node

Then, it checks in order Conversation.StartNodes looking for the first node which passes the ConversationNode.Test() and sets it as Conversation.NodesByID["Start"]


Modding and Event Hooks

Then 3 events will fire in a row - all 3 will cancel the conversation if "false" is returned.

Event: BeginConversation

Fired on: "Speaker" (not the player) Parameters: <Conversation>"Conversation" and <GameObject>"With" pointing to the player.

Event: PlayerBeginConversation

Fired on: "Player" Parameters: <Conversation>"Conversation" and <GameObject>"Speaker" pointing to the person to be talked to.

Event: ObjectTalking

Is fired on the speaker when asked to check object talking in the HaveConversation() parameters. This event would normally be checked before getting this far if you enter via ConversationScript, so before these other events, but can sometimes be fired after.

Core Conversation Loop

After the pre-conversation events above, the conversation has started, and enters at the selected "Start" node. This could have been mutated during the previous events as well, so is checked once as we begin the loop.

During each step of the loop, we call ConversationNode.Visit(Speaker, Player), to trigger any "on visit" effects in the node. We then sort the ConversationNode.Choices using the ConversationChoice.Sorter which uses the Choices's "Ordinal" property to sort, but also automatically handles any "End" nodes with ConversationChoice.END_SORT_ORDINAL. To sort an option after an End node, you can set it to END_SORT_ORDINAL + 1.

Event: ShowConversationChoices

Fired on "Speaker" Parameters:

  • <List<ConversationChoice>>Choices - can set this parameter as well as read to "extend" the choices available in a node.
  • <ConversationNode>CurrentNode - the current conversation node.
  • <ConversationNode>firstNode - the start node for conversation. (lowercase f intentional)

This event is passed the sorted choices and gets one last chance to pass back additional choices / resort / or whatever it wants to do. We suggest setting the response to a new List<ConversationChoice>(Choices) before mutating it as changes made to the original "Choices" here will persist throught the current conversation, but the choices used are not the Node.Choices, it is the response from this event's Event.GetObjectParameter("Choices") as List<ConversationChoice>.

Peek at result nodes ConversationChoice.Goto(Speaker, peekOnly: true)

We use this information to mark if the choice is visited or not, but each choice will have "goto" called with peekOnly to ask to resolve the node it will visit.


Building the message

After this event fires, the current menu will render its text. The conversation itself has a Conversation.Introduction which will prepend the node's message if it is set, and after it is displayed, the value will be reset to "" — showing the "intro" only once. We take the ConversationNode.Text and select from its random selections (~ separated) and apply Variable Replacement to it, then prepend the intro, and affix a TradeNote if enabled on the node. This is the conversation's content and the choices are displayed.

Picking a Choice

Call: ConversationChoice.Visit(Speaker, Player)

When the choice is selected, we call ConversationChoice.Visit() — it can return false to abort following through with the choice. The default implementation handles checks for things like "GiveBook".

Event: LeaveConversationNode

Fired on "Speaker" Parameters:

  • <ConversationNode>CurrentNode - the current conversation node.
  • <string>GotoID - the goto id of the node WE ARE LEAVING (not the one we are going to!!!)

Call: ConversationChoice.Goto(Speaker, peekOnly: false)

ConversationChoice.Goto(Speaker) is responsible for telling us the next node in the conversation. By default it resolves GotoID or other special GotoIDs.

Optional Event: GetConversationNode

In the default handling of ConversationChoice.Goto() it will resolve any GotoID starting with * into a ConversationNode by broadcasting a GetConversationNode on the speaker.

Parameters:

* <string> GotoID - Input the "GotoID" including a * - I.E. *waterritual
* <ConversationNode> ConversationNode - This is "output" from the event - we read this to get the result node.  Check the GivesRep part for an example of returning the water ritual.

Exit loop if ConversationNode is null now

Event: VisitConversationNode

Fired on "Speaker" Parameters:

  • <ConversationNode>CurrentNode - the new conversation node.
  • <string>GotoID - the goto id of the node

Call: ConversationNode.Enter(ConversationNode previous, GameObject speaker)

This virtual method can return even yet another new node if it wants,

Exit loop if ConversationNode is null now

Exiting the Conversation Loop

Event: AfterConversation

Fired on: Speaker No Parameters

Conversation Classes and XML properties

The xml file for conversation consists of a single <conversations> node with multiple <conversation> nodes underneath. It does not currently support extending the base game dialog via Load="Merge" or the other similar tricks used for many of the XMLs. Creating a Conversation.xml file for your mod would be done when you want to add a new specific conversation / template to be used in the game.

XRL.World.Conversation XML Node: <conversation>

property details Description
ID string / required / "key" The conversation ID used to reference this conversation template via a <part Name="ConversationScript" ConversationID="....">
C# Properties
StartNodes List<ConversationNode> C# only. Represents the children <node ID="Start"> (of which there can be multiple)
NodesByID Dictionary<string, ConversationNode> C# only. Represents the children <node> that are not "Start" - the "Start" node will be added here when conversation begins and it is chosen.

Conversation's have children <node> nodes:

XRL.World.ConversationNode - XML Node: <node>

property details Description
ID string / required / "key" The node ID used in other "GotoID" properties, etc. Can contain multiple "Start" nodes, one of which will be selected given the boolean filter parameters.
TradeNote boolean TradeNote="show" in XML will set this to true, it tells the conversation engine to display the [Press T or Tab to trade] after the conversation text.
bCloseable boolean Defaults to true, but Closable="false" in XML will set this to false, telling the conversation to not allow "escape" to get out of it.
Text string The text content of the <text> node. Uses Text Replacement strings.
Choices List<ConversationChoice> (C# only) The <choice> nodes below this node from the XML.
Visit node triggers
CompleteQuestStep string A comma separated list of QuestID~StepID that will Complete the given quest step when the node is entered (awarding XP, etc)
GiveItem string A comma separated list of BlueprintID that will give items to the player whenever this node is entered.
Filter / Test properties for Start nodes
IfWearingBlueprint string A single BlueprintID that the player must have equipped to see this start node.

XRLCore.Core.Game.Player.Body.HasObjectEquipped(IfWearingBlueprint)

IfHasBlueprint string A single BlueprintID that the player must in their Inventory to see this start node.

XRL.Core.XRLCore.Core.Game.Player.Body.GetPart<Parts.Inventory>().FireEvent(Event.New("HasBlueprint", "Blueprint", IfHasBlueprint))

IfLevelLessOrEqual string (of a number) A string representation of the level the character must be less than or equal to to see this node.

XRL.Core.XRLCore.Core.Game.Player.Body.Statistics["Level"].Value <= Convert.ToInt32(IfLevelLessOrEqual))

IfHaveQuest string A string Quest ID the player must have to get this start node.

XRL.Core.XRLCore.Core.Game.HasQuest(IfHaveQuest)

IfNotHaveQuest string A string Quest ID the player must *NOT* have to get this start node.

!XRL.Core.XRLCore.Core.Game.HasQuest(IfNotHaveQuest)

IfFinishedQuest string A string Quest ID the player must have completed get this start node.

XRL.Core.XRLCore.Core.Game.FinishedQuest(IfFinishedQuest)

IfNotFinishedQuest string A string Quest ID the player must *NOT* have completed get this start node.

!XRL.Core.XRLCore.Core.Game.FinishedQuest(IfNotFinishedQuest)

IfFinishedQuestStep string A string Quest Step ID the player must have completed get this start node.

XRL.Core.XRLCore.Core.Game.FinishedQuestStep(IfFinishedQuestStep)

IfNotFinishedQuestStep string A string Quest Step ID the player must *NOT* have completed get this start node.

!XRL.Core.XRLCore.Core.Game.FinishedQuestStep(IfNotFinishedQuestStep)

IfHaveObservation string A string Observation ID the player must have to get this start node.

Qud.API.JournalAPI.HasObservation(IfHaveObservation)

IfHaveState string A string state flag the game must have to get this start node.

XRL.Core.XRLCore.Core.Game.HasGameState(IfHaveState)

IfNotHaveState string A string state flag the game must *NOT* have to get this start node.

!XRL.Core.XRLCore.Core.Game.HasGameState(IfNotHaveState)

IfHaveItemWithID string (C# ONLY) A specific item ID that must be in the players inventory. This is a C# only property because Obejct ID doesn't exist until the object is created from the blueprint, making this option only useful on dynamic conversation nodes.
SpecialRequirement string Other special requirements.
  • LovedByConsortium : XRLCore.Core.Game.PlayerReputation.get("Consortium") < World.Reputation.lovedRep
  • IsMapNoteRevealed:MapNoteID : Qud.API.JournalAPI.IsMapOrVillageNoteRevealed( SpecialRequirement.Split(':')[1] )
  • !IsMapNoteRevealed:MapNoteID : NOT of the above
C# Methods
Copy(ConversationNode source) void Copies all the properties and makes a copy of each Choice in the Choices list
Test() virtual bool Tests the various If* and other requirements properties to see if the node should be chosen for a start node.
Visit(GameObject speaker, GameObject player) virtual void Tells the node that it was visited, is used to handle GiveItem and CompleteQuestStep and tracking "Visited" status.
Unused/Unimplemented properties
Filter string / deprecated A potentailly unused property, it seems to have no code paths calling it
GiveOneItem string / deprecated A seemingly unused string (on node), no code paths in base CoQ reference it
StartQuest string / deprecated A seemingly unused string (on node), no code paths in base CoQ reference it
RevealMapNoteId string / deprecated A seemingly unused string (on node), no code paths in base CoQ reference it
TakeItem string / deprecated A seemingly unused string (on node), no code paths in base CoQ reference it
ClearOwner string / deprecated A seemingly unused string (on node), no code paths in base CoQ reference it

XRL.World.ConversationChoice - XML Node: <choice>

property details Description
Ordinal int Determines the choice sort order.
Text string The line of text rendered for the choice in the UI.
GotoID string The conversation node that will be navigated to if this choice is selected. (Reserved: "End" "EndFight")
ParentNode ConversationNode The parent conversation node.
Consts
WATER_RITUAL_ORDINAL 980 Ordinal for water ritual nodes
TRADE_ORDINAL 990 Ordinal for trade nodes
END_SORT_ORDINAL 999999 Ordinal for end nodes
Visit node triggers
Achievement string If this choice is selected an achievement with this ID will be awarded.
StartQuest string Contains a Quest ID. This quest will start when the node is selected.
RevealMapNoteID string Contains a Map Note ID. This map note will be revealed when the node is selected.
CompleteQuestStep string A comma separated list of QuestID~StepID that will Complete the given quest step when the node is entered (awarding XP, etc)
Execute string A reference to a static function, in the format MyNamespace.MyClass:MyStaticFunction which will be called when this node is selected.
CallScript string A reference to a static function, in the format MyNamespace.MyClass.MyStaticFunction which will be called when this node is selected.
IdGift string Creates, identifies and gives to the player an object with the specified blueprint.
GiveItem string A comma separated list of BlueprintID that will give items to the player whenever this node is entered.
GiveOneItem string A comma separated list of BlueprintID. The player will be given the option to choose one of these items.
TakeItem string A comma separated list of BlueprintID which can optionall include [destroy],... and/or [byid],.... Will take all instances of an item blueprint (or items with the specified id if [byid] is included.) from the player. Items will be taken into the speakers inventory or destroyed if the [destroy] spec is included.
onAction Func<bool> (delegate) Called when a choice is selected, if false is returned the GotoID won't be followed and the current node will be re-rendered.
Filter / Test properties for Conversation Choices
IfWearingBlueprint string A single BlueprintID that the player must have equipped to see this choice.

XRLCore.Core.Game.Player.Body.HasObjectEquipped(IfWearingBlueprint)

IfHasBlueprint string A single BlueprintID that the player must in their Inventory to see this choice.

XRL.Core.XRLCore.Core.Game.Player.Body.GetPart<Parts.Inventory>().FireEvent(Event.New("HasBlueprint", "Blueprint", IfHasBlueprint))

IfHaveQuest string A string Quest ID the player must have to get this choice.

XRL.Core.XRLCore.Core.Game.HasQuest(IfHaveQuest)

IfNotHaveQuest string A string Quest ID the player must *NOT* have to get this choice.

!XRL.Core.XRLCore.Core.Game.HasQuest(IfNotHaveQuest)

IfFinishedQuest string A string Quest ID the player must have completed get this choice.

XRL.Core.XRLCore.Core.Game.FinishedQuest(IfFinishedQuest)

IfNotFinishedQuest string A string Quest ID the player must *NOT* have completed get this choice.

!XRL.Core.XRLCore.Core.Game.FinishedQuest(IfNotFinishedQuest)

IfFinishedQuestStep string A string Quest Step ID the player must have completed get this choice.

XRL.Core.XRLCore.Core.Game.FinishedQuestStep(IfFinishedQuestStep)

IfNotFinishedQuestStep string A string Quest Step ID the player must *NOT* have completed get this choice.

!XRL.Core.XRLCore.Core.Game.FinishedQuestStep(IfNotFinishedQuestStep)

IfHaveObservation string A string Observation ID the player must have to get this choice.

Qud.API.JournalAPI.HasObservation(IfHaveObservation)

IfHaveState string A string state flag the game must have to get this choice.

XRL.Core.XRLCore.Core.Game.HasGameState(IfHaveState)

IfNotHaveState string A string state flag the game must *NOT* have to get this choice.

!XRL.Core.XRLCore.Core.Game.HasGameState(IfNotHaveState)

IfHaveItemWithID string (C# ONLY) A specific item ID that must be in the players inventory. This is a C# only property because Obejct ID doesn't exist until the object is created from the blueprint, making this option only useful on dynamic conversation nodes.
IfDelegate Func<bool> An arbitrary If delegate.
SpecialRequirement string Other special requirements.
  • LovedByConsortium : XRLCore.Core.Game.PlayerReputation.get("Consortium") < World.Reputation.lovedRep
  • IsMapNoteRevealed:MapNoteID : Qud.API.JournalAPI.IsMapOrVillageNoteRevealed( SpecialRequirement.Split(':')[1] )
  • !IsMapNoteRevealed:MapNoteID : NOT of the above
C# Methods
Copy(ConversationChoice source) void Copies all the properties.
TestSpecialRequirement() virtual bool Tests the SpecialRequirement member and returns true or false.
TestHaveItemWithID() virtual bool Tests the IfHaveItemWithID member and returns true or false.
Test() virtual bool Tests the various If* and other requirements properties to see if the choice should be accepted or the current node should re-render. Called during Visit()
Goto(GameObject speaker, bool peekOnly=false) virtual ConversationNode Called on the conversation node when the choice is rendered or selected to gather information about the node that will be navigated to. Returns the node that will be navigated to. peekOnly is set to true when rendering choices, and to false when actually navigating.
GetDisplayText() virtual string Called when the conversation node is rendered, returns the final line of text that will be displayed.
CheckSpecialRequirements(GameObject Speaker, GameObject Player) virtual bool Called when the choice is selected. Perfoms a variety of hard-coded interactions.
Visit(GameObject Speaker, GameObject Player) virtual bool Called when the choice is selected. If false is returned, the GotoID will not be respected and the currently selected choice will re-render.


Unused/Unimplemented properties
ID string Currently unused, but can be referenced.
Filter string / deprecated Should not be used. Does nothing!
ClearOwner string / deprecated A seemingly unused string (on node), no code paths in base CoQ reference it
TakeBlueprint string / deprecated A seemingly unused string (on node), no code paths in base CoQ reference it
GetScriptClassName method Returns a unique script ID. Unused.


Snippet of Conversation.xml with Mehmet's script

<?xml version="1.0" encoding="utf-8"?>
<conversations>
  <conversation ID="JoppaMehmet">
    <node ID="Start" IfNotHaveQuest="What's Eating the Watervine?">
      <text>
Live and drink, =player.formalAddressTerm=. May you find shade in Joppa.</text>
      <choice GotoID="AboutJoppa1">What can you tell me about Joppa?</choice>
      <choice GotoID="LookingForWork1">I am in search of work.</choice>
      <choice GotoID="End">Live and drink.</choice>
    </node>
    <node ID="Start" IfHaveQuest="What's Eating the Watervine?" IfNotFinishedQuest="What's Eating the Watervine?">
      <text>
Live and drink, =player.formalAddressTerm=. Have you tidings from Red Rock?
      </text>
      <choice GotoID="FinishExploringRedrock1" IfHasBlueprint="Girshling Corpse">Yes. I found bits of gnawed watervine and slew a white spiderling. I carry its corpse with me.</choice>
      <choice GotoID="End">I'm working on it, =pronouns.personTerm=! Live and drink.</choice>
    </node>
    <node ID="Start" IfHaveQuest="What's Eating the Watervine?" IfFinishedQuest="What's Eating the Watervine?">
      <text>
Live and drink, =player.formalAddressTerm=. May you find shade in Joppa.</text>
      <choice GotoID="AboutJoppa2">What can you tell me about Joppa?</choice>
      <choice GotoID="End">Live and drink.</choice>
    </node>
    <node ID="FinishExploringRedrock1">
      <text>
What a hideous thing! I dread the horrors its presence portends. Bring the
corpse to Elder Irudad's hut for the Elder to examine.
      </text>
      <choice GotoID="End">As you say.</choice>
    </node>
    <node ID="LookingForWork1">
      <text>
Some critters are eating our watervine. Faarooq claims he saw one slinking around a vine patch. Ugly little thing, he says; pale white, eight legs, an ear-splitting whine.

I noticed a bit of red dirt in the watervine pool, the same we find in the soil at a nearby &amp;Ycave to the north&amp;y we call &amp;rRed Rock.&amp;y

Travel to &amp;rRed Rock&amp;y and kill as many of these critters as you can. Bring back the corpse of one, too. &amp;GElder Irudad&amp;y will reward your efforts.
</text>
      <choice GotoID="End" StartQuest="What's Eating the Watervine?">I will do as you ask.</choice>
      <choice GotoID="Start">I will perform no such peasant's task.</choice>
    </node>
    <node ID="AboutJoppa1">
      <text>
You would be wise to speak with &amp;GElder Irudad.&amp;y Look for his hut to the north.
    </text>
      <choice GotoID="LookingForWork1" IfNotFinishedQuest="What's Eating the Watervine?">I am in search of work.</choice>
      <choice GotoID="End">Live and drink.</choice>
    </node>
    <node ID="AboutJoppa2">
      <text>
You would be wise to speak with &amp;GElder Irudad.&amp;y Look for his hut to the north.
</text>
      <choice GotoID="End">Live and drink.</choice>
    </node>
  </conversation>
</conversations>