NPCs ‐ Conversations ‐ Dynamic conversations - ThePix/QuestJS GitHub Wiki
By default, this feature is turned off, as this allows Quest to tell the player that she is not going to get anywhere using TALK TO. To turn it on, go to the settings.js file, and add this:
settings.noTalkTo = false
A dynamic conversation uses items as conversation topics, with the topics getting displayed and hidden as the game progresses. We do not have to do anything to an NPC except ensure he or she has no "talkto" attribute (besides the default one).
What we need is some topics...
Topics as Items
Each topic is an item with the TOPIC template. The template should be sent true
if it is visible from the start. Its "loc" attribution has to be set to the name of the NPC, and the "alias" attribute to what the player will see in the menu of conversation choices.
createItem("Mary_The_Garden", TOPIC(true), {
loc:"Mary",
alias:"What's the deal with the garden?",
nowShow:["Mary_The_Garden_Again"],
script:function() {
msg("You ask Mary about the garden, but she's not talking.")
},
})
createItem("Mary_The_Garden_Again", TOPIC(false), {
loc:"Mary",
alias:"Seriously, what's the deal with the garden?",
msg:"You ask Mary about the garden, but she's STILL not talking.",
},
})
createItem("Mary_The_Weather", TOPIC(true), {
loc:"Mary",
alias:"The weather",
script:function() {
msg("You talk to Mary about the weather.")
},
})
Topics can have a "msg" attribute, a "script" or both. If it has both, the script will be used first.
Topics as an attribute
You might prefer to create topics as an attribute of the character they belong to. This keeps them together, and is a little less typing. Behind the scenes Quest will use the attribute to create the usual topic objects (and then delete the attribute), so the end result is the same, and in fact there is no reason you could not create some topics one way and some the other way.
convTopics:[
{
showTopic:true,
alias:"What's the deal with the garden?",
nowShow:["Seriously, what's the deal with the garden?"],
script:function() {
msg("You ask Mary about the garden, but she's not talking.")
},
},
{
alias:"Seriously, what's the deal with the garden?",
script:function() {
msg("You ask Mary about the garden, but she's STILL not talking.")
},
},
{
showTopic:true,
alias:"The weather",
script:function() {
msg("You talk to Mary about the weather.")
},
},
],
If you want the topic to be visible from the start, set "showTopic" to true
. Note that the "loc" attribute is set automatically.
The object names will be the name of the NPC, followed by "convTopics" followed by a number, unless you give it a specific name.
For "nowShow" and "nowHide", it maybe, therefore, be more convenient to use the alias, rather the name, but be aware that if more than one topic has the same alias, Quest will go with the first it finds, regardless of the NPC.
Generating Topics
If you have a lot of similar topics, you might want to think about creating them in a loop. The example here allows for several topics for a bar tender asking for different drinks.
Topics in Action
When the player talks to the NPC, a list of available topics will be displayed. A topic will be used if the player chooses that option (an extra option will be added allowing the player to choose none of the above). When a topic is used, its "script" function will be run (if there is one) and then its "msg" string displayed (if there is one).
The "script" attribute function is passed a dictionary, with "char" set to the NPC and "topic" set to the topic. The "msg" attribute can similarly use "char" and "topic" in text processor directives.
Controlling when a topic is available - basic
The "nowShow" attribute is an array of topic names that should become available after this topic has been used (but only if they have not already been hidden). Conversely, "nowHide" can be used to hide other topics. These are not limited to the NPC; topics for other NPCs can also be shown and hidden here.
By default a topic is hidden after use, set "hideAfter" to false
to stop that.
Controlling when a topic is available - advanced
Whether a topic is offered to the player for a certain NPC is governed by three considerations, all of which need to be true for a topic to be available.
Who it belongs to
If the topic's "belongsTo" function returns true
when sent this NPC's name the topic will be available. By default, this will be if the topic's "loc" attribute is set to the NPC's name.
Sometimes you have a topic that you want two or more NPCs to have, and it does not really matter which one the player talks to. You can accomplish this by using a custom "belongsTo" function. Let us say the player could ask Toby about the weather as well as Mary
createItem("Mary_Toby_The_Weather", TOPIC(true), {
belongsTo:function(loc) { return ["Mary", "Toby"].includes(loc) },
alias:"The weather",
script:function(options) {
msg("You talk to " + options.char.alias + " about the weather.")
},
})
If you have several topics available to a specific group, you might find it more convenient to create a dedicated function.
const maryAndToby = function(loc) { return ["Mary", "Toby"].includes(loc) }
createItem("Mary_Toby_The_Weather", TOPIC(true), {
belongsTo:maryAndToby,
alias:"The weather",
script:function(options) {
msg("You talk to {nm:char} about the weather.", options)
},
})
Or perhaps it should be available for the one NPC designated as meteorologist, but who that is might change.
createItem("meteorologist_The_Weather", TOPIC(true), {
belongsTo:function(loc) { return loc === game.player.meteorologist },
alias:"The weather",
script:function(options) {
msg("You talk to {nm:char} about the weather.", options)
},
})
Again, you could put that in its own function if you use it a lot.
const meteorologist = function(loc) { return loc === game.player.meteorologist }
Life cycle
Generally a topic is not available at the start, the player does something - perhaps using another topic - and it becomes available. Later the player uses this topic, and it is no longer available. This is the topic's life cycle, and behind the scenes is controlled by two attributes. The topic is available when both "showTopic" is true
and "hideTopic" is false
.
Why two? Because it is useful to differentiate between hidden before showing, and hidden after showing.
Stage | showTopic | hideTopic |
---|---|---|
Before | false | false |
Available | true | false |
After | true | true |
The initial value of "showTopic" can be set by the Boolean used with TOPIC.
After a topic is used, "hideTopic" gets set to true
(unless "hideAfter is false
). A topic's "nowShow" is used to set "showTopic" to true
for the named topics, while "nowHide" is used to set "hideTopic" to true
.
You can, however, also set them yourself with "show()" and "hide()".
w.Lara_ask_about_flowers.show()
w.Kyle_destoyed_flowers.hide()
An implication here is that a topic that has been used will not re-appear, as hide takes priority over show, and once a topic is hidden, none of the standard functions will make it reappear - though you can set "hideTopic" to false to resurrect a topic, if you really feel you need to.
Specific situation
Finally, you can have a topic only visible in a certain situation. A topic will only be available if its "isVisible" function returns true
when sent this NPC. By default this is always the case, but you can add your custom "isVisible" function. Note that the rules about showTopic
and hideTopic
still apply; the topic will only be visible if showTopic
is true
, hideTopic
is false
and the "isVisible" function returns true
.
This example has the topic only available when the NPC is in the kitchen.
isVisible:function(char) {
return char.isAtLoc("kitchen");
},
If you have an NPC who can follow the player, you could use this to control the topics asking the NPC to follow or not. Note that both are there from the start, and both set not to be hidden after use. Whether visible is determined in the "isVisible" attribute.
createItem("robot_follow_me", TOPIC(true), {
alias:"Follow me",
hideAfter:false,
loc:"robot",
isVisible:function(loc) { return !w.robot.leaderName },
script:function() {
msg("'Follow me,' you say to the robot.")
msg("'Yes, sir.'")
w.robot.setLeader(player)
},
})
createItem("robot_stop_following", TOPIC(true), {
alias:"Stop following me",
hideAfter:false,
loc:"robot",
isVisible:function(loc) { return w.robot.leaderName },
script:function() {
msg("'Stop following me,' you say to the robot.")
msg("'Yes, sir.'")
w.robot.setLeader()
},
})
Resetting topics
Occasionally you want to reset topics so the player can ask them again. Perhaps he can rewind time or something. It is not going to be something that crops up much, so there is no built-in facility, but it is easy enough to do.
The first thing to do is give all that topics that need to be reset an attribute that flags them as such. I am going to call it "resettingTopic". All those that will be visible straightaway also need to have an attribute to flag that, say "showFromStart". Both should be set to true
.
In your code, you go through every object in the game, looking for those with "resettingTopic" set to true
, and for those objects, set "showTopic" and "hideTopic" appropriately.
for (const key in w) {
if (w[key].resettingTopic) {
w[key].showTopic = w[key].showFromStart
w[key].hideTopic = false
}
}
If it is all the topics for a certain character, you could check for that instead of using "resettingTopic".
Expiring
You can set a conversation topic to expire after so many turns, by giving it a "countdown" attribute. The countdown starts from when "showTopic" is true. This is useful for responses to earlier conversations; if a character asks the player's opinion on something, it is a bit weird if the player does not answer until the next day. Clearly some care is required to ensure the game does not become unwinnable.
Preamble (greeting)
You can give a character a "greeting" function; this will fire before the user is invited to select a topic - but not if there are no topics or the player cannot speak.
This is probably most useful to give a greeting the first time the player talks to this NPC. This is readily accomplished by checking how many times the player has spoken to the NPC; if "talkto_count" is zero, this is the first time.
greeting: function() {
if (this.talkto_count === 0) {
msg("'Excuse me, can I have a word,' you say to Lara.")
}
}
An alternative approach to greetings is to give an NPC a single starting topic that is the greeting, and have that then show a set of additional topics.
Count
As mentioned above, each NPC has a "talkto_count" attribute. In addition, every topic has a "count" attribute that can be used to determine how many times the user has selected it. It will be zero in the topic's script the first time the topic is used.
Accessing topics by alias
To show or hide a topic in code, you can use the "findTopic", "showTopic" and "hideTopic" attributes of the character, which can take the alias, rather than the name - it will try to match the name first, as that will be unique; if that fails it will try to make to an alias. You can add a second attribute, a number indicating which topic with that alias to show or hide.
This example will show the third topic with the alias "Carrots" for Lara.
w.Lara.showTopic('Carrots', 3)
Using the alias is a little more brittle than using the name; if you later realise you missed an apostrophe, say, you will need to make sure you update not just the topic, but every reference to it. It is not a great way to do it!
Note that if Quest fails to find a topic it will assume this is a bug, and will show an error.
trouble-shooting
A topic is not showing up when it should...
There are a number of factors that you should check. Let us suppose the topic has the name "druid_tree", and this belongs to the NPC called "druid". In your browser, get to the relevant point in your game and open the developer console with F12. Type these commands (lines starting with two slashes are comments - do not type them!).
w.druid_tree.isLocatedAt("druid")
// should be true
// if not, check the "loc" of the topic is set to "druid"
// or, if using convTopics, that the topic is in the right convTopics list
w.druid_tree.showTopic
// should be true
// if not, you need to set it to true if you want it visible from the start
// or have something set it to true at the right time
w.druid_tree.hideTopic
// should be false
// if not, look at anywhere that might set it to true
// try searching for "hideTopic" and see what you find
w.druid_tree.isVisible()
// should be true
// if not, you must have a custom function; this is on you!
w.druid.getTopics()
// should give a list that includes this topic
// if the above are right, this should be too
Hyperlinks or Dropdowns or ...
Quest 6 has various menu options. By default, dynamic conversations use hyperlinks with showMenu
, but you can use any of them as specified in settings.js.
settings.funcForDynamicConv = 'showMenuNumbersOnly'
The various options are discussed here.
Note that the setting needs the name as a string, as the function does not exist when settings.js is loaded.
... Or something else
If you want to create your own custom menu system, create a function that takes three parameters, the title, a list of options and a callback function. This example is just the default renamed. I suggest this is done in code.js.
function myCustomMenuFunction(title, options, fn) {
const opts = {article:DEFINITE, capital:true, noLinks:true}
io.input(title, options, false, fn, function(options) {
for (let i = 0; i < options.length; i++) {
let s = '<a class="menu-option" onclick="io.menuResponse(' + i + ')">';
s += (typeof options[i] === 'string' ? options[i] : lang.getName(options[i], opts))
s += '</a>';
msg(s);
}
})
}
You then need to add the name to the list in io
. This will need to be done after the function above has been defined; I suggest immediately after the function in code.js.
io.menuFunctions['myCustomMenuFunction'] = myCustomMenuFunction
Now in settings.js you can set your function:
settings.funcForDynamicConv = 'myCustomMenuFunction'
Have the NPC ask a question
Why should the player be the only one to initiate a discussion? You can use this system to have an NPC ask the player.
In this example, the player can ask the NPC about the weather - this is the first topic. In the response, the NPC's "askTopics" function attribute is called, which presents the player with a question, and the two options - which are topics as normal.
convTopics:[
{
showTopic:true,
alias:"The weather",
script:function() {
msg("You talk to " + this.alias + " about the weather; he asks your opinion...")
this.askTopics("Tell Kyle your view on the weather...", w.kyle_response_good, w.kyle_response_bad)
},
},
{
alias:"The weather is good",
name:'kyle_response_good',
script:function() {
msg("You tell Kyle you think the weather is good.")
},
},
{
alias:"The weather is bad",
name:'kyle_response_bad',
script:function() {
msg("You tell Kyle the weather is bad; he shakes his head sadly.")
},
},
],
NPCs can ask questions in any situation, not just when the player has asked a question herself. This example is when the player first enters a room. Note that although the NPC is talking, this is an attribute of the location, as it is entering the location that is the trigger. The function calls the "askTopic" function attribute of the NPC, offering the two options defined above.
onFirstEnter:function() {
msg("'Ah, hello,' says Kyle. 'What about all this weather?'")
w.Kyle.askTopics("Tell Kyle your view on the weather...", w.kyle_response_good, w.kyle_response_bad)
},
You can chain as many as you like together, effectively having a CYOA story embedded in your game. This can be a good way to stop the player "lawnmowering" options, i.e, just going through every single topic. This way she has to commit to decisions.
If you do have a lot of options, you might prefer to use a different data structure to hold it.
Here is a simple example; this would be an attribute of your NPC, and is a series of nested dictionaries. At the top level we have two attributes, "t" and "options", the first being the text displayed, the second being the choices that are presented to the user. The "options" attribute is itself a dictionary - each entry being one choice. The user will see, in this case, the options "Yes" and "No". When the user makes a choice, that dictionary will be used. Again, we have a "t" attribute in each, but one also has a "script" attribute
interviewTopics:{
t:"'Hi,' says Lara, 'have you got my carrots?'",
options:{
"Yes":{
t:"'Yes,' you reply, 'got them right here.'",
script:function() { log('Chose yes') },
},
"No":{
t:"'No,' you reply, 'still looking.'|'Oh... Well when will you have them?",
},
},
},
You can go ever deeper. The next example adds further options if the player chooses "No".
interviewTopics:{
t:"'Hi,' says Lara, 'have you got my carrots?'",
options:{
"Yes":{
t:"'Yes,' you reply, 'got them right here.'",
script:function() { log('Chose yes') },
},
"No":{
t:"'No,' you reply, 'still looking.'|'Oh... Well when will you have them?",
options:{
"Soon... Probably":{
options:{
"Never":true,
"I'll do it now":"'Okay, I'll do it now.'",
},
t:"Soon... Probably,' you reassure the hungry rabbit.|'But when?' she asks. 'Fading away bunny!'",
},
"Never":{
t:"'It's really not going to happen.'|Lara looks disappointed with you. And very angry. 'Why not?'",
},
},
},
},
},
You will see that there are two types of dictionary at play here. One is always called "options", the other is not and defines a specific option.
Note: The dictionaries will not be saved when the user saves her game, so do not change values in it whilst the game is in progress.
The specific-option dictionary
It can have these attributes:
- t: Text to be displayed. Use a vertical bar, |, to mark a new paragraph. You can use the text processor, with parameters "char" and "topic".
- script: A script to run, which will be before the text is printed. Will be passed a dictionary with parameters "char" and "topic".
- options: A dictionary of options for the user to select from.
- question: Another specific-option dictionary (see later).
The options dictionary
It can have any number of attributes. Note that you can use spaces and punctuation for name of this attribute, as seen above for "Soon... Probably". I normally advise against that, but it is allowed in JavaScript, and fine to do here.
Each value can be another dictionary (a specific-option dictionary) or a string or true
. If a string, that is the same as a dictionary with the "t" attribute set to the string. If true
, a response with the same name on this NPC will be used.
Free-text questions
What if you want to ask a question and let the player type an answer? This could be part of a character creation process, and you are asking the user to supply the character's name, for example.
TODO!
If you have both "options" and "question" attributes for a specific-option dictionary, the "question" attribute will be ignored.
Conversing With Other Items
What if the player can talk to something that is not an NPC, such as a magic painting? You do not want the painting to be an NPC, so cannot use the template. However, you can give it the relevant attributes. There are five to add, but you can just copy-and-paste from here.
hereVerbs:["Talk to"],
talkto:npc_utilities.talkto,
getTopics:npc_utilities.getTopics,
pause:function() {},
talker:true,
For more complex items you may need to handle display verbs differently - see here.