Modding:Entity Behavior conversable
This entity behavior lets the player right click the entity to open a conversation dialog. The conversation prompts can be customized based on the players previous conversations. A few actions, such as giving the player items, can be triggered by the conversation.
Overview
The conversation is divided into components, which are basically conversation prompts. Components are identified by a code, like most VS assets. Each component decides which component to execute next, and whether to do that immediately or to wait for a response from the player. When the player responds (by clicking a response link in the dialog), the dialog is cleared and the new components append their text to the dialog.
Typically multiple components are shown in the dialog. The welcome dialog shown on the side is the result of:
- "testhasmeet" - an invisible condition component, which jumped to the "firstmeet" component.
- "firstmeet" - a talk component, which added the "Haven't seen you around before. You just wake up?" text. Because the talk component is owned by the trader, it immediately jumped to the next component.
- "firstmeetresponse" - another talk component, which added the potential response links to the dialog. Because this talk component is owned by the player, it stops the execution here. When the player clicks one of the response links, the dialog will be cleared, and execution will start at the component specified in the response definition.
The component configuration is stored in an asset in the config/dialog/ folder. The example conversation above comes from survival/config/dialogue/trader.json. This is the start of it:
{
components: [
{
code: "testhasmet",
owner: "trader",
type: "condition",
variable: "entity.hasmet",
isNotValue: "true",
thenJumpTo: "firstmeet",
elseJumpTo: "welcomeback"
},
{
code: "firstmeet",
owner: "trader",
type: "talk",
setVariables: { "entity.hasmet": "true" },
text: [
{ value: "Haven't seen you around before. You just wake up?" },
],
},
{
code: "firstmeetresponse",
owner: "player",
type: "talk",
text: [
{ value: "I don't know what you mean.", jumpTo: "daisies" },
{ value: "I think so.", jumpTo: "morning" },
{ value: "I might still be dreaming.", jumpTo: "dream" },
{ value: "Damn it's good to see a friendly face.", jumpTo: "bold" },
],
},
...
The behavior specifies which component configuration to use through the dialogue property, and must be specified on both the client and server side. trader-buildmaterials.json installs the behavior like this:
client: {
...
behaviors: [
...
{ code: "conversable", dialogue: "config/dialogue/trader" }
],
...
},
server: {
...
behaviors: [
...
{ code: "conversable", dialogue: "config/dialogue/trader" }
],
...
},
In addition to running the conversation dialog, when the player right clicks the NPC, this behavior schedules a taskAI to have the NPC walk to the player and look at them. It also plays the "welcome" animation on the NPC.
Conditions
Conditions are used to conditionally change the control flow of the conversation. A condition checks whether a variable has a specified value. Variable values are strings. Variable names are also strings, but they must be prefixed with the scope name followed by a dot. For example "entity.hasmet" is the "hasmet" variable in the "entity" scope. There are 3 variable scopes for the conversation system.
- "global"
- variables that are shared by all players and accessible through all NPC.
- "player"
- variables that are specific to each player, but can be accessed through any NPC.
- "entity"
- variables that are specific to each player NPC combination. Note that internally the entity variables are first indexed by only the NPC entity id to get a dictionary for that NPC, indexed by variable name. However, the variable name is internally prefixed with playeruid + "-", so that each player-NPC combination gets its own copy of the variable.
The example dialog stores "hasmet" in the "entity" scope. This way each player can independently meet each NPC. If the "player" scope were used instead, then after the player met any NPC, they would all recognize the player. If the "global" scope were used instead, then after any player meets any NPC, all NPCs would claim to know all players.
In the base game, conversation variables can only be set from conversation components. Specifically each component supports a setVariables property, which is a dictionary from variable name to variable value. After the component executes, all variables in that dictionary are set to the specified values. For example, this component sets "entity.hasmet" to "true" when it executes.
{
code: "firstmeet",
owner: "trader",
type: "talk",
setVariables: { "entity.hasmet": "true" },
text: [
{ value: "Haven't seen you around before. You just wake up?" },
],
},
DlgConditionComponent uses a condition to determine which component to jump to next. The following example checks the value of "entity.hasmet". If it is anything other "true" (including if the variable is unset), then it jumps to "firstmeet". If it is "true", then it jumps to "welcomeback".
{
code: "testhasmet",
owner: "trader",
type: "condition",
variable: "entity.hasmet",
isNotValue: "true",
thenJumpTo: "firstmeet",
elseJumpTo: "welcomeback"
},
Alternatively, the isValue property can be used in DlgConditionComponent. The following example does the same thing as the previous one, but uses isValue instead of isNotValue.
{
code: "testhasmet",
owner: "trader",
type: "condition",
variable: "entity.hasmet",
isValue: "true",
thenJumpTo: "welcomeback",
elseJumpTo: "firstmeet"
},
For DlgTalkComponent, conditions can be attached to the player responses, so that the player is only shown links for the responses with passing conditions. In the below example from treasurehunter.json, the "Got anything to trade" response is always shown. The "dialogue-specialruin" and "dialogue-buyelk" responses are only shown if the "entity.bronzereceived" variable is set to "true" (which happens in a different component that is not shown).
{
code: "main",
owner: "player",
type: "talk",
text: [
{ value: "Got anything to trade, {npcname}?", jumpTo: "opentrade" },
...
{ value: "dialogue-specialruin", jumpTo: "maprepeat", conditions: [{ variable: "entity.bronzereceived", isValue: "true" }] },
{ value: "dialogue-buyelk", jumpTo: "buyelk", conditions: [{ variable: "entity.bronzereceived", isValue: "true" }] },
]
If multiple conditions are attached to a reponse, then all of them must be true to show the response. The isNotValue property can be used instead of isValue to only show the response if the variable does not match the given value.
In addition to the real variables, the DlgTalkComponent supports the following pseudo-variables. DlgConditionComponent does not support pseudo-variables.
- "player.inventory"
- true if the player has the specified item in their inventory. The item is specified as a json item stack in the variable value, for example:
{ variable: "player.inventory", isValue: "{type: 'item', code: 'pickaxe-tinbronze'}" }
- "player.heldstack"
- true if the player has the specified item in their active hotbar slot. The item is specified as a json item stack in the variable value, for example:
{ variable: "player.heldstack", isValue: "{type: 'item', code: 'pickaxe-tinbronze'}" }
. Alternatively the special value "damagedtool" matches any tool that is at less than max durability. Presumably "damagedtool" was built for the currently unused "repairheld" trigger.
For code modes, new pseudo-variables cannot be added without resorting to Harmony patches. However, mods can add code to the OnDialogueControllerInit delegate to set real variables right before the conversation is started.
Note that the amount of trust that the conversation system puts in the client is unusual for VS. The rest of VS has a server authoritative architecture. The DlgConditionComponents are evaluated on both the client and server, if there is somehow a mismatch, then they become desynced. The DlgTalkComponent response prompt conditions are only evaluated on the client, and the server trusts whichever prompt the client says the player chose (through the SelectAnswerPacketId network message).
The conditions read conversation variables that other components set. The initial value of these variables are sent when the player connects to the server (through a message on the "dialogue" network channel). After that, the client and server must evaluate the conversations the same way for the variables to stay in sync.
Components
These are the properties supported by every type of component.
- code: (string, default null)
- identifier for this component. Other components use this to jump to the component.
- type: (string, default "")
- type of component. There are only 3 types of components, with multiple alias to them.
- sound: (asset code, default null)
- sound to play after this component executes.
- setVariables: (string to string dictionary, default null)
- variables to set when this component starts executing. See the conditions section for an explanation of variables. The key is the variable name, and the value is the variable value. The variable name should start with "global.", "player.", or "entity.".
- trigger: (string, default null)
- trigger name to execute when the component starts executing. See the trigger section for an explanation of triggers.
- triggerData: (json object, default null)
- data to pass to the trigger. The format of this data depends on which trigger is used.
The type of component is specified through the type field. The DlgGenericComponent has multiple aliases. So most of this document uses the class name to unambiguously identify the components. The following is the mapping from type string to component class.
type | class |
---|---|
"talk" | DlgTalkComponent |
"condition" | DlgConditionComponent |
"" | DlgGenericComponent |
"trigger" | |
"setvariables" | |
"jump" |
DlgTalkComponent
Required properties:
- owner: (string, default null)
- if this property is set to "player", then this component produces a list of response links that the player can choose from. If the owner property is any other string, then that represents text that the NPC says. All strings other than "player" have the same behavior, but "trader" is commonly used.
Properties for player owner mode:
- text: (DialogTextElement array, default null)
- each element is a response that the player can choose from. Each element can have the following fields.
- value: (string)
- a lang code for the response text. The language files should have a corresponding entry with the same name.
- conditions: (ConditionElement array, default null)
- if this array is non-empty, then all of these conditions must be met for the response link to be shown. If the conditions are not met, then the player cannot select the response, because the response link is not shown. See the conditions section for more details.
- jumpTo: (string, default null)
- the component code to jump to if the player selects this response.
Properties for trader owner mode:
- text: (DialogTextElement array, default null)
- each element is a response that the NPC can say. Typically only one response is specified. If multiple are specified, then each time the component is executed, one response is randomly selected out of those whose conditions are met.
- value: (string)
- a lang code for the response text. The language files should have a corresponding entry with the same name.
- conditions: (ConditionElement array, default null)
- if this array is non-empty, then all of these conditions must be met for the response to possibly be shown. See the conditions section of this page for more details.
- jumpTo: (string, default "next")
- the code of the component to immediately execute next. "next" is a special value that means the following component listed in the components array in the json file.
The value field has a code for a string. That code is looked up in the player's language file. DlgTalkComponent does a search and replace on that string of the following substrings:
search | replace |
---|---|
"{characterclass}" | The character's class, in the player's local language |
"{playername}" | The player's name |
"{npcname}" | The NPC's name |
DlgConditionComponent
This component only changes which component is run next. DlgConditionComponent itself does not append any text to the dialgo.
This allows the author to change the conversation flow based on the value of a variable. It is like an if statement in a programming language. If the condition is true, then it immediately executes the "thenJumpTo" component next. Otherwise it immediately executes the "elseJumpTo" component next.
See the condition section for more details about how to code a condition with the variable, isValue, and isNotValue properties.
Properties:
- variable: (string, default null)
- variable to read for the condition
- isValue: (string, default null)
- check whether the variable has the specified value. Exactly one of isValue or isNotValue must be specified.
- isNotValue: (string, default null)
- check whether the variable has any value (including unset) other than the specified value. Exactly one of isValue or isNotValue must be specified.
- thenJumpTo: (component code, default null)
- run this component next if the condition is true.
- elseJumpTo: (component code, default null)
- run this component next if the condition is false.
DlgGenericComponent
Depending on the situation this component is used to set variables, run triggers, or jump to another component. Often instead of using a DlgGenericComponent, whatever it is doing can be folded into the previous component in the conversation. For instance, if a DlgGenericComponent were used to set a variable after a trader DlgTalkComponent, then they could be merged together by moving setVariables to the DlgTalkComponent.
Properties:
- jumpTo: (string, default "next")
- the code of the component to immediately execute next. "next" is a special value that means the following component listed in the components array in the json file.
Triggers
Triggers are generic mechanism to let the conversation system perform actions beyond showing text and setting variables. Triggers are identified by a string, and they take a json object as triggerData. These triggers are available for all entities that use the converse behavior:
- "playanimation": (AnimationMetaData, default null)
- play the animation on the entity with the behavior (not on the player conversing with the entity).
- "giveitemstack": (JsonItemStack, default null)
- give the player the specified item. If the player's inventory is full, then drop the item on the ground.
- "spawnentity": (DlgSpawnEntityConfig, default null)
- spawn the specified entity within the specified range of the player. Note that it will only spawn the entity at the surface level, even if the player is somehow talking to a trader underground.
- codes: (array of WeighedCode, default null)
- array of possible entities to spawn. Typically only one is listed. If multiple are listed, then one is randomly picked based on the weights of each entity.
- code: (entity code, default null)
- the entity to spawn if this entry is chosen.
- weight: (float, default 1)
- the relative chance to pick this entity.
- range: (float, default 0)
- spawn the entity up to this many blocks away from the player. If too small of a value is chosen, then spawning the entity may fail if no open spaces were found in the specified range.
- giveStacks: (array JsonItemStack, default null)
- give the spawned entity these items. For example, this is used to give the spawned elk a saddle.
- "takefrominventory": (JsonItemStack, default null)
- take the specified item from the player's inventory. Triggers cannot fail. So nothing will happen if the player does not have the specified item in their inventory. This should be preceded by a condition that checks the "player.inventory" pseudo-variable.
- "repairheld": (none)
- repair the item in the player's active hotbar slot.
- "attack": (dictionary, default {"type": "BluntAttack", "damage": 0})
- deal the specified kind of damage to the player. This bypasses the taskai system. So the NPC will not continue attacking the player after dealing the damage.
Entities with the "trader" entity class support this additional trigger:
- "opentrade": (none)
- exit the conversation dialog and open the trading dialog with the NPC.
Code mods can add custom triggers by registering adding a callback to the onControllerCreated Action in BehaviorConversable. That callback should then add new methods to handle triggers to the DialogTriggers field of the dialog controller. See EntityTrader.Initialize for an example.
Behavior properties
- dialogue: (dialogue asset name, default null)
- the asset name of the dialogue. The dialogue must exist in the config/dialog/ folder.
Content Modding | |||||||||
---|---|---|---|---|---|---|---|---|---|
Basics | Content Mods • Developing a Content Mod • Packaging & Release | ||||||||
Tutorials |
|
||||||||
Concepts | Modding Concepts • Modinfo • Variants • Domains • Patching • Remapping • World Properties | ||||||||
Moddable Assets |
|
||||||||
Uncategorized |
|
Wondering where some links have gone?
The modding navbox is going through some changes! Check out Navigation Box Updates for more info and help finding specific pages.
Modding | |
---|---|
Modding Introduction | Getting Started • Theme Pack |
Content Modding | Content Mods • Developing a Content Mod • Basic Tutorials • Intermediate Tutorials • Advanced Tutorials • Content Mod Concepts |
Code Modding | Code Mods • Setting up your Development Environment |
Property Overview | Item • Entity • Entity Behaviors • Block • Block Behaviors • Block Classes • Block Entities • Block Entity Behaviors • Collectible Behaviors • World properties |
Workflows & Infrastructure | Modding Efficiency Tips • Mod-engine compatibility • Mod Extensibility • VS Engine |
Additional Resources | Community Resources • Modding API Updates • Programming Languages • List of server commands • List of client commands • Client startup parameters • Server startup parameters Example Mods • API Docs • GitHub Repository |