NPC ~ NPC Battle Integration Changes Design Document - uchicago-cs/chiventure GitHub Wiki

Overview

Most of the changes we'd like to make answer the questions under NPC_Battle and Class type in the NPC Battle Integration Design Document. This document essentially addresses two questions: 1) making npc_t and npc_battle_t more compatible with battle structs; 2) incorporating player class. To this end, we would like to make several changes to npc_t, npc_battle_t, and related functions in order to increase compatibility among npc_battle, npc, and battle modules. We have also met with RPG-Battles and RPG-Player Class to consult these changes and get our ideas approved.

npc_t Changes

Original Version:

npc_t
typedef struct npc {
    /* hh is used for hashtable, as provided in uthash.h */
    /* Second hash handle is for storing npcs in specific rooms */
    UT_hash_handle hh, hh_room;
    /* NPC identifier */
    char *npc_id;
    /* short description of the NPC, <51 chars */
    char *short_desc;
    /* long description of the NPC, <301 chars */
    char *long_desc;
    /* pointer to an existing convo struct */
    convo_t *dialogue;
    /* pointer to inventory hashtable */
    item_hash_t *inventory;
    /* pointer to an existing class struct */
    class_t *class;
    /* pointer to an exisitng npc_move struct */
    npc_mov_t *movement;
     /* boolean representing whether or not the NPC will engage in battles */
    bool will_fight;
    /* either NULL or a pointer to an existing npc_battle struct */
    npc_battle_t *npc_battle;
} npc_t;

Changes to make:

npc_t
typedef struct npc {
    /* hh is used for hashtable, as provided in uthash.h */
    /* Second hash handle is for storing npcs in specific rooms */
    UT_hash_handle hh, hh_room;
    /* NPC identifier */
    char *npc_id;
    /* short description of the NPC, <51 chars */
    char *short_desc;
    /* long description of the NPC, <301 chars */
    char *long_desc;
    /* pointer to an existing convo struct */
    convo_t *dialogue;
    /* pointer to inventory hashtable */
    item_hash_t *inventory;
    /* pointer to an existing class struct */
    class_t *class;
    /* pointer to an existing npc_move struct */
    npc_mov_t *movement;
    /* enum indicating hostility level of the npc */
    hostility_t hostility_level;
    /* either NULL or a pointer to an existing npc_battle struct */
    npc_battle_t *npc_battle;
} npc_t;

We replaced bool will_fight, a boolean indicating whether the NPC will engage in battle, with hostility_level, which is a hostility_t enum indicating whether an NPC is HOSTILE, CONDITIONAL FRIENDLY, or FRIENDLY. Instead of having a boolean, we can test whether an NPC can engage in battle by checking if their hostility_level is HOSTILE. This is a more nuanced version of will_fight as there are more interesting scenarios that can branch off when incorporating the hostility_t struct into npc_t.

With this design, the line between FRIENDLY and non-FRIENDLY npcs will be more clear. FRIENDLY npcs, such as those that guide players through the game, introduce quests, etc. will remain FRIENDLY throughout the game as their main purpose is not to fight. HOSTILE npcs will always be ready to fight as they primarily serve as battlers. CONDITIONAL FRIENDLY npcs can also fight but only when circumstances push them towards becoming HOSTILE. For example, when a player provokes them using dialogue or by challenging them to a battle to increase their xp points or unlock a new level in the game. Such npcs will be initialized as CONDITIONAL FRIENDLY at the start of the game but upon provocation, their hostility_level will be set as HOSTILE. As such, we will need to implement a function convert_hostility() that converts an npc's hostility_level from CONDITIONAL FRIENDLY to HOSTILE if a player has provoked an npc.

convert_hostility() sketch:

void convert_hostility(npc_t *npc, node_action_t *actions)
{
    assert(npc != NULL);
    assert(actions != NULL);

    if (actions->action == START_BATTLE && 
        npc->hostility_level == CONDITIONAL_FRIENDLY) {
        npc->hostility_level = HOSTILE;
    }
    return;
}

NPC dialogue can incorporate convert_hostility() into do_node_actions() in src/npc/src/dialogue.c such that if a node action is START_BATTLE, the function calls convert_hostility().

npc_battle_t Changes

Original Version:

npc_battle_t
typedef struct npc_battle {
    /* NPC health level */
    int health;
    /* pointer to an existing stat struct */
    stat_t *stats;
    /* pointer to an existing move struct */
    move_t *moves;
    /* difficulty of the NPC's ai */
    difficulty_t ai;
    /* hostility level of the npc */
    hostility_t hostility_level;
    /* health level at which the NPC will surrender */
    int surrender_level;
} npc_battle_t;

Changes to make:

npc_battle_t
typedef struct npc_battle {
    /* pointer to an existing stat struct */
    stat_t *stats;
    /* pointer to an existing move struct */
    move_t *moves;
    /* difficulty of the NPC's ai */
    difficulty_t ai;
    /* hostility level of the npc */
    hostility_t hostility_level;
    /* class of the npc */
    class_t *class_type;
    /* An inventory of items that can be used in battle */
    battle_item_t *items;
} npc_battle_t;

