Modding:Conversations
This page is about modding. See the modding overview for an abstract on modding. |
Conversations are trees of XML loaded from Conversations.xml
and usually executed from a ConversationScript
part on a game object.
The most common elements are the Node and the Choice: a node is a piece of text spoken by the creature you're interacting with, coupled with a list of choices for the player to respond with.
This usually takes you to another node where the cycle repeats.
For extensive conversation design in mods that use a lot of conversations, some modders have recommended using a tool such as Twine to map out your conversation logic.
Adding a Conversation
In order to be conversable, an object should have a ConversationScript
part and define a ConversationID
which references a conversation template of the same ID
in Conversations.xml.
A barebones definition might look like this for a lovely snapjaw.
<!-- ObjectBlueprints.xml-->
<objects>
<object Name="Snapjaw Pal" Inherits="Snapjaw">
<part Name="ConversationScript" ConversationID="FriendlySnapjaw" />
</object>
</objects>
<!-- Conversations.xml-->
<conversations>
<conversation ID="FriendlySnapjaw">
<start ID="Welcome">
<text>ehekehe. gn. welcom.</text>
<choice Target="LibDink">Thank you.</choice>
</start>
<node ID="LibDink">
<text>hrffff... lib? dink?</text>
<text>nyeh. heh! friemd?</text>
<choice Target="End">Live and drink.</choice>
</node>
</conversation>
</conversations>
(Note that the outer conversations
tag is required.)
XML Tags
These are the basic tags supported by conversations, not including any custom elements used by parts.
XML Tag | Description |
---|---|
<conversation>
|
Single conversation template typically containing <node> and <start> elements, linked to a ConversationScript via its ID .
|
<node>
|
Collection of <text> from the Speaker's point of view, along with a range of <choice> for the Player to respond with.
|
<start>
|
Special variant of <node> that can be selected when starting a conversation. For backwards compatibility, a <node> with an ID of "Start" will behave similarly.
|
<choice>
|
Collection of <text> from the Player's point of view, commonly defines a Target <node> to navigate to if selected. The Target attribute has two special values: Start and End , which will return to the beginning of the conversation or end it, respectively. For backwards compatibility, the GotoID attribute will behave similarly to Target .
|
<text>
|
Contains a block of text to display for an element, multiple of these can be defined and randomly selected from if valid. Additional text nodes can be recursively defined within other text nodes, allowing groups of text to use the same conditions. For backwards compatibility, delimiting the text with ~ characters will behave similarly to multiple text nodes.
|
<part>
|
Reference to a C# class that inherits from IConversationPart . Any attributes defined here will be inserted into the fields & properties of the part, if possible. Anything defined as a child element of the part can be loaded with custom C# behavior. |
Merging
If multiple elements with the same ID
are defined within the same scope, a merge will occur by default where the properties of the later element overwrite the former.
If an explicit ID isn't defined, one will be created based on other attributes.
You can alter the conflict behavior of an element by setting a Load
attribute with valid values of: "Merge", "Replace", "Add", or "Remove".
<conversation ID="FriendlySnapjaw">
<node ID="SnappyNoise">
<text>gnnnnnnn.</text> <!-- ID is "Text" -->
<text>beh. mmmf.</text> <!-- ID is "Text2" -->
<text>mmnnn!</text> <!-- ID is "Text3" -->
<choice Target="LibDink">Thank you.</choice> <!-- ID is "LibDinkChoice" -->
</node>
</conversation>
<conversation ID="FriendlySnapjaw"> <!-- Will merge with above conversation -->
<node ID="SnappyNoise"> <!-- Will merge with "SnappyNoise" node -->
<text>gra! gra! gra!</text> <!-- ID is "Text" and will merge -->
<text Cardinal="3">gra! gra! gra!</text> <!-- ID is "Text3" and will merge -->
<choice Target="End">Live and drink.</choice> <!-- ID is "EndChoice" and will not merge -->
</node>
</conversation>
Inheritance
In cases where you'd like your elements to appear in multiple places, you can inherit their properties with the Inherits
attribute.
By default, every conversation inherits from BaseConversation
, which holds the definitions of common elements to all conversations like trade and the water ritual.
The attribute can also take a comma separated list, meaning you can inherit and merge the properties of multiple parent elements together.
Unlike merging, the properties of the current element have precedence over those it is inheriting from.
<conversation ID="FriendlySnapjaw">
<start ID="SnappyNoise">
<text>gnnnnnnn.</text>
<choice Target="LibDink">Thank you.</choice>
</start>
</conversation>
<conversation ID="ExcitedSnapjaw" Inherits="FriendlySnapjaw"> <!-- Inherits SnappyNoise -->
<node ID="SnappyBye">
<text>gra! gra! gra!</text>
<choice Target="End">Live and drink.</choice>
</node>
</conversation>
<conversation ID="AngryArconaut">
<start ID="Grumpy">
<text>I hate things.</text>
<choice Inherits="ExcitedSnapjaw.SnappyBye.EndChoice" /> <!-- Inherits "Live and drink." -->
</start>
</conversation>
Distribution
An alternative to explicitly inheriting elements where you'd like them repeated is distribution, where you specify directly on the element where it should propagate.
The Distribute
attribute normally takes a list of element types, but if Qualifier="ID"
is specified, a list of IDs can be provided.
Choices that are defined as children under a conversation will propagate to all start nodes by default.
<conversation ID="FriendlySnapjaw">
<start ID="SnappyHello">
<text>heeeelo!</text>
</start>
<start ID="SnappyNoise">
<text>gnnnnnnn.</text>
</start>
<choice Target="End">Live and drink.</choice> <!-- Added to both start nodes -->
<choice GiveItem="Dagger" Distribute="SnappyNoise" Qualifier="ID">It is time to grill cheese.</choice>
</conversation>
Delegates
Unique to conversations are their delegate attributes such as IfHaveQuest="What's Eating the Watervine?"
or GiveItem="Joppa Recoiler"
.
These are distinguished between two types: Predicates which control whether an element is accessible, and Actions which perform some task when the element is selected.
After the Deep Jungle update these are now for the most part agnostic as to what their parent element is.
<conversation ID="FriendlySnapjaw">
<start ID="FurFriend" IfHavePart="ThickFur"> <!-- Hidden if player doesn't have thick fur -->
<text>ooohh. pretty...</text>
<text IfReputationAtLeast="Loved">deheh. like you. hohohoho.</text> <!-- Hidden if not Loved by speaker's faction -->
<choice Target="End" IfReputationAtLeast="Loved" GiveItem="Dagger">I like you too.</choice> <!-- Gives the player a dagger if selected-->
<choice Target="End">Thank you.</choice>
</start>
</conversation>
Custom Delegates
It's possible to add your own delegates for you to use in XML by adding a [ConversationDelegate]
attribute to a static method in C#.
Depending on the return type it will either be registered as a predicate (bool) or action (void), and variants of the delegate will automatically be created.
For example the below delegate will automatically create the inversion IfNotHaveItem
, and because we set the Speaker
attribute parameter, another two (IfSpeakerHaveItem
, IfSpeakerNotHaveItem
) where Context.Target
holds the Speaker instead of the Player.
[HasConversationDelegate] // This is required on the surrounding class to reduce the search complexity.
public static class DelegateContainer
{
// A predicate that receives a DelegateContext object with our values assigned, this to protect mods from signature breaks.
[ConversationDelegate(Speaker = true)]
public static bool IfHaveItem(DelegateContext Context)
{
// Context.Value holds the quoted value from the XML attribute.
// Context.Target holds the game object.
// Context.Element holds the parent element.
return Context.Target.HasObjectInInventory(Context.Value);
}
}
Parts
For more advanced or specific logic not easily reduced to a generally accessible delegate, a custom part is preferred.
Similar to their equivalent for objects in ObjectBlueprints.xml
, parts define custom behaviour for elements within and can be attached to most any element just like delegates.
If you use a period within the part's name, it's assumed you are specifying your own namespace and won't be required to place your part within XRL.World.Conversations.Parts
. You can optionally declare a Namespace
on the root <conversations>
element, and concatenated sub-namespaces on each <conversation>
. It will try to find your part within those first before falling back to the default namespace.
<conversation ID="JoppaZealot">
<part Name="SpiceContext" />
<start ID="OrphanOfTheSalt">
<text>
Blah! Orphan of the salt! Blooh!
<part Name="TextInsert" Spoken="false" NewLines="2" Text="[Press Tab or T to open trade]" />
</text>
<choice Target="End">
<text>You intrigue me. I will go to the Six Day Stilt for no particular reason.</text>
<part Name="QuestHandler" QuestID="O Glorious Shekhinah!" Action="Start" />
</choice>
</start>
</conversation>
A very basic C# implementation of a part that adds a laugh to any text it's added to might look like this.
public class SnapjawLaugh : IConversationPart
{
public override bool WantEvent(int ID, int Propagation)
{
return base.WantEvent(ID, Propagation)
|| ID == PrepareTextEvent.ID
;
}
public override bool HandleEvent(PrepareTextEvent E)
{
E.Text.Append("\n\nehehehehe!");
return base.HandleEvent(E);
}
}
Events
Conversations have their own set of events to handle, but should be immediately familiar to anyone that has tampered with the Minimal Events of game objects.
Unlike min events which cascade down, conversation events will propagate up the element tree from where it was fired (See event bubbling). This means an event fired on a choice will first be handled by parts on the choice itself, then its parent node, last the node's conversation.
Finally the propagation is separated by perspective, Speaker and Listener (the listener being you, the player). In most cases when you attach a part that modifies the text of a node, you do not want to also modify the text of its underlying choices since those are spoken by a different entity. By default parts will register for the perspective they are placed in, but can be overriden with the Register
attribute.
<conversation ID="EventfulSnapjaw">
<part Name="SpiceContext" Register="All" /> <!-- Registers for Speaker events by default, but overrides with both -->
<start ID="TasterOfTheSalt">
<part Name="SnapjawLaugh" /> <!-- Registers for Speaker events -->
<text>mmmg. salt.</text>
<text>tasty.</text>
<choice Target="End">
<text>Salt responsibly, friend.</text>
<part Name="ReceiveItem" Blueprints="EmptyWaterskin" /> <!-- Registers for Listener events -->
</choice>
</start>
</conversation>
Tables
Below are non-exhaustive tables of existing parts, events, delegates, and additional XML attributes.
Parts
Described with their function, parameters and an example where applicable.
Name | Description |
---|---|
AddSlynthCandidate | Adds the current location as a possible sanctuary for the slynth during the quest Landing Pads.
Sanctuary: Optional explicit sanctuary name to use instead of zone name.
|
ChangeTarget | Takes any number of predicates as parameters and changes the navigation target (GetTargetElementEvent) if all predicates match.
Target: The alternate navigation target if predicates match.
|
GiveArtifact | Make the player give an artifact from their inventory to continue navigation. |
GiveReshephSecret | Share a secret from Resheph's life to gain some XP. |
IPredicatePart | An extensible abstract part that takes predicates as parameters and can check all or any for a match. ChangeTarget derives from this, for example. |
LibrarianGiveBook | Donate a book to the librarian to gain some XP. |
PaxInfectLimb | Choose a limb to infect with Klanq.
IfQuestActive: Only display this element if the quest is active and the player has no infected limb.
|
QuestHandler | Manipulates quest and step state. If a child text is provided it will replace the normal ending tag for that action, e.g. [Accept Quest] .
QuestID: The quest's ID, usually the same as its display name.
|
ReceiveItem | The player receives one or several potentially identified items.
Blueprints: A comma separated list of blueprints for the player to receive.
|
RequireReputation | Require the player's reputation with a specific faction to be at or above a certain level to continue. If the parent element is a choice, its text will be greyed out.
Faction: The ID of a faction within the current game.
|
SpiceContext | Replaces template variables in the spoken text with excerpts from history spice, e.g. replacing =spice.commonPhrases.sacred.!random= with sanctified .
|
Tag | Adds an ending tag to the parent element's text. Most parts with functionality will handle their own tags via GetChoiceTagEvent , this is mostly for informational purposes.The game makes a distinction between spoken and unspoken text for the purposes of filtering (e.g. the cawing of corvids), hence why unspoken tags should not be written together with the spoken text.
|
TakeItem | Items are taken from the player's inventory or body.
Blueprints: A comma separated list of blueprints to take.
|
TextFilter | Mutates the spoken text with a filter, e.g. the cawing of corvids or croaking of frogs.
FilterID: The ID of a filter, valid values are: Angry, Corvid, WaterBird, Fish, Frog, Leet, Lallated, Weird, and Cryptic Machine.
|
TextInsert | Appends or prepends a text that can be either spoken or unspoken to the element.
Prepend: Add the text to the beginning instead of the end.
|
Trade | Opens the trade screen when navigating to the parent element. |
VillageContext | Replaces template variables in the spoken text with excerpts from the village's history, e.g. replacing =village.sacred= with the act of procreation .Uses the faction of the speaker to find the village's historical context. GameState: Try to get the village's historical context from the faction provided in specified game state.
|
WaterRitualRandomMutation | Purchase a random mutation of specified category for reputation in the water ritual.
Category: A mutation category, valid values for the base game are: Physical, Mental.
|
Events
Source notes the deepest element that you can expect the event to propagate from. In order, the values are Conversation -> Node -> Choice -> Text.
Order notation is very approximate, as the same event will be fired multiple times on different elements during a navigation.
Name | Source | Description | Order |
---|---|---|---|
IsElementVisibleEvent | Text | Fired when determining whether an element is possibly available for rendering and selection, after any predicates defined on the element. | Before: GetTextElementEvent, After: EnteredElementEvent |
GetTextElementEvent | Text | Fired when choosing a text element for preparation and can control the chosen text. | Before: PrepareTextEvent, After: IsElementVisibleEvent |
PrepareTextEvent | Text | Fired when preparing spoken text for display after a node has been entered. This precedes the standard variable replacements like =subject.name= and allows setting a new Subject and Object. |
Before: DisplayTextEvent, After: GetTextElementEvent |
DisplayTextEvent | Choice | Fired before displaying the prepared text to screen. This is where you will typically add unspoken text like tooltips or other metagame information. |
Before: ColorTextEvent, After: PrepareTextEvent |
ColorTextEvent | Choice | Fired when coloring the display text of an element. | With: DisplayTextEvent |
GetChoiceTagEvent | Choice | Fired when selecting an ending tag to apply to the display text such as [begin trade]. | With: DisplayTextEvent |
EnteredElementEvent | Choice | Fired after an element has successfully been entered. | Before: PrepareTextEvent, After: EnterElementEvent |
EnterElementEvent | Choice | Fired as an element is being entered and can prevent navigation. | Before: EnteredElementEvent, After: LeaveElementEvent |
GetTargetElementEvent | Choice | Fired after leaving the current node and can control the navigation target. | Before: EnterElementEvent, After: LeaveElementEvent |
LeaveElementEvent | Node | Fired as an element is being left and can prevent navigation. | Before: GetTargetElementEvent, After: GetDisplayTextEvent |
LeftElementEvent | Node | Fired after an element has successfully been exited. | Before: EnteredElementEvent, After: GetTargetElementEvent |
HideElementEvent | Choice | Fired when evaluating elements to display that are hidden by special outside conditions. One such condition is the last choice selected that will be hidden if navigation was successful but did not leave the current node. |
Before: PrepareTextEvent, After: IsElementVisibleEvent |
PredicateEvent | Text | Fired by the IfCommand predicate and controls visibility similarly to IsElementVisibleEvent.
|
Before: IsElementVisibleEvent, After: EnteredElementEvent |
Delegates
An Inverse predicate can be invoked with IfNot
to negate its value.
A Speaker delegate can be invoked with IfSpeaker
/SetSpeaker
to target the speaker game object.
If both are applicable then it can also be invoked with IfNotSpeaker
.
Name | Type | Description | Inverse | Speaker |
---|---|---|---|---|
IfHaveQuest | Predicate | Continue if the player has an active or finished quest by specified ID. | Yes | No |
IfHaveActiveQuest | Predicate | Continue if the player has an active quest by specified ID. | Yes | No |
IfFinishedQuest | Predicate | Continue if the player has a finished quest by specified ID. | Yes | No |
IfFinishedQuestStep | Predicate | Takes a '~' separated value of "Quest ID~Step ID" and checks if the step is completed. | Yes | No |
IfHaveObservation | Predicate | Continue if the player knows of any gossip or lore with the specified ID. | Yes | No |
IfHaveObservationWithTag | Predicate | Continue if the player knows of any gossip or lore with the specified tag. | Yes | No |
IfHaveSultanNoteWithTag | Predicate | Continue if the player knows any history of a sultan with the specified tag. | Yes | No |
IfHaveVillageNote | Predicate | Continue if the player knows any history of a village with the specified ID. | Yes | No |
IfHaveState | Predicate | Continue if any global game state has been set by specified ID. | Yes | No |
IfTestState | Predicate | Evaluates an expression of format "ID Operator Value", for example "SlynthSettlementFaction = Joppa", comparing the global game state to the specified value. | Yes | No |
IfHaveConversationState | Predicate | Continue if any local conversation state has been set by specified ID. | Yes | No |
IfHaveText | Predicate | Continue if the specified value is present within the currently displayed node text. | Yes | No |
IfLastChoice | Predicate | Continue if the last selected choice in this conversation has the specified ID. | Yes | No |
IfCommand | Predicate | Fires an event on the element with the specified value as its command, continue if the result is set true by a consuming part. | Yes | No |
IfReputationAtLeast | Predicate | Continue if the player has the specified reputation level or higher, valid entries are "Loved", "Liked", "Indifferent", "Disliked", and "Hated". | Yes | No |
IfTime | Predicate | Takes a time tick from the Calendar, with valid ranges like "325-1000" (Harvest Dawn to Jeweled Dusk) or "1100-500" (Waxing Beetle Moon to Waxing Salt Sun). | Yes | No |
IfLedBy | Predicate | Continue if the speaker is led by the specified value, valid entries are "*" (anyone), "Player" or a blueprint ID. | Yes | No |
IfZoneID | Predicate | Continue if the current zone's ID starts with the specified value, e.g. "JoppaWorld.22.14.1.0.13" for Grit Gate, "JoppaWorld.5.2" for the entire Stilt. | Yes | No |
IfZoneName | Predicate | Continue if the current zone's name contains the specified value, e.g. "Grit Gate". | Yes | No |
IfZoneLevel | Predicate | Continue if the current zone's Z level is within the specified range, e.g. "10-15" to include the surface and 5 zones down. | Yes | No |
IfZoneTier | Predicate | Continue if the current zone's regional zone tier is within the specified range. | Yes | No |
IfZoneWorld | Predicate | Continue if the current zone is within the specified world, e.g. "JoppaWorld". | Yes | No |
IfUnderstood | Predicate | Continue if the player has seen and understands the specified blueprint. | Yes | No |
IfIn100 | Predicate | Continue if the randomly rolled value is below or equal to the specified value. | No | No |
IfGenotype | Predicate | Continue if the target is of the specified genotype. | Yes | Yes |
IfSubtype | Predicate | Continue if the target is of the specified subtype. | Yes | Yes |
IfTrueKin | Predicate | Continue if the target counts as a true kin and can implant cybernetics. | Yes | Yes |
IfMutant | Predicate | Continue if the target counts as a mutant and can gain mutations. | Yes | Yes |
IfHaveItem | Predicate | Continue if the target has an item of the specified blueprint in their inventory or equipped on their body. | Yes | Yes |
IfWearingBlueprint | Predicate | Continue if the target has an item of the specified blueprint equipped on their body. | Yes | Yes |
IfHaveBlueprint | Predicate | Continue if the target has an item of the specified blueprint in their inventory. | Yes | Yes |
IfHavePart | Predicate | Continue if the target has a part by the specified class name. Mutations are a variant of a part that this is applicable to. | Yes | Yes |
IfHaveTag | Predicate | Continue if the target's blueprint has the specified tag. | Yes | Yes |
IfHaveProperty | Predicate | Continue if the target game object has the specified property. | Yes | Yes |
IfHaveTagOrProperty | Predicate | Continue if the target has the specified tag or property. | Yes | Yes |
IfHaveLiquid | Predicate | Continue if the target has the specified liquid with an optional amount of drams, e.g. "water" for at least 1 dram of water, or "sludge:64" for 64 drams of sludge. | Yes | Yes |
IfLevelLessOrEqual | Predicate | Continue if the target is at or below the specified value. | Yes | Yes |
AwardXP | Action | Gives the specified amount of XP to the target, preceding the amount with an exclamation mark will suppress the XP popup, e.g. "!5000". | No | Yes |
FinishQuest | Action | Marks a quest by the specified ID as finished. This will not complete all the quest's steps. | No | No |
FireEvent | Action | Constructs an event from a comma and colon separated list of "EventID,Parameter1:Value1,Parameter2:Value2,Para..." then fires it on the target game object. | No | Yes |
FireSystemsEvent | Action | Constructs an event from a comma and colon separated list of "EventID,Parameter1:Value1,Parameter2:Value2,Para..." then fires it on all game systems. | No | No |
SetStringState | Action | Takes a comma separated list of "StateID,Value" and sets the global game state to the specified string value. If no value is specified the state is removed. | No | No |
SetIntState | Action | Takes a comma separated list of "StateID,Value" and sets the global game state to the specified int value. If no value is specified the state is removed. | No | No |
AddIntState | Action | Takes a comma separated list of "StateID,Value" and adds the specified value to the global game state's value. | No | No |
SetBooleanState | Action | Takes a comma separated list of "StateID,Value" and sets the global game state to the specified boolean value. If no value is specified the state is removed. | No | No |
ToggleBooleanState | Action | Toggles the boolean value of the global game state by specified ID. If no value existed prior, it will be set to true. | No | No |
SetStringProperty | Action | Takes a comma separated list of "PropertyID,Value" and sets a string property on the target game object to the specified value. If no value is specified the property is removed. | No | Yes |
SetIntProperty | Action | Takes a comma separated list of "PropertyID,Value" and sets an int property on the target game object to the specified value. If no value is specified the property is removed. | No | Yes |
SetStringConversationState | Action | Takes a comma separated list of "StateID,Value" and sets the temporary conversation state to the specified string value. If no value is specified the state is removed. | No | No |
SetIntConversationState | Action | Takes a comma separated list of "StateID,Value" and sets the temporary conversation state to the specified int value. If no value is specified the state is removed. | No | No |
SetBooleanConversationState | Action | Takes a comma separated list of "StateID,Value" and sets the temporary conversation state to the specified boolean value. If no value is specified the state is removed. | No | No |
RevealObservation | Action | Reveals a piece of gossip or lore by the specified ID. | No | No |
GiveLiquid | Action | Fills valid liquid containers of the target with specified liquid, e.g. "water" for 1 dram of water, or "sludge:64" for 64 drams of sludge. | No | Yes |
UseLiquid | Action | Empties valid liquid containers of the target with specified liquid, e.g. "water" for 1 dram of water, or "sludge:64" for 64 drams of sludge. | No | Yes |
Notify | Action | Triggers a GenericNotifyEvent on the target object and game systems.
|
No | Yes |
StartQuest | Part Generator | Adds a QuestHandler part to the parent element with the Start action and specified QuestID. | No | No |
CompleteQuestStep | Part Generator | Adds a QuestHandler part to the parent element with the Step action and specified "QuestID~StepID". | No | No |
GiveItem | Part Generator | Adds a ReceiveItem part to the parent element with the specified comma separated blueprints. | No | No |
TakeItem | Part Generator | Adds a TakeItem part to the parent element with the specified comma separated blueprints. | No | No |
Choice ordering
The following attributes can be added to the <choice>
XML tag to manipulate the order in which conversational choices are presented. By default, choices are ordered by how they appear in Conversations.xml
.
Name | Description |
---|---|
Priority | An integer priority that that specifies where a choice should appear; choices with a higher priority appear closer to the top. By default all choices are given a priority of zero unless explicitly specified, although some parts influence the priority of a choice. |
Before | Place a choice before another choice with the specified ID, e.g., Before="WaterRitualChoice" .
|
After | Place a choice after another choice with the specified ID, e.g. After="WaterRitualChoice" .
|
|