NPC ~ Dialogue - uchicago-cs/chiventure GitHub Wiki
This document will outline how NPC dialogue, together with its various features, have been implemented. (See: dialogue.h, dialogue.c)
Content:
- Basics
- Building a Conversation
- Running a Conversation
- CLI Integration
- Conditional Dialogue
- Tone in Dialogue
- Dialogue Actions
- Dialogue Quests
- Future Work
NPC dialogue is implemented using directed graphs. That is, a combination of nodes and edges:
Starting from node A, a player can traverse to different nodes using "edges" (i.e. dialogue options), until they arrive at node F, where the conversation ends. In our implementation, this graph was achieved with the following structs:
typedef struct edge {
char *quip;
node_t *from, *to;
} edge_t;
typedef struct edge_list {
edge_t *edge;
struct edge_list *next, *prev;
} edge_list_t;
typedef struct node {
char *node_id;
char *npc_dialogue;
edge_list_t *edges;
} node_t;
Note: the actual implementation contains a few more fields, to facilitate more complex dialogue functionalities, but the basic idea is the same.
Ideally, we want to display dialogue to the player in the following manner:
Pick an item: a sword or a shield?
1. Sword
2. Shield
To achieve this, we need to implement some kind of "numeric labeling" for the dialogue options. This task is made more complex when, later, we introduce the concept of conditional dialogue. To be brief, here is an outline for how these numeric labels are generated:
- Upon reaching a new node, each dialogue option is checked for their "availability" (conditional dialogue).
- Once we know which dialogue options are available, we assign numeric labels to those dialogue options.
- These (numeric labels & dialogue options) are all stored in a string, when is sent to the CLI for printing. (See:
create_return_string()
indialogue.c
)
To store all the nodes and edges of a conversation, we have a convo struct:
typedef struct convo {
int num_nodes;
node_list_t *all_nodes;
edge_list_t *all_edges;
node_t *cur_node;
} convo_t;
Each NPC holds a convo struct, representing the conversation they have.
To build a conversation, use the following steps.
Step 1: Create the conversation
convo_t *c = convo_new()
Step 2: Build the nodes
add_node(c, "Node 1", "Pick an item: a sword or a shield?");
add_node(c, "Node 2a", "The path of offence. Good choice.");
add_node(c, "Node 2b", "The path of defence. Smart choice.");
Step 3: Build the edges
add_edge(c, "Sword", "Node 1", "Node 2a");
add_edge(c, "Shield", "Node 1", "Node 2b");
The convo
struct can then be assigned to an NPC.
A conversation is run in the following manner:
- The player types
talk to <NPC>
. Behind the scenes,start_conversation()
is called. - A dialogue string (NPC's speech + dialogue options) is printed to the terminal.
- The player chooses his response. Behind the scenes,
run_conversation_step()
is called. - We traverse to the selected node.
- Steps 2-4 are repeated, until the conversation is done.
As mentioned above, the relevant functions are:
char *start_conversation(convo_t *c, int *rc, game_t *game);
char *run_conversation_step(convo_t *c, int input, int *rc, game_t *game);
To run dialogue from the CLI, we need a way to parse numbers, as opposed to words. To facilitate that, we use a mode functionality. (See: mode.h)
In short, when the player types the command talk to <NPC>
, the game switches into "conversation mode." In conversation mode, we parse the user's input as numbers rather than words. We then call the associated dialogue function to carry out the conversation. Once the conversation is over, the game switches back to "normal mode."
To install conditional dialogue, we introduce a conditions
field into the edge
struct. condition_t is defined in condition.h
. (See: condition.h)
typedef struct edge {
char *quip;
node_t *from, *to;
condition_t *conditions;
} edge_t;
Using functions from condition.h
, we can create item/attribute conditions, as well as test whether these conditions are met. If the conditions are met, then the edge is available.
Edge availability is reflected in edge linked lists:
typedef enum {
EDGE_DISABLED = -1,
EDGE_UNAVAILABLE,
EDGE_AVAILABLE
} edge_avail_status;
typedef struct edge_list {
edge_avail_status availability;
edge_t *edge;
struct edge_list *next, *prev;
} edge_list_t;
Availabilities are important when it comes to creating the dialogue strings. Note: EDGE_DISABLED is a special status that disables an edge permanently. This may be useful in the context of quests, for instance, where you may only want to be able to start a quest once.
The NPC Battle and Dialogue teams implemented the tone
struct into NPC battles and dialogue.
/* Tones */
typedef enum {
POSITIVE,
NEUTRAL,
NEGATIVE
} tone_t;
A tone is given to a node and edge in the conversation indicating the emotional tone of the Player's or NPC's words. When the player or NPC reaches a dialogue option with a certain tone, that tone will affect the opposite party's hostility
.
/* Hostility level options */
typedef enum hostility {
FRIENDLY = 0,
CONDITIONAL_FRIENDLY = 1,
HOSTILE = 2
} hostility_t;
When a certain dialogue option is reached, change_npc_hostility
will be called, setting the Player's or NPC's hostility
value to the appropriate value (POSITIVE
tone --> FRIENDLY
, NEGATIVE
tone --> HOSTILE
). NPCs will be defined as either FRIENDLY
, HOSTILE
, or CONDITIONAL_FRIENDLY
, where CONDITIONAL_FRIENDLY
NPCs can become hostile.
As of now, hostile NPCs and Players are intended to immediately initiate battles.
To install dialogue actions, we introduce a node_action
struct:
typedef enum {
GIVE_ITEM,
TAKE_ITEM,
START_QUEST,
START_BATTLE
} node_action_type;
typedef struct node_action {
node_action_type action;
char *action_id;
struct node_action *next, *prev;
} node_action_t;
Which we place in the node
struct:
typedef struct node {
char *node_id;
char *npc_dialogue;
int num_edges;
int num_available_edges;
edge_list_t *edges;
node_action_t *actions;
} node_t;
Essentially, when the player arrives at a node, actions
is checked. If it is not empty, then we execute each action. Note: Currently, only two actions are available: GIVE_ITEM
and TAKE_ITEM
.
Quests and tasks can be given to the player in dialogue. When a player reaches a certain conversation node, their stats are checked to see if they meet the given prerequisites for a quest (see: Incorporating Quests into NPC Design Doc). If it is, an npc_quest
is added to their quests
quest_list, with the corresponding tasks being added into the tasks
task_list.
(within the NPC struct)
/* pointer to a quest with dialogue */
npc_quest_list_t *quests;
/* pointer to a task with dialogue */
npc_task_list_t *tasks;
/*
* A linked list of npc_quest structs
* (provided by the Quest team)
*/
typedef struct npc_quest_list {
npc_quest_t *head;
int length;
} npc_quest_list_t;
Each npc_quest
and npc_task
has its own convo_t
struct holding unique dialogue that will be activated alongside the quest or task.
/*
* A singular quest node for npc_quest_list_t
* (provided by the Quest team)
*/
typedef struct npc_quest {
char *id;
convo_t *dialogue;
npc_quest_t *next;
} npc_quest_t;
See: Loading NPCs from WDL Files
Once Quest and Battle have been fully integrated into the game, we should explore allowing players to start quests and battles through dialogue. This would make for a more interesting gameplay experience. Additionally, other, unique actions could be added such as recruiting NPCs as player companions or performing certain behavioral actions (e.g. hugging, laughing, frowning, etc.).
The functions that execute a variety of KINDs of actions are defined in the actionmanagement module. There is a function in NPC dialogue called do_node_actions which is intended to execute the action, but it is not currently linked with actionmanagement. Before, this was intended to be done with an additional npc_actions
module. Right now, actions have only been possible in dialogue via monkeypatching.
Right now, a NEGATIVE
tone is designed to turn the NPC HOSTILE
immediately, upon which the NPC will initiate a battle. But more nuanced situations could be created from this setup. For example, maybe an NPC has a threshold of how many NEGATIVE
toned-comments they receive before they become HOSTILE
. Or perhaps HOSTILE
NPCs don't immediately engage in battle with the Player.
Currently, after the player traverses an edge, the edge is permanently disabled if the node it leads to contains actions. This is to prevent issues like receiving multiple copies of the same item, starting the same quest twice, etc. However, this is not an optimal solution, as we can surely imagine scenarios where despite having an action, we want to continue giving players access to the node in the future. For example, if a player forgets what an NPC told them about a quest, they should be able to go back and ask him. To that end, the edge disabling logic could be made more complex.
Currently, there is one .WDL file with the appropriate tests for its functions. However, more tests wouldn't hurt, and would make adding new parts to WDLs easier in the future.