After meeting with Battles, we decided to move surrender_level into stat_t and we removed health from npc_battle_t as stat_t more comprehensively incorporates player health levels. hp indicates current health level and max_hp indicates maximum health level (starting health level). Additionally, we added class_type and items. Because the stats and battle items an npc holds will differ based on its class_type, we decided to incorporate that attribute into npc_battle_t. Adding battle_item_t can increase compatibility between npc_battle_t and combatant_t, which is a struct in battle_structs.h for a player engaged in battle. These changes will require making further changes to the main interface (init, new, and free) functions in relevant modules.

NPC class_type

Player Class told us they are including a "vanilla" or standard class_type for NPCs. During our meeting, we concluded that npcs and players can both share the default class_types listed in src/playerclass/src/class_prefabs.c:

const char* const DEFAULT_CLASS_NAMES[] = {
    "bard",
    "monk",
    "ranger",
    "rogue",
    "warrior",
    "wizard"
};

To differentiate between npcs and players, however, Player Class will include a flag in class_prefab_new() indicating whether the character is an npc or not. This way, the functions class_prefab_add_skills(), class_allocate_skills(), etc. will be applicable to npcs as well which will be useful when setting distinct skills for certain class_types. For example, the vanilla/basic npc can have a default max_hp set to 30 and speed set to 20 while a bard npc can have max_hp set to 15 and speed set to 15.

We also decided to keep hostility_level and class_type as separate attributes. That is, an npc will not have a predetermined hostility_level based simply on their class_type. Instead, each class can have players with various hostility levels that can even change throughout the game upon provocation.

In our meeting with Skill Trees, we discussed having certain special skill nodes unlocked already for certain classes like wizard or warrior that are more powerful than other classes, while having all nodes locked for the vanilla npc case. The Skill Trees team is currently more focused on developing the effects of skills to be more complex and adding nodes to the skill tree struct rather than on differentiating skill trees for each class_type. But in the future, they could collaborate with Player Class to develop a separate skill tree for each class_type, including the basic npc.

Implementation

The following are modules to make changes to in order to implement the aforementioned design plans.

  1. npc_battle.h and npc_battle.c
  2. npc.h and npc.c
  3. npc tests
  • test_npc.c
  • test_npc_battle.c
  1. battle tests
  • test_battle_flow.c
  • test_battle_print.c
  1. npc examples
  2. battle examples

Next Steps

In addition to modifying the above modules, for the next sprint we will first focus on working with NPC dialogue to implement a situation where the player provokes a CONDITIONAL FRIENDLY npc and starts a battle. Second, as mentioned in our initial design document, we will work with Open World to design where npcs will be located based on their hostility_level, how a player can come up to challenge an npc to battle, and how a HOSTILE npc will come up to a player to start a fight.

Provocation in Dialogue

A joint meeting between the Battle and Dialogue teams produced the following ideas for a potential use of hostility_t in dialogue:

  1. Implement more NPC Actions specifically designed to provoke NPCs. Actions are represented as nodes within the directed graph representing dialogue. The node struct is defined as follows:
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;

A specific struct called node_action_t represents action nodes, meaning that for every NPC dialogue option, there are a certain number of defined actions that the player can take in response. The types of actions are defined by the node_action enum.

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;

In future implementations, we could have dialogues trigger certain actions by adding more options to node_action_type that can directly affect hostility_level. Some examples include INSULT, PUNCH/KICK, or YELL. These actions can be taken outside or inside a dialogue, causing the dynamic modification of hostility_level. However, this would depend on the integration of dialogue and action, which likely may not happen soon enough.

  1. Implement a tone_t enum within edge struct. Edges contain player dialogue responses.
typedef struct edge {
    char *quip;
    node_t *from, *to;
    condition_t *conditions;
} edge_t;

We suggest the following modification of the edge_t to include a tone_t enum.

typedef struct edge {
    char *quip;
    node_t *from, *to;
    condition_t *conditions;
    tone_t tone;
} edge_t;

Various types of tones could be defined which would then be checked to dynamically alter hostility_level. Here are some potential examples of types of tones:

typedef enum {
    FRIENDLY,
    ANGRY,
    SAD,
    JEALOUS,
    SHY,
    DISAPPOINTED,
    APOLOGETIC,
    HAPPY;
} tone_t;

When the player chooses a response in dialogue, the tone value of the option will be checked. Certain types of tones could cause the a CONDITIONAL_FRIENDLY NPC to become HOSTILE (ANGRY, JEALOUS) or vice-versa (APOLOGETIC, FRIENDLY).

  1. Another way to modify hostility_level would be to check for certain NPC dialogue responses in the code designing a conversation itself. For example, an example for NPC Dialogue called convotest.c defines a scene_t enum, where each NPC dialogue option is identified with a certain scene. We could simply check which scene the conversation is taking place in and modify hostility_level accordingly.
enum scene {wellMet, privacyVio, homeExpl, FightFlwr, FightStnd, Leave, ERROR};
char *scene_name(enum scene s)
{
    switch(s)
    {
    case 1:
        return "WellMet";
    case 2:
        return "PrivacyVio";
    case 3:
        return "HomeExpl";
    case 4:
        return "FightFlwr";
    case 5:
        return "FightStnd";
    case 6:
        return "Leave";
    default:
        return "ERROR";
    };
}