T4modules Chats Beyond Hack and Slash
Chats: Beyond Hack and Slash
"But this is a roguelike," you say, "I //like// the hacking and slashing." Well fine... how about a chat //before// the fight, then? And what better an example than the Black Knight from the Monty Python and the Holy Grail?
The first thing to do is to let the game know that an NPC can talk. In the code below we set the "can_talk" variable of the entity and we also set the black knight's faction (factions) to be the same as the players.
newEntity{
name = "The Black Knight", define_as = "BLACK_KNIGHT",
display = "k", color=colors.BLACK,
desc = a knight dressed in black armor stands before you.,
level_range = {1, 4}, exp_worth = 1,
can_talk = "black_knight",
faction = "players",
}
The game can now check for the can_talk variable when you bump into a NPC (or maybe target an NPC with a talk command, however you decide to code it). If you are using the Combat interface from the example module I suggest you edit the bumpInto function like so
function _M:bumpInto(target)
local reaction = self:reactionToward(target)
if reaction < 0 then
return self:attackTarget(target)
elseif reaction >= 0 then
-- Talk ?
if self.player and target.can_talk then
local chat = Chat.new(target.can_talk, target, self)
chat:invoke()
elseif target.player and self.can_talk then
local chat = Chat.new(self.can_talk, self, target)
chat:invoke()
-- Displace
elseif self.move_others then
game.level.map:remove(self.x, self.y, Map.ACTOR)
game.level.map:remove(target.x, target.y, Map.ACTOR)
game.level.map(self.x, self.y, Map.ACTOR, target)
game.level.map(target.x, target.y, Map.ACTOR, self)
self.x, self.y, target.x, target.y = target.x, target.y, self.x, self.y
end
end
end
Remember how we set the black knight's faction to "players"? Well that ensures us a favorable reaction between the two, taking us into the "elseif reaction >= 0" loops. The creation of a new chat takes three variables, a string with the name of the chat, the npc and the player. In our example we will be passing "black_knight", the black knight entity and finally the player entity. Now we need to actually create the chat so T-Engine has something to find when trying to create the Chat object.
The default location for chats is in the data/chats folders of your module. Create data/chats/black_knight.lua and open it. Each round of dialog will be created using the newChat function. We need to supply an id tag for each dialog round, the text that the NPC will say, and the possible responses we can select from. The answers table is where most of the magic happens, but we will start with a simple example.
newChat{ id="first",
text = #light_green#* The black knight stares at you in silence., answers = { {"You fight with the strength of many men, sir knight. I am Arthur, King of the Britons.", jump="second"}, {Walk away.}, },
}
Each entry in the answers table is itself a table which I will refer to as a response rather than the singular answer. The first entry in the response is a string and this is what will be displayed in the chat window. Other parameters can be supplied in the response such as the "jump" parameter above. The jump parameter tells the Chat the id of the round of dialog to be loaded when that option is selected. The second response does not have a jump parameter, so selecting that option will quit the Chat. Now we have to define the next few rounds of dialog!
newChat{ id="second", text = #light_green#* The black knight continues to stare at you in silence., answers = { {"I seek the finest and the bravest knights in the land to join me in my court of Camelot.", jump="third"}, {Walk away.}, }, } newChat{ id="third", text = #light_green#* The black knight is silent once again., answers = { {"You have proved yourself worthy; will you join me?", jump="fourth"}, {Walk away.}, }, } newChat{ id="fourth", text = #light_green#* Silence... well what did you expect?, answers = { {"You make me sad. So be it. Come, Patsy.", jump="fifth"}, {Walk away.}, }, } newChat{ id="fifth", text = "None shall pass.", answers = { {"What?", jump="fifth"}, {"I have no quarrel with you, good sir knight, but I must cross this bridge.", jump="sixth"}, {Walk away.}, }, }
In that last ("fifth") round of dialog we see something interesting, a recursive dialog. Nothing wrong there, and it actually saves us from coding an extra round of dialog. We are getting near the hack-and-slash bits!
newChat{ id="sixth",
text = "Then you shall die.", answers = { {"What?", jump="fifth"}, {"I command you as King of the Britons to stand aside!", jump="seventh"}, {Walk away.}, },
} newChat{ id="seventh",
text = "I move for no man.", answers = { {"What?", jump="seventh"}, {"So be it!", action=function(npc, player) npc.faction = "enemies" }, {Walk away.}, },
}
The interesting response is the one below. The "action" parameter should be a function that accepts the npc as a first argument and the player as the second. What you do then is up to you, but in our case we want the game to recognize that the player and black knight are now enemies. A simple way to do that is to change the black knight's faction from "players" to "enemies." The response does not have a jump entry, so the Chat will end but since we are now enemies with the knight we can attack him to our heart's content.
{"So be it!", action=function(npc, player) npc.faction = "enemies" },
Wait, there is one more thing! We need to return the id of the initial round of dialog, in our case "first." This should be done at the end of the chat lua file.
return "first"
There, you are all done. There are some more advanced chat features like conditional responses and for now you can dig into the ToME code if you want to discover those.
Randomizing chats
If you want to randomize chats just use tables as in the following example:
greeting = {"Hi", "Hello", "Good day", "Bonjour", "Greetings"}
hope = {"hope", "trust", "see", "perceive"}
newChat{ id="hello",
text = rng.table(greeting) .. " @playername@! I " .. rng.table(hope) .. " your quest goes well.", answers = { {rng.table(greeting) .. "!"}, },
}
Notice the use of rng.table(foo) to pick a random string and the use of ".." to combine the strings.
Conditional Answers and Jumps
An answer in the newChat answers table can have a cond variable with the following signature:
cond = function(npc, player)
...
end
If the function returns true then the answer will appear in the chat, but if it returns false it will not. You can use this to only allow certain options in dialogs after a sub-quest has been completed, or a certain item is in the player's inventory.
You can also have a single answer send the player to various places using the action function. In the action function above we did not bother with a return value, but if there is a return value that will be used as the next chat instead of the jump variable. For example, say you want a NPC to offer the player a side quest only 20% of the times they talk. You could do:
action = function(npc, player)
if rng.percent(20) then return "offer quest" else return "regular response" end
end