├── doors.json ├── armor.json ├── creatures.json ├── items.json ├── weapons.json ├── tutorial ├── 07-experience-formula.tex ├── 01-intro.md ├── 03-items.md ├── 02-entities.md ├── 07-player.md ├── 06-creatures.md ├── 08-tying-it-together.md ├── 05-areas.md ├── 04-inventories.md ├── 07-experience-formula.svg └── 09-battle-system.md ├── src ├── weapon.hpp ├── armor.hpp ├── armor.cpp ├── item.hpp ├── weapon.cpp ├── entity.hpp ├── item.cpp ├── entity_manager.hpp ├── door.hpp ├── door.cpp ├── area.hpp ├── player.hpp ├── battle.hpp ├── creature.hpp ├── dialogue.hpp ├── inventory.hpp ├── entity_manager.cpp ├── area.cpp ├── player.cpp ├── creature.cpp ├── inventory.cpp ├── battle.cpp └── main.cpp ├── areas.json ├── JsonBox-License.txt ├── LICENSE └── README.md /doors.json: -------------------------------------------------------------------------------- 1 | { 2 | "door_01_02": { 3 | "description": "sturdy wooden door", 4 | "areas": ["area_01", "area_02"], 5 | "locked": 1, 6 | "key": "item_iron_key" 7 | } 8 | } -------------------------------------------------------------------------------- /armor.json: -------------------------------------------------------------------------------- 1 | { 2 | "armor_leather": { 3 | "name": "Leather Armor", 4 | "defense": 1, 5 | "description": "Armor made from tanned animal hide. It smells a bit funny, but it'd do the job." 6 | } 7 | } -------------------------------------------------------------------------------- /creatures.json: -------------------------------------------------------------------------------- 1 | { 2 | "creature_rat": { 3 | "name": "Rat", 4 | "hp": 3, 5 | "strength": 5, 6 | "agility": 3, 7 | "evasion": 0.015625, 8 | "xp": 1, 9 | "equipped_weapon": "weapon_rat_claw" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /items.json: -------------------------------------------------------------------------------- 1 | { 2 | "item_gold_coin": { 3 | "name": "Gold Coin", 4 | "description": "A small disc made of lustrous metal" 5 | }, 6 | 7 | "item_iron_key": { 8 | "name": "Iron Key", 9 | "description": "A heavy iron key with a simple cut" 10 | } 11 | } -------------------------------------------------------------------------------- /weapons.json: -------------------------------------------------------------------------------- 1 | { 2 | "weapon_dagger": { 3 | "name": "Dagger", 4 | "damage": 2, 5 | "description": "A small blade, probably made of iron. Keep the sharp end away from your body." 6 | }, 7 | 8 | "weapon_rat_claw": { 9 | "name": "_rat_claw", 10 | "damage": 1, 11 | "description": "" 12 | } 13 | } -------------------------------------------------------------------------------- /tutorial/07-experience-formula.tex: -------------------------------------------------------------------------------- 1 | % Building this then running `pdftocairo` from `poppler` was easier than 2 | % messing around with MathJax or anything like that. 3 | \documentclass[preview]{standalone} 4 | 5 | \usepackage{amsmath} 6 | 7 | \begin{document} 8 | 9 | \[ 10 | \left\lfloor 11 | 1 + k(1 + (n\ \operatorname{mod}\ 2))\tanh\left(\frac{n}{30}\right) 12 | \right\rfloor 13 | \] 14 | 15 | \end{document} 16 | -------------------------------------------------------------------------------- /src/weapon.hpp: -------------------------------------------------------------------------------- 1 | #ifndef WEAPON_HPP 2 | #define WEAPON_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include "item.hpp" 8 | 9 | class EntityManager; 10 | 11 | class Weapon : public Item 12 | { 13 | public: 14 | 15 | int damage; 16 | 17 | // Constructors 18 | Weapon(std::string id, std::string name, std::string description, int damage); 19 | Weapon(std::string id, JsonBox::Value& v, EntityManager* mgr); 20 | 21 | void load(JsonBox::Value& v, EntityManager* mgr); 22 | }; 23 | 24 | #endif /* WEAPON_HPP */ 25 | -------------------------------------------------------------------------------- /src/armor.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ARMOR_HPP 2 | #define ARMOR_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include "item.hpp" 8 | 9 | class EntityManager; 10 | 11 | class Armor : public Item 12 | { 13 | public: 14 | 15 | int defense; 16 | 17 | // Constructors 18 | Armor(std::string id, std::string name, std::string description, int defense); 19 | Armor(std::string id, JsonBox::Value& v, EntityManager* mgr); 20 | 21 | // Load the armor from the Json value 22 | void load(JsonBox::Value& v, EntityManager* mgr); 23 | }; 24 | 25 | #endif /* ARMOR_HPP */ 26 | -------------------------------------------------------------------------------- /src/armor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "armor.hpp" 5 | #include "item.hpp" 6 | #include "entity_manager.hpp" 7 | 8 | Armor::Armor(std::string id, std::string name, std::string description, int defense) : 9 | Item(id, name, description) 10 | { 11 | this->defense = defense; 12 | } 13 | 14 | Armor::Armor(std::string id, JsonBox::Value& v, EntityManager* mgr) : Item(id, v, mgr) 15 | { 16 | this->load(v, mgr); 17 | } 18 | 19 | void Armor::load(JsonBox::Value& v, EntityManager* mgr) 20 | { 21 | JsonBox::Object o = v.getObject(); 22 | this->defense = o["defense"].getInteger(); 23 | 24 | return; 25 | } 26 | -------------------------------------------------------------------------------- /src/item.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ITEM_HPP 2 | #define ITEM_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include "entity.hpp" 8 | 9 | class EntityManager; 10 | 11 | class Item : public Entity 12 | { 13 | public: 14 | 15 | // Name and description of the item 16 | std::string name; 17 | std::string description; 18 | 19 | // Constructors 20 | Item(std::string id, std::string name, std::string description); 21 | Item(std::string id, JsonBox::Value& v, EntityManager* mgr); 22 | 23 | // Load the item information from the JSON value 24 | virtual void load(JsonBox::Value& v, EntityManager* mgr); 25 | }; 26 | 27 | #endif /* ITEM_HPP */ 28 | -------------------------------------------------------------------------------- /src/weapon.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "weapon.hpp" 5 | #include "item.hpp" 6 | #include "entity_manager.hpp" 7 | 8 | Weapon::Weapon(std::string id, std::string name, std::string description, int damage) : 9 | Item(id, name, description) 10 | { 11 | this->damage = damage; 12 | } 13 | 14 | 15 | Weapon::Weapon(std::string id, JsonBox::Value& v, EntityManager* mgr) : Item(id, v, mgr) 16 | { 17 | this->load(v, mgr); 18 | } 19 | 20 | void Weapon::load(JsonBox::Value& v, EntityManager* mgr) 21 | { 22 | JsonBox::Object o = v.getObject(); 23 | this->damage = o["damage"].getInteger(); 24 | 25 | return; 26 | } 27 | -------------------------------------------------------------------------------- /src/entity.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ENTITY_HPP 2 | #define ENTITY_HPP 3 | 4 | #include 5 | #include 6 | 7 | class EntityManager; 8 | 9 | class Entity 10 | { 11 | public: 12 | 13 | std::string id; 14 | 15 | Entity(std::string id) 16 | { 17 | this->id = id; 18 | } 19 | 20 | // Destructor must be made virtual as all derived classes are 21 | // treated as Entity in the EntityManager 22 | virtual ~Entity() {} 23 | 24 | // Pure virtual function stops Entity from being instantiated and forces it 25 | // to be implemented in all derived types 26 | virtual void load(JsonBox::Value& v, EntityManager* mgr) = 0; 27 | }; 28 | 29 | #endif /* ENTITY_HPP */ 30 | -------------------------------------------------------------------------------- /src/item.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "item.hpp" 5 | #include "entity.hpp" 6 | #include "entity_manager.hpp" 7 | 8 | Item::Item(std::string id, std::string name, std::string description) : Entity(id) 9 | { 10 | this->name = name; 11 | this->description = description; 12 | } 13 | 14 | Item::Item(std::string id, JsonBox::Value& v, EntityManager* mgr) : Entity(id) 15 | { 16 | this->load(v, mgr); 17 | } 18 | 19 | void Item::load(JsonBox::Value& v, EntityManager* mgr) 20 | { 21 | JsonBox::Object o = v.getObject(); 22 | this->name = o["name"].getString(); 23 | this->description = o["description"].getString(); 24 | 25 | return; 26 | } 27 | -------------------------------------------------------------------------------- /areas.json: -------------------------------------------------------------------------------- 1 | { 2 | "area_01": { 3 | "dialogue": { 4 | "description": "You are in room 1", 5 | "choices": [] 6 | }, 7 | "doors": ["door_01_02"], 8 | "inventory": { 9 | "items": [ 10 | ["item_gold_coin", 5], 11 | ["item_iron_key", 1] 12 | ], 13 | "weapons": [ 14 | ["weapon_dagger", 1] 15 | ], 16 | "armor": [ 17 | ["armor_leather", 1] 18 | ] 19 | }, 20 | "creatures": [] 21 | }, 22 | 23 | "area_02": { 24 | "dialogue": { 25 | "description": "You are in room 2", 26 | "choices": [] 27 | }, 28 | "doors": ["door_01_02"], 29 | "inventory": { 30 | "items": [ 31 | ["item_gold_coin", 100] 32 | ], 33 | "weapons": [], 34 | "armor": [] 35 | }, 36 | "creatures": 37 | [ 38 | "creature_rat" 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /src/entity_manager.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ENTITY_MANAGER_HPP 2 | #define ENTITY_MANAGER_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include "entity.hpp" 8 | 9 | class EntityManager 10 | { 11 | private: 12 | 13 | std::map data; 14 | 15 | public: 16 | 17 | // Load the JSON file and determine which map to save the data to 18 | // according to the type T 19 | template 20 | void loadJson(std::string filename); 21 | 22 | // Return the entity given by id 23 | template 24 | T* getEntity(std::string id); 25 | 26 | // Constructor 27 | EntityManager(); 28 | 29 | // Destructor 30 | ~EntityManager(); 31 | }; 32 | 33 | // Convert a derived entity type to its id prefix. e.g. Item -> "item" 34 | template 35 | std::string entityToString(); 36 | 37 | #endif /* ENTITY_MANAGER_HPP */ 38 | -------------------------------------------------------------------------------- /src/door.hpp: -------------------------------------------------------------------------------- 1 | #ifndef DOOR_HPP 2 | #define DOOR_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include "entity.hpp" 8 | 9 | class Item; 10 | class EntityManager; 11 | 12 | class Door : public Entity 13 | { 14 | public: 15 | 16 | // Door description e.g. large wooden door, rusted iron gate 17 | std::string description; 18 | 19 | // < 0 is open 20 | // 0 is unlocked but closed 21 | // > 0 is locked and needs key to open 22 | int locked; 23 | 24 | // If the player has the required key then they can unlock the door. 25 | Item* key; 26 | 27 | std::pair areas; 28 | 29 | Door(std::string id, std::string description, std::pair areas, 30 | int locked, Item* key = nullptr); 31 | Door(std::string id, JsonBox::Value& v, EntityManager* mgr); 32 | 33 | void load(JsonBox::Value& v, EntityManager* mgr); 34 | }; 35 | 36 | #endif /* DOOR_HPP */ 37 | -------------------------------------------------------------------------------- /src/door.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "door.hpp" 5 | #include "item.hpp" 6 | #include "entity.hpp" 7 | #include "entity_manager.hpp" 8 | 9 | Door::Door(std::string id, std::string description, std::pair areas, 10 | int locked, Item* key) : Entity(id) 11 | { 12 | this->description = description; 13 | this->areas = areas; 14 | this->locked = locked; 15 | this->key = key; 16 | } 17 | 18 | Door::Door(std::string id, JsonBox::Value& v, EntityManager* mgr) : Entity(id) 19 | { 20 | this->load(v, mgr); 21 | } 22 | 23 | void Door::load(JsonBox::Value& v, EntityManager* mgr) 24 | { 25 | JsonBox::Object o = v.getObject(); 26 | this->description = o["description"].getString(); 27 | this->locked = o["locked"].getInteger(); 28 | if(o.find("key") != o.end()) 29 | { 30 | this->key = mgr->getEntity(o["key"].getString()); 31 | } 32 | JsonBox::Array a = o["areas"].getArray(); 33 | if(a.size() == 2) 34 | { 35 | this->areas.first = a[0].getString(); 36 | this->areas.second = a[1].getString(); 37 | } 38 | 39 | return; 40 | } 41 | -------------------------------------------------------------------------------- /JsonBox-License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Anhero Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Mansfield 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/area.hpp: -------------------------------------------------------------------------------- 1 | #ifndef AREA_HPP 2 | #define AREA_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "entity.hpp" 9 | #include "inventory.hpp" 10 | #include "creature.hpp" 11 | #include "dialogue.hpp" 12 | 13 | class EntityManager; 14 | class Door; 15 | 16 | // Movement is achieved through the use of areas, which are contained 17 | // units of space consisting of an inventory, a list of creatures and 18 | // a dialogue 19 | class Area : public Entity 20 | { 21 | public: 22 | 23 | // Dialogue is run whenever the area is entered 24 | Dialogue dialogue; 25 | 26 | // Items contained within the area. Not split into individual containers 27 | // for simplicity 28 | Inventory items; 29 | 30 | // Links between rooms. Every door should have this as one of its area 31 | // pointers 32 | std::vector doors; 33 | 34 | // Creatures contained within the area. Not pointers because we want unique 35 | // instances of the creatures 36 | std::vector creatures; 37 | 38 | // Constructors 39 | Area(std::string id, Dialogue dialogue, Inventory items, 40 | std::vector creatures); 41 | Area(std::string id, JsonBox::Value& v, EntityManager* mgr); 42 | 43 | // Load the area from the given Json value 44 | void load(JsonBox::Value& v, EntityManager* mgr); 45 | 46 | // Return a Json object representing the area 47 | JsonBox::Object getJson(); 48 | }; 49 | 50 | #endif /* AREA_HPP */ 51 | -------------------------------------------------------------------------------- /src/player.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLAYER_HPP 2 | #define PLAYER_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "creature.hpp" 9 | 10 | class EntityManager; 11 | 12 | class Player : public Creature 13 | { 14 | public: 15 | 16 | // Name of the player's class 17 | // Class may be Fighter, Rogue etc 18 | std::string className; 19 | 20 | // Level of the player 21 | unsigned int level; 22 | 23 | // Ids of areas visited by the player 24 | std::unordered_set visitedAreas; 25 | 26 | // Constructors 27 | Player(std::string name, int hp, int strength, int agility, double evasion, 28 | unsigned int xp, unsigned int level, std::string className); 29 | Player(); 30 | Player(JsonBox::Value& saveData, JsonBox::Value& areaData, EntityManager* mgr); 31 | 32 | // Calculates the total experience required to reach a certain level 33 | unsigned int xpToLevel(unsigned int level); 34 | 35 | // Level the player to the next level if it has enough experience 36 | // to do so, returning true if it could level up and false otherwise. 37 | bool levelUp(); 38 | 39 | // Create a Json object representation of the player 40 | JsonBox::Object toJson(); 41 | 42 | // Save the player to a file named after them 43 | void save(EntityManager* mgr); 44 | 45 | // Attempt to load all data from the JSON value 46 | void load(JsonBox::Value& saveData, EntityManager* mgr); 47 | void loadArea(JsonBox::Value& areaData, EntityManager* mgr); 48 | }; 49 | 50 | #endif /* PLAYER_HPP */ 51 | -------------------------------------------------------------------------------- /src/battle.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BATTLE_HPP 2 | #define BATTLE_HPP 3 | 4 | #include 5 | 6 | #include "dialogue.hpp" 7 | 8 | class Creature; 9 | 10 | // Possible event types, should equate to what the player 11 | // can do in a battle 12 | enum class BattleEventType { ATTACK, DEFEND }; 13 | 14 | class BattleEvent 15 | { 16 | public: 17 | 18 | // Creature that initiated the event, e.g. the attacker 19 | Creature* source; 20 | // Creature being affected, e.g. the one being attacked 21 | Creature* target; 22 | // Type of event, e.g. attack, or defense 23 | BattleEventType type; 24 | 25 | // Constructor 26 | BattleEvent(Creature* source, Creature* target, BattleEventType type); 27 | 28 | // Convert the event type to the corresponding function and call it 29 | // on the source and target 30 | int run(); 31 | }; 32 | 33 | class Battle 34 | { 35 | private: 36 | 37 | // All the creatures that are participating in the fight. 38 | // We assume the player is a Creature with id "player". 39 | // A vector is used because we need to get the nth element 40 | // for use with a Dialogue 41 | std::vector combatants; 42 | 43 | // Actions that the player can take in the battle 44 | Dialogue battleOptions; 45 | 46 | // Remove a creature from the combatants list, and report that it's dead 47 | void kill(Creature* creature); 48 | 49 | // Run the next turn for the enemies and the player. 50 | // Computes what the enemies should do and asks for the player's 51 | // action, then compiles an event queue of the actions before 52 | // proceeding through the queue and running each action. 53 | void nextTurn(); 54 | 55 | public: 56 | 57 | // Constructor 58 | Battle(std::vector& combatants); 59 | 60 | // Run the battle until either the player dies, or all the opposing 61 | // combatants do 62 | void run(); 63 | }; 64 | 65 | #endif /* BATTLE_HPP */ 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpp-rpg-tutorial 2 | 3 | *This is an old tutorial written several years ago for a website which no longer exists. There are no guarantees that 4 | it adheres to the modern best-practices, and it could do with several improvements, but I have migrated it here in 5 | case people still find it useful.* 6 | 7 | Source code for my old C++ RPG tutorial, now contained in the tutorial folder. The master branch is 8 | (i.e. should be) up to date with the tutorial itself, and the other branches are for development and new additions. 9 | Code is distributed according to the MIT license, so basically do what you want as long as you preserve the copyright 10 | notices and give credit, other than that feel free to fork and use! 11 | 12 | This project uses JsonBox for JSON manipulation, which the license file `JsonBox-license.txt` applies to. JsonBox should 13 | be installed separately. 14 | 15 | ## Building the code 16 | 17 | The following script will clone the required repositories and build the project's source on \*nix systems. 18 | You'll need to have `git`, `clang` and `cmake` installed for the script to work. 19 | 20 | ```bash 21 | # Clone jsonbox & this tutorial 22 | git clone https://github.com/anhero/JsonBox.git 23 | git clone https://github.com/Piepenguin1995/cpp-rpg-tutorial.git 24 | 25 | # Build JsonBox 26 | cd JsonBox 27 | cmake . && make 28 | 29 | # Copy the include files and the library file to the tutorial folder 30 | cd .. 31 | cp -r JsonBox/include cpp-rpg-tutorial 32 | cp JsonBox/Export.h cpp-rpg-tutorial/include 33 | cp JsonBox/libJsonBox.a cpp-rpg-tutorial 34 | 35 | # Build the source using clang 36 | cd cpp-rpg-tutorial/src 37 | clang++ -std=c++11 main.cpp area.cpp armor.cpp battle.cpp creature.cpp door.cpp entity_manager.cpp inventory.cpp item.cpp player.cpp weapon.cpp ../libJsonBox.a -I ../include/ -rpath ../ -o ../rpg.out 38 | 39 | # Run the game 40 | cd .. 41 | ./rpg.out 42 | ``` 43 | -------------------------------------------------------------------------------- /src/creature.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CREATURE_HPP 2 | #define CREATURE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "entity.hpp" 9 | #include "inventory.hpp" 10 | 11 | class Area; 12 | class EntityManager; 13 | class Weapon; 14 | class Armor; 15 | class Door; 16 | 17 | class Creature : public Entity 18 | { 19 | public: 20 | 21 | // Name of the creature 22 | std::string name; 23 | 24 | // Creature stats 25 | int hp; 26 | int maxHp; 27 | int strength; 28 | int agility; 29 | double evasion; 30 | unsigned int xp; 31 | 32 | // Items that the creature possesses 33 | Inventory inventory; 34 | 35 | // Currently equipped weapon. Used as a pointer to an atlas entry, 36 | // but not necessary. nullptr denotes that no weapon is equipped 37 | Weapon* equippedWeapon; 38 | 39 | // Currently equipped armor 40 | Armor* equippedArmor; 41 | 42 | // Area the creature resides in. Used for player motion but also could 43 | // be used for enemy AI 44 | std::string currentArea; 45 | 46 | // Constructors 47 | Creature(std::string id, std::string name, int hp, int strength, int agility, double evasion, 48 | unsigned int xp); 49 | Creature(std::string id, JsonBox::Value& v, EntityManager* mgr); 50 | 51 | // Equip a weapon by setting the equipped weapon pointer. Currently 52 | // a pointless function (simple enough to be rewritten each time) 53 | // but handy if dual wielding is ever added, or shields etc 54 | void equipWeapon(Weapon* weapon); 55 | 56 | // Equip the armor into it's correct slot. A slightly more useful 57 | // function! 58 | void equipArmor(Armor* armor); 59 | 60 | // Convert internal area id into a pointer 61 | Area* getAreaPtr(EntityManager* mgr); 62 | 63 | // Attack the target creature, reducing their health if necessary 64 | int attack(Creature* target); 65 | 66 | // Go through a door 67 | // 0 = Door is locked 68 | // 1 = Door unlocked using key 69 | // 2 = Door is open 70 | int traverse(Door* door); 71 | 72 | // Create a JSON object containing the creature data 73 | virtual JsonBox::Object toJson(); 74 | 75 | // Attempt to load all data from the JSON value 76 | virtual void load(JsonBox::Value& v, EntityManager* mgr); 77 | }; 78 | 79 | #endif /* CREATURE_HPP */ 80 | -------------------------------------------------------------------------------- /src/dialogue.hpp: -------------------------------------------------------------------------------- 1 | #ifndef DIALOGUE_HPP 2 | #define DIALOGUE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // Gameplay is expressed using dialogues, which present a piece of 10 | // information and some responses, and the ask the user to pick one. If 11 | // they do not pick a valid one then the dialogue loops until they do 12 | class Dialogue 13 | { 14 | private: 15 | 16 | // Initial piece of information that the dialogue displays 17 | std::string description; 18 | 19 | // A vector of choices that will be outputted. No numbering is 20 | // necessary, the dialogue does that automatically 21 | std::vector choices; 22 | 23 | public: 24 | 25 | // Run the dialogue 26 | int activate() 27 | { 28 | // Output the information 29 | std::cout << description << std::endl; 30 | 31 | // Output and number the choices 32 | for(int i = 0; i < this->choices.size(); ++i) 33 | std::cout << i+1 << ": " << this->choices[i] << std::endl; 34 | 35 | // Repeatedly read input from stdin until a valid option is 36 | // chosen 37 | int userInput = -1; 38 | while(true) 39 | { 40 | std::cin >> userInput; 41 | // 'Valid' means within the range of numbers outputted 42 | if(userInput >= 0 && userInput <= this->choices.size()) 43 | { 44 | return userInput; 45 | } 46 | } 47 | } 48 | 49 | // Note that the vector is not passed by reference. Whilst that would 50 | // be more efficient, it forces us to create a vector outside of the 51 | // constructor. By passing by value we can call the constructor using 52 | // an initialisation list such as 53 | // Dialogue my_dialogue("Hello", {"Choice1", "Choice"}); 54 | Dialogue(std::string description, std::vector choices) 55 | { 56 | this->description = description; 57 | this->choices = choices; 58 | } 59 | 60 | // Create a dialogue from a JSON value 61 | Dialogue(JsonBox::Value& v) 62 | { 63 | JsonBox::Object o = v.getObject(); 64 | description = o["description"].getString(); 65 | for(auto choice : o["choices"].getArray()) 66 | choices.push_back(choice.getString()); 67 | } 68 | 69 | Dialogue() {} 70 | 71 | void addChoice(std::string choice) 72 | { 73 | this->choices.push_back(choice); 74 | } 75 | 76 | unsigned int size() 77 | { 78 | return this->choices.size(); 79 | } 80 | }; 81 | 82 | #endif /* DIALOGUE_HPP */ 83 | -------------------------------------------------------------------------------- /src/inventory.hpp: -------------------------------------------------------------------------------- 1 | #ifndef INVENTORY_HPP 2 | #define INVENTORY_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "entity_manager.hpp" 9 | 10 | class Item; 11 | class Weapon; 12 | class Armor; 13 | 14 | class Inventory 15 | { 16 | private: 17 | 18 | // We use a similar method here to in EntityManager where we store 19 | // a list of base pointers. We use a list and not a vector as inventories 20 | // are highly mutable. This way they can also be efficiently sorted. 21 | // The first element of the pair stores a pointer to the item in 22 | // the EntityManager, and the second element stores the quantity of the item 23 | std::list> items; 24 | 25 | // Given the Json value v which contains a list of items, weapons, or armor of type T 26 | // load the Ts into the storage list (either items, weapons, or armor) 27 | template 28 | void load(JsonBox::Value& v, EntityManager* mgr); 29 | 30 | // Return a JSON representation of all the items of the type T 31 | template 32 | JsonBox::Array jsonArray(); 33 | 34 | public: 35 | 36 | // Add an item to the inventory 37 | void add(Item* item, int count); 38 | 39 | // Remove the specified number of items from the inventory 40 | void remove(Item* item, int count); 41 | 42 | // Returns the count of the specified item 43 | int count(Item* item); 44 | template 45 | int count(unsigned int n); 46 | 47 | // Return the nth item in the storage list 48 | template 49 | T* get(unsigned int n); 50 | 51 | // Output a list of the items onto stdout, formatted nicely and 52 | // numbered if required 53 | template 54 | int print(bool label = false); 55 | 56 | // Remove all items from the inventory 57 | void clear(); 58 | 59 | // Merge the specified inventory with the current one, adding 60 | // item quantities together if they already exist and adding the item 61 | // into a new slot if they do not 62 | void merge(Inventory* inventory); 63 | 64 | // Load the inventory from a JSON value 65 | Inventory(JsonBox::Value& v, EntityManager* mgr); 66 | Inventory() {} 67 | 68 | // Print the entire inventory; items, then weapons, then armor, 69 | // but if the inventory is empty then output "Nothing" 70 | int print(bool label = false); 71 | 72 | // Get a Json object representation of the inventory 73 | JsonBox::Object getJson(); 74 | }; 75 | 76 | #endif /* INVENTORY_HPP */ 77 | -------------------------------------------------------------------------------- /src/entity_manager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "entity_manager.hpp" 5 | #include "item.hpp" 6 | #include "weapon.hpp" 7 | #include "armor.hpp" 8 | #include "creature.hpp" 9 | #include "area.hpp" 10 | #include "door.hpp" 11 | 12 | template 13 | void EntityManager::loadJson(std::string filename) 14 | { 15 | JsonBox::Value v; 16 | v.loadFromFile(filename); 17 | 18 | JsonBox::Object o = v.getObject(); 19 | for(auto entity : o) 20 | { 21 | std::string key = entity.first; 22 | this->data[key] = dynamic_cast(new T(key, entity.second, this)); 23 | } 24 | } 25 | 26 | template 27 | T* EntityManager::getEntity(std::string id) 28 | { 29 | // The id prefix should match to the type T, so take the 30 | // first characters of the id up to the length of the 31 | // prefix and compare the two 32 | if(id.substr(0, entityToString().size()) == entityToString()) 33 | return dynamic_cast(this->data.at(id)); 34 | else 35 | return nullptr; 36 | } 37 | 38 | EntityManager::EntityManager() {} 39 | 40 | EntityManager::~EntityManager() 41 | { 42 | for(auto& entity : this->data) 43 | { 44 | delete entity.second; 45 | } 46 | } 47 | 48 | // Template specialisations 49 | template <> std::string entityToString() { return "item"; } 50 | template <> std::string entityToString() { return "weapon"; } 51 | template <> std::string entityToString() { return "armor"; } 52 | template <> std::string entityToString() { return "creature"; } 53 | template <> std::string entityToString() { return "area"; } 54 | template <> std::string entityToString() { return "door"; } 55 | 56 | // Template instantiations 57 | template void EntityManager::loadJson(std::string); 58 | template void EntityManager::loadJson(std::string); 59 | template void EntityManager::loadJson(std::string); 60 | template void EntityManager::loadJson(std::string); 61 | template void EntityManager::loadJson(std::string); 62 | template void EntityManager::loadJson(std::string); 63 | 64 | template Item* EntityManager::getEntity(std::string); 65 | template Weapon* EntityManager::getEntity(std::string); 66 | template Armor* EntityManager::getEntity(std::string); 67 | template Creature* EntityManager::getEntity(std::string); 68 | template Area* EntityManager::getEntity(std::string); 69 | template Door* EntityManager::getEntity(std::string); 70 | 71 | -------------------------------------------------------------------------------- /src/area.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "area.hpp" 6 | #include "door.hpp" 7 | #include "entity.hpp" 8 | #include "inventory.hpp" 9 | #include "creature.hpp" 10 | #include "dialogue.hpp" 11 | #include "entity_manager.hpp" 12 | 13 | Area::Area(std::string id, Dialogue dialogue, Inventory items, 14 | std::vector creatures) : Entity(id) 15 | { 16 | this->dialogue = dialogue; 17 | this->items = items; 18 | for(auto creature : creatures) 19 | { 20 | this->creatures.push_back(*creature); 21 | } 22 | } 23 | 24 | Area::Area(std::string id, JsonBox::Value& v, EntityManager* mgr) : Entity(id) 25 | { 26 | this->load(v, mgr); 27 | } 28 | 29 | void Area::load(JsonBox::Value& v, EntityManager* mgr) 30 | { 31 | JsonBox::Object o = v.getObject(); 32 | 33 | // Build the dialogue 34 | // This is an optional parameter because it will not be saved 35 | // when the area is modified 36 | if(o.find("dialogue") != o.end()) 37 | this->dialogue = Dialogue(o["dialogue"]); 38 | 39 | // Build the inventory 40 | this->items = Inventory(o["inventory"], mgr); 41 | 42 | // Build the creature list 43 | this->creatures.clear(); 44 | for(auto creature : o["creatures"].getArray()) 45 | { 46 | // Create a new creature instance indentical to the version 47 | // in the entity manager 48 | Creature c(*mgr->getEntity(creature.getString())); 49 | this->creatures.push_back(c); 50 | } 51 | // Attach doors 52 | if(o.find("doors") != o.end()) 53 | { 54 | this->doors.clear(); 55 | for(auto door : o["doors"].getArray()) 56 | { 57 | Door* d = nullptr; 58 | // Each door is either an array of the type [id, locked] or 59 | // a single id string. 60 | if(door.isString()) 61 | { 62 | d = mgr->getEntity(door.getString()); 63 | } 64 | else 65 | { 66 | d = mgr->getEntity(door.getArray()[0].getString()); 67 | d->locked = door.getArray()[1].getInteger(); 68 | } 69 | this->doors.push_back(d); 70 | } 71 | } 72 | 73 | return; 74 | } 75 | 76 | JsonBox::Object Area::getJson() 77 | { 78 | JsonBox::Object o; 79 | // We don't need to save the dialogue because it doesn't change 80 | 81 | // Save the inventory 82 | o["inventory"] = this->items.getJson(); 83 | 84 | // Save the creatures 85 | JsonBox::Array a; 86 | for(auto creature : this->creatures) 87 | { 88 | a.push_back(JsonBox::Value(creature.id)); 89 | } 90 | o["creatures"] = a; 91 | 92 | // Save the doors 93 | a.clear(); 94 | for(auto door : this->doors) 95 | { 96 | JsonBox::Array d; 97 | d.push_back(door->id); 98 | d.push_back(door->locked); 99 | a.push_back(d); 100 | } 101 | o["doors"] = a; 102 | 103 | return o; 104 | } 105 | -------------------------------------------------------------------------------- /src/player.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "area.hpp" 6 | #include "player.hpp" 7 | #include "creature.hpp" 8 | #include "entity_manager.hpp" 9 | 10 | Player::Player(std::string name, int hp, int strength, int agility, double evasion, 11 | unsigned int xp, unsigned int level, std::string className) : 12 | Creature("player", name, hp, strength, agility, evasion, xp) 13 | { 14 | this->level = level; 15 | this->className = className; 16 | } 17 | 18 | Player::Player() : Player::Player("", 0, 0, 0, 0.0, 0, 1, "nullid") 19 | { 20 | } 21 | 22 | Player::Player(JsonBox::Value& saveData, JsonBox::Value& areaData, EntityManager* mgr) : Player::Player() 23 | { 24 | this->load(saveData, mgr); 25 | this->loadArea(areaData, mgr); 26 | } 27 | 28 | // Calculates the total experience required to reach a certain level 29 | unsigned int Player::xpToLevel(unsigned int level) 30 | { 31 | return (unsigned int)(1.5 * std::pow(this->level, 3)); 32 | } 33 | 34 | // Level the player to the next level if it has enough experience 35 | // to do so, returning true if it could level up and false otherwise. 36 | bool Player::levelUp() 37 | { 38 | // Can't level up if there's not enough experience 39 | if(this->xp < xpToLevel(this->level+1)) 40 | { 41 | return false; 42 | } 43 | 44 | // Advance to the next level 45 | ++level; 46 | 47 | // Variables to keep track of stat changes, and their associated 48 | // multipliers, which depend on the class. The multiplier affects 49 | // how much that stat increases each level, and is higher if the 50 | // class specialises in that stat 51 | // [hp, strength, agility] 52 | unsigned int statIncreases[3] = {0, 0, 0}; 53 | float statMultipliers[3] = {0, 0, 0}; 54 | statMultipliers[0] = 13.0; 55 | statMultipliers[1] = this->className == "Fighter" ? 8.0 : 6.0; 56 | statMultipliers[2] = this->className == "Rogue" ? 8.0 : 6.0; 57 | 58 | // Compute the stat increases for each stat 59 | for(int i = 0; i < 3; ++i) 60 | { 61 | float base = std::tanh(this->level / 30.0) * ((this->level % 2) + 1); 62 | statIncreases[i] += int(1 + statMultipliers[i] * base); 63 | } 64 | 65 | // Adjust all of the stats accordingly 66 | this->hp += statIncreases[0]; 67 | this->maxHp += statIncreases[0]; 68 | this->strength += statIncreases[1]; 69 | this->agility += statIncreases[2]; 70 | 71 | // Tell the user that they grew a level, what the increases were 72 | // and what their stats are now 73 | std::cout << this->name << " grew to level " << level << "!\n"; 74 | std::cout << "Health +" << statIncreases[0] << " -> " << this->maxHp << std::endl; 75 | std::cout << "Strength +" << statIncreases[1] << " -> " << this->strength << std::endl; 76 | std::cout << "Agility +" << statIncreases[2] << " -> " << this->agility << std::endl; 77 | std::cout << "----------------\n"; 78 | 79 | return true; 80 | } 81 | 82 | JsonBox::Object Player::toJson() 83 | { 84 | JsonBox::Object o = Creature::toJson(); 85 | 86 | o["className"] = JsonBox::Value(this->className); 87 | o["level"] = JsonBox::Value(int(this->level)); 88 | 89 | return o; 90 | } 91 | 92 | void Player::save(EntityManager* mgr) 93 | { 94 | // Construct JSON representation of the player 95 | // and save it to a file 96 | JsonBox::Value v(this->toJson()); 97 | v.writeToFile(this->name + ".json"); 98 | 99 | // Construct a JSON object containing the areas 100 | // the player has visited 101 | JsonBox::Object o; 102 | for(auto area : this->visitedAreas) 103 | { 104 | o[area] = mgr->getEntity(area)->getJson(); 105 | } 106 | JsonBox::Value v2(o); 107 | // Write the object to a file similar to the player data 108 | v2.writeToFile(this->name + "_areas.json"); 109 | 110 | return; 111 | } 112 | 113 | // Attempt to load all data from the JSON value 114 | void Player::load(JsonBox::Value& saveData, EntityManager* mgr) 115 | { 116 | // Load data shared with Creature 117 | Creature::load(saveData, mgr); 118 | 119 | // Load optional variables 120 | JsonBox::Object o = saveData.getObject(); 121 | 122 | this->className = o["className"].getString(); 123 | this->level = o["level"].getInteger(); 124 | 125 | return; 126 | } 127 | 128 | void Player::loadArea(JsonBox::Value& areaData, EntityManager* mgr) 129 | { 130 | // Load the area 131 | JsonBox::Object o = areaData.getObject(); 132 | for(auto area : o) 133 | { 134 | std::string key = area.first; 135 | mgr->getEntity(key)->load(area.second, mgr); 136 | this->visitedAreas.insert(key); 137 | } 138 | 139 | return; 140 | } 141 | -------------------------------------------------------------------------------- /src/creature.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "creature.hpp" 6 | #include "entity.hpp" 7 | #include "inventory.hpp" 8 | #include "weapon.hpp" 9 | #include "armor.hpp" 10 | #include "door.hpp" 11 | #include "area.hpp" 12 | #include "entity_manager.hpp" 13 | 14 | Creature::Creature(std::string id, std::string name, int hp, int strength, int agility, double evasion, 15 | unsigned int xp) : Entity(id) 16 | { 17 | this->name = name; 18 | this->hp = hp; 19 | this->maxHp = hp; 20 | this->strength = strength; 21 | this->agility = agility; 22 | this->evasion = evasion; 23 | this->equippedArmor = nullptr; 24 | this->equippedWeapon = nullptr; 25 | this->xp = xp; 26 | } 27 | 28 | Creature::Creature(std::string id, JsonBox::Value& v, EntityManager* mgr) : 29 | Creature(id, "", 0, 0, 0, 0, 0) 30 | { 31 | this->load(v, mgr); 32 | } 33 | 34 | void Creature::equipWeapon(Weapon* weapon) 35 | { 36 | this->equippedWeapon = weapon; 37 | 38 | return; 39 | } 40 | 41 | void Creature::equipArmor(Armor* armor) 42 | { 43 | this->equippedArmor = armor; 44 | 45 | return; 46 | } 47 | 48 | Area* Creature::getAreaPtr(EntityManager* mgr) 49 | { 50 | return mgr->getEntity(this->currentArea); 51 | } 52 | 53 | int Creature::attack(Creature* target) 54 | { 55 | // Damage done 56 | int damage = 0; 57 | 58 | if(double(std::rand()) / RAND_MAX > target->evasion) 59 | { 60 | // Calculate attack based on strength and weapon damage 61 | int attack = this->strength + (this->equippedWeapon == nullptr ? 0 : this->equippedWeapon->damage); 62 | // Calculate defense based on agility and armor defense 63 | int defense = target->agility + (target->equippedArmor == nullptr ? 0 : target->equippedArmor->defense); 64 | // 1/32 chance of a critical hit 65 | if(std::rand() % 32 == 0) 66 | { 67 | // Ignore defense and do damage in range [attack/2, attack] 68 | damage = attack / 2 + std::rand() % (attack / 2 + 1); 69 | } 70 | else 71 | { 72 | // Normal hit so factor in defense 73 | int baseDamage = attack - defense / 2; 74 | // Do damage in range [baseDamage/4, baseDamage/2] 75 | damage = baseDamage / 4 + std::rand() % (baseDamage / 4 + 1); 76 | // If the damage is zero then have a 50% chance to do 1 damage 77 | if(damage < 1) 78 | { 79 | damage = std::rand() % 2; 80 | } 81 | } 82 | // Damage the target 83 | target->hp -= damage; 84 | } 85 | 86 | return damage; 87 | } 88 | 89 | 90 | int Creature::traverse(Door* door) 91 | { 92 | int flag = 2; 93 | // Open the door if it is shut 94 | if(door->locked == 0) 95 | { 96 | door->locked = -1; 97 | flag = 2; 98 | } 99 | else if(door->locked > 0) 100 | { 101 | // Unlock and open the door if the creature has the key 102 | if(this->inventory.count(door->key)) 103 | { 104 | door->locked = -1; 105 | flag = 1; 106 | } 107 | // Creature does not have key so door remains locked 108 | else 109 | { 110 | return 0; 111 | } 112 | } 113 | if(door->areas.first == this->currentArea) 114 | { 115 | this->currentArea = door->areas.second; 116 | } 117 | else if(door->areas.second == this->currentArea) 118 | { 119 | this->currentArea = door->areas.first; 120 | } 121 | 122 | return flag; 123 | } 124 | 125 | JsonBox::Object Creature::toJson() 126 | { 127 | JsonBox::Object o; 128 | o["name"] = JsonBox::Value(this->name); 129 | o["hp"] = JsonBox::Value(this->hp); 130 | o["hp_max"] = JsonBox::Value(this->maxHp); 131 | o["strength"] = JsonBox::Value(this->strength); 132 | o["agility"] = JsonBox::Value(this->agility); 133 | o["evasion"] = JsonBox::Value(this->evasion); 134 | o["xp"] = JsonBox::Value(int(this->xp)); 135 | o["inventory"] = JsonBox::Value(this->inventory.getJson()); 136 | o["equipped_weapon"] = JsonBox::Value(this->equippedWeapon == nullptr ? "nullptr" : this->equippedWeapon->id); 137 | o["equipped_armor"] = JsonBox::Value(this->equippedArmor == nullptr ? "nullptr" : this->equippedArmor->id); 138 | 139 | return o; 140 | } 141 | 142 | void Creature::load(JsonBox::Value& v, EntityManager* mgr) 143 | { 144 | JsonBox::Object o = v.getObject(); 145 | this->name = o["name"].getString(); 146 | this->hp = o["hp"].getInteger(); 147 | if(o.find("hp_max") != o.end()) 148 | { 149 | this->maxHp = o["hp_max"].getInteger(); 150 | } 151 | else 152 | { 153 | this->maxHp = hp; 154 | } 155 | this->strength = o["strength"].getInteger(); 156 | this->agility = o["agility"].getInteger(); 157 | this->evasion = o["evasion"].getDouble(); 158 | this->xp = o["xp"].getInteger(); 159 | 160 | if(o.find("inventory") != o.end()) 161 | { 162 | this->inventory = Inventory(o["inventory"], mgr); 163 | } 164 | if(o.find("equipped_weapon") != o.end()) 165 | { 166 | std::string equippedWeaponName = o["equipped_weapon"].getString(); 167 | this->equippedWeapon = equippedWeaponName == "nullptr" ? nullptr : mgr->getEntity(equippedWeaponName); 168 | } 169 | if(o.find("equipped_armor") != o.end()) 170 | { 171 | std::string equippedArmorName = o["equipped_armor"].getString(); 172 | this->equippedArmor = equippedArmorName == "nullptr" ? nullptr : mgr->getEntity(equippedArmorName); 173 | } 174 | 175 | return; 176 | } 177 | -------------------------------------------------------------------------------- /src/inventory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "inventory.hpp" 8 | #include "item.hpp" 9 | #include "weapon.hpp" 10 | #include "armor.hpp" 11 | #include "entity_manager.hpp" 12 | 13 | template 14 | void Inventory::load(JsonBox::Value& v, EntityManager* mgr) 15 | { 16 | for(auto item : v.getArray()) 17 | { 18 | std::string itemId = item.getArray()[0].getString(); 19 | int quantity = item.getArray()[1].getInteger(); 20 | this->items.push_back(std::make_pair(mgr->getEntity(itemId), quantity)); 21 | } 22 | } 23 | 24 | template 25 | JsonBox::Array Inventory::jsonArray() 26 | { 27 | JsonBox::Array a; 28 | for(auto item : this->items) 29 | { 30 | // Skip if the id does not match to the type T 31 | if(item.first->id.substr(0, entityToString().size()) != entityToString()) 32 | continue; 33 | // Otherwise add the item to the array 34 | JsonBox::Array pair; 35 | pair.push_back(JsonBox::Value(item.first->id)); 36 | pair.push_back(JsonBox::Value(item.second)); 37 | a.push_back(JsonBox::Value(pair)); 38 | } 39 | 40 | return a; 41 | } 42 | 43 | void Inventory::add(Item* item, int count) 44 | { 45 | for(auto& it : this->items) 46 | { 47 | if(it.first->id == item->id) 48 | { 49 | it.second += count; 50 | return; 51 | } 52 | } 53 | this->items.push_back(std::make_pair(item, count)); 54 | } 55 | 56 | void Inventory::remove(Item* item, int count) 57 | { 58 | // Iterate through the items, and if they are found then decrease 59 | // the quantity by the quantity removed 60 | for(auto it = this->items.begin(); it != this->items.end(); ++it) 61 | { 62 | if((*it).first->id == item->id) 63 | { 64 | (*it).second -= count; 65 | if((*it).second < 1) this->items.erase(it); 66 | return; 67 | } 68 | } 69 | } 70 | 71 | template 72 | T* Inventory::get(unsigned int n) 73 | { 74 | // Using a list so we don't have random access, and must 75 | // step through n times from the start instead 76 | unsigned int i = 0; 77 | auto it = this->items.begin(); 78 | for(; it != this->items.end(); ++it) 79 | { 80 | if((*it).first->id.substr(0, entityToString().size()) != entityToString()) 81 | continue; 82 | if(i++ == n) break; 83 | } 84 | if(it != this->items.end()) 85 | return dynamic_cast((*it).first); 86 | else 87 | return nullptr; 88 | } 89 | 90 | int Inventory::count(Item* item) 91 | { 92 | for(auto it : this->items) 93 | { 94 | if(it.first->id == item->id) 95 | return it.second; 96 | } 97 | return 0; 98 | } 99 | 100 | template 101 | int Inventory::count(unsigned int n) 102 | { 103 | return count(get(n)); 104 | } 105 | 106 | template 107 | int Inventory::print(bool label) 108 | { 109 | unsigned int i = 1; 110 | 111 | for(auto it : this->items) 112 | { 113 | // Skip if the id does not match to the type T 114 | if(it.first->id.substr(0, entityToString().size()) != entityToString()) 115 | continue; 116 | // Number the items if asked 117 | if(label) std::cout << i++ << ": "; 118 | // Output the item name, quantity and description, e.g. 119 | // Gold Piece (29) - Glimmering discs of wealth 120 | std::cout << it.first->name << " (" << it.second << ") - "; 121 | std::cout << it.first->description << std::endl; 122 | } 123 | 124 | // Return the number of items outputted, for convenience 125 | return this->items.size(); 126 | } 127 | 128 | // Overload of print to print all items when the template argument is empty 129 | int Inventory::print(bool label) 130 | { 131 | unsigned int i = 0; 132 | 133 | if(items.empty()) 134 | { 135 | std::cout << "Nothing" << std::endl; 136 | } 137 | else 138 | { 139 | i += print(label); 140 | i += print(label); 141 | i += print(label); 142 | } 143 | 144 | return i; 145 | } 146 | 147 | void Inventory::clear() 148 | { 149 | this->items.clear(); 150 | } 151 | 152 | void Inventory::merge(Inventory* inventory) 153 | { 154 | // You can't merge an inventory with itself! 155 | if(inventory == this) return; 156 | 157 | // Loop through the items to be added, and add them. Our addition 158 | // function will take care of everything else for us 159 | for(auto it : inventory->items) this->add(it.first, it.second); 160 | 161 | return; 162 | } 163 | 164 | Inventory::Inventory(JsonBox::Value& v, EntityManager* mgr) 165 | { 166 | JsonBox::Object o = v.getObject(); 167 | load(o["items"], mgr); 168 | load(o["weapons"], mgr); 169 | load(o["armor"], mgr); 170 | } 171 | 172 | JsonBox::Object Inventory::getJson() 173 | { 174 | JsonBox::Object o; 175 | 176 | o["items"] = JsonBox::Value(jsonArray()); 177 | o["weapons"] = JsonBox::Value(jsonArray()); 178 | o["armor"] = JsonBox::Value(jsonArray()); 179 | 180 | return o; 181 | } 182 | 183 | // Template instantiations 184 | template void Inventory::load(JsonBox::Value&, EntityManager*); 185 | template void Inventory::load(JsonBox::Value&, EntityManager*); 186 | template void Inventory::load(JsonBox::Value&, EntityManager*); 187 | 188 | template JsonBox::Array Inventory::jsonArray(); 189 | template JsonBox::Array Inventory::jsonArray(); 190 | template JsonBox::Array Inventory::jsonArray(); 191 | 192 | template int Inventory::count(unsigned int); 193 | template int Inventory::count(unsigned int); 194 | template int Inventory::count(unsigned int); 195 | 196 | template Item* Inventory::get(unsigned int); 197 | template Weapon* Inventory::get(unsigned int); 198 | template Armor* Inventory::get(unsigned int); 199 | 200 | template int Inventory::print(bool); 201 | template int Inventory::print(bool); 202 | template int Inventory::print(bool); 203 | -------------------------------------------------------------------------------- /src/battle.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "battle.hpp" 8 | #include "creature.hpp" 9 | #include "dialogue.hpp" 10 | 11 | BattleEvent::BattleEvent(Creature* source, Creature* target, BattleEventType type) 12 | { 13 | this->source = source; 14 | this->target = target; 15 | this->type = type; 16 | } 17 | 18 | int BattleEvent::run() 19 | { 20 | switch(type) 21 | { 22 | case BattleEventType::ATTACK: 23 | return source->attack(target); 24 | case BattleEventType::DEFEND: 25 | return 0; 26 | default: 27 | return 0; 28 | } 29 | } 30 | 31 | void Battle::kill(Creature* creature) 32 | { 33 | // Find the creature's position in the combatants vector 34 | auto pos = std::find(this->combatants.begin(), this->combatants.end(), creature); 35 | 36 | // Don't try and delete the creature if it doesn't exist 37 | if(pos != this->combatants.end()) 38 | { 39 | std::cout << creature->name << " is slain!\n"; 40 | 41 | // Health == 0 is used in main as a condition to check if the creature is 42 | // dead, but this function could be called when the creature is not killed 43 | // by reducing their health to zero (by a death spell, for example), so we 44 | // ensure the creature's health is 0 and is marked as dead 45 | creature->hp = 0; 46 | // Remove the creature 47 | this->combatants.erase(pos); 48 | } 49 | 50 | return; 51 | } 52 | 53 | Battle::Battle(std::vector& combatants) 54 | { 55 | this->combatants = combatants; 56 | 57 | // Construct the menu 58 | this->battleOptions = Dialogue("What will you do?", 59 | { 60 | "Attack", 61 | "Defend" 62 | }); 63 | 64 | // Store the unique creature names and whether there is 65 | // only one or more of them. This code assumes that the 66 | // creatures have not already been assigned unique names, 67 | // which should be the case as a battle cannot be left 68 | // and then resumed again 69 | std::map names; 70 | for(auto com : this->combatants) 71 | { 72 | // Skip the player, who shouldn't be renamed 73 | if(com->id == "player") continue; 74 | // If the name hasn't been recorded and the creature 75 | // isn't the player, record the name 76 | if(names.count(com->name) == 0) 77 | { 78 | names[com->name] = 0; 79 | } 80 | // If there is already one creature, record there are being 81 | // more than one. We don't want the actual number, simply 82 | // that there's more and so we should label them. 83 | else if(names[com->name] == 0) 84 | { 85 | names[com->name] = 1; 86 | } 87 | } 88 | 89 | // Creature unique names for the combatants 90 | for(auto& com : this->combatants) 91 | { 92 | std::string newName = com->name; 93 | // If the name is marked as being shared by more than 94 | // one creature 95 | if(names.count(com->name) > 0 && names[com->name] > 0) 96 | { 97 | // Append (1) to the end of the name, and then increase the 98 | // number for the next creature 99 | newName += " (" + std::to_string(names[com->name]) + ")"; 100 | names[com->name] += 1; 101 | } 102 | // Change the creature name to the new one, which might just be 103 | // the same as the original 104 | com->name = newName; 105 | } 106 | } 107 | 108 | void Battle::run() 109 | { 110 | std::vector::iterator player; 111 | std::vector::iterator end; 112 | do 113 | { 114 | // Continue the battle until either the player dies, 115 | // or there is only the player left 116 | player = std::find_if(this->combatants.begin(), this->combatants.end(), 117 | [](Creature* a) { return a->id == "player"; }); 118 | end = this->combatants.end(); 119 | 120 | this->nextTurn(); 121 | } 122 | while(player != end && this->combatants.size() > 1); 123 | 124 | return; 125 | } 126 | 127 | void Battle::nextTurn() 128 | { 129 | // Queue of battle events. Fastest combatants will be 130 | // at the start of the queue, and so will go first, 131 | // whereas slower ones will be at the back 132 | std::queue events; 133 | 134 | // Sort the combatants in agility order 135 | std::sort(combatants.begin(), combatants.end(), [](Creature* a, Creature* b) { return a->agility > b->agility; }); 136 | 137 | // Iterate over the combatants and decide what they should do, 138 | // before adding the action to the event queue. 139 | for(auto com : this->combatants) 140 | { 141 | if(com->id == "player") 142 | { 143 | // Create the target selection dialogue 144 | Dialogue targetSelection = Dialogue("Who?", {}); 145 | // Created every turn because some combatants may die 146 | for(auto target : this->combatants) 147 | { 148 | if(target->id != "player") 149 | { 150 | targetSelection.addChoice(target->name); 151 | } 152 | } 153 | 154 | // Ask the player for their action (attack or defend) 155 | int choice = this->battleOptions.activate(); 156 | 157 | switch(choice) 158 | { 159 | default: 160 | case 1: 161 | { 162 | // Player is attacking, so ask for the target. 163 | // Dialogue returns the number of the choice but with 164 | // the player removed, so we have to do some fancy 165 | // arithmetic to find the actual location of the target 166 | // and then convert that to a pointer 167 | int position = targetSelection.activate(); 168 | for(int i = 0; i < position; ++i) 169 | { 170 | if(this->combatants[i]->id == "player") ++position; 171 | } 172 | Creature* target = this->combatants[position-1]; 173 | // Add the attack command to the event queue 174 | events.push(BattleEvent(com, target, BattleEventType::ATTACK)); 175 | break; 176 | } 177 | case 2: 178 | { 179 | // Player is defending, so do nothing 180 | events.push(BattleEvent(com, nullptr, BattleEventType::DEFEND)); 181 | break; 182 | } 183 | } 184 | } 185 | else 186 | { 187 | // Simple enemy AI where enemy constantly attacks player 188 | Creature* player = *std::find_if(this->combatants.begin(), this->combatants.end(), 189 | [](Creature* a) { return a->id == "player"; }); 190 | 191 | events.push(BattleEvent(com, player, BattleEventType::ATTACK)); 192 | } 193 | } 194 | 195 | // Take each event from the queue in turn and process them, 196 | // displaying the results 197 | while(!events.empty()) 198 | { 199 | // Take event from the front of the queue 200 | BattleEvent event = events.front(); 201 | switch(event.type) 202 | { 203 | case BattleEventType::ATTACK: 204 | { 205 | // The event can't be run if either the source or the 206 | // target were slain previously in this turn, so we 207 | // must check that they're valid first 208 | auto a = this->combatants.begin(); 209 | auto b = this->combatants.end(); 210 | if(std::find(a, b, event.source) == b || std::find(a, b, event.target) == b) 211 | { 212 | break; 213 | } 214 | std::cout << event.source->name 215 | << " attacks " 216 | << event.target->name 217 | << " for " 218 | << event.run() 219 | << " damage!\n"; 220 | // Delete slain enemies 221 | if(event.target->hp <= 0) 222 | { 223 | this->kill(event.target); 224 | } 225 | break; 226 | } 227 | case BattleEventType::DEFEND: 228 | std::cout << event.source->name 229 | << " defends!\n"; 230 | break; 231 | default: 232 | break; 233 | } 234 | events.pop(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /tutorial/01-intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | In this tutorial we will be creating a text-based RPG game using C++. It will be a classic dungeon-crawl game, where the player explores a vast network of rooms while fighting monsters and gathering loot. 4 | 5 | We will be using the C++11 standard quite heavily---so please ensure you have a complier that supports it---as well as making use of the Standard Template Library and templates in general. If you aren't experienced with these don't worry, I'll explain them the first time they're used. 6 | 7 | The code for this tutorial is available on [Github](https://github.com/Piepenguin1995/cpp-rpg-tutorial) and is licensed according to the MIT license so you're free to do pretty much whatever you want with it, though it's mostly there for use as a reference. 8 | 9 | ### JsonBox ### 10 | 11 | We'll also be making use of JSON files in order to store the data used by the game; these aren't especially complicated to understand or manipulate, but you will have to install the [JsonBox](https://github.com/anhero/JsonBox) library, for which you'll need [CMake](http://www.cmake.org/). 12 | 13 | On *nix systems installation is simple, just download the JsonBox code and run 14 | 15 | ```bash 16 | cmake . && make 17 | ``` 18 | 19 | then copy the `include` folder into the same folder as where you'll put your source code files and copy `libJsonBox.a` into wherever your put your library files. Then when you compile, make sure to link `libJsonBox.a` and include the headers. This'll depend on your compiler. 20 | 21 | For Windows refer to [this tutorial](http://www.cmake.org/runningcmake/), but essentially you run `cmake` and configure it to generate build files for your chosen IDE (i.e. Visual Studio), then use those to build a `.dll` file for JsonBox. 22 | 23 | #### Without Building the Library #### 24 | 25 | If you run into trouble installing the library then you can include it directly by downloading JsonBox and copying both the `include` and `src` folders into your project, then replacing all instances of `#include ` with `#include "JsonBox/something.h"` so the compiler knows where to find the header files. Your project layout should then look like 26 | 27 | ```bash 28 | project/ 29 | |-> src/ 30 | |-> Array.cpp 31 | |-> other-jsonbox-files 32 | |-> main.cpp 33 | |-> other-tutorial-files 34 | |-> JsonBox/ 35 | |-> Array.h 36 | |-> other-jsonbox-headers 37 | ``` 38 | 39 | Now you can compile the project as usual, and you won't have to link the library (although compilation times will be much higher). 40 | 41 | ### Overarching Design ### 42 | 43 | Before we get into writing any code, the first thing to do is consider how the game's code will be structured. For small projects, coding as you go along can be a viable strategy but for more complicated programs, especially games, taking a step back before diving in is crucial. 44 | 45 | The quick and dirty way would probably involve creating a bunch of disjoint classes, say one for the player, one for the enemies, one for the items, and so on, and then bundling them all together in a big game loop state machine which controlled the gameplay itself. Whilst this might work (and actually we're going to do something similar) it isn't likely to turn out too nicely, especially that big loop. 46 | 47 | Ideally we'd decouple the game from the game engine, so that whilst the mechanics of the game are all handled by the program, they're handled in such an way as to make adding an additional creature or area of the game world require very little work on the part of the programmer. This is the approach we'll be taking, by defining things as abstractly as possible in the code, and then implementing the game itself in JSON files, which the program will load and read at runtime. This has the added bonus of not having to recompile the code every time you want to make a small change to the game. 48 | 49 | So that we know what we're working towards, let's examine a small JSON file. 50 | 51 | ```json 52 | { 53 | "door_01_02": { 54 | "description": "sturdy wooden door", 55 | "areas": ["area_01", "area_02"], 56 | "locked": 1, 57 | "key": "item_iron_key" 58 | } 59 | } 60 | ``` 61 | 62 | The file begins with `{`, which defines the start of a new JSON object. This object then contains a series of *keys* (in this case only one) written as a string, which have associated *values*. To denote a key, we put a `:` at the end of a string, and then start a new JSON value. A JSON value can be any of the standard data types in C++ as well as a string (but not a key), an array, or even another JSON object. Arrays are much more flexible in JSON than in C++, as they can contain any combination of values, as well as any type of value besides a JSON object. For example, the following is a perfectly valid array. 63 | 64 | ```json 65 | [ 66 | ["foo", 7], 67 | ["bar", 3.5, "true"], 68 | 8 69 | ] 70 | ``` 71 | 72 | We will call anything defined by data in a JSON file an *entity*, and all such entities will be derived from a base `Entity` class which will establish their common functionality. We will use an `EntityManager` class to handle the individual entities, including loading them from their JSON files and giving the rest of the program access to them. 73 | 74 | Using this entity system we will create `Item`, `Weapon`, `Armor`, `Creature`, `Area`, `Door`, and `Player` entities. Items, weapons, and armor will be collectable and useable by the player, and will increase their effectiveness in battle against different creatures as they progress through the dungeon that makes up the game world. This dungeon will be constructed from various different areas---which are like physical rooms but a bit more general---each of which will contain creatures to fight, items to collect, and doors to step through into the next area. 75 | 76 | ### Formatting ### 77 | 78 | Finally a word about the formatting used in this tutorial and across the rest of the site. Any code will be placed inside code boxes like the JSON snippets are above. If that code forms an entire file and is not a snippet, it will have line numbers, otherwise the line will be unnumbered. Additionally, all code blocks containing code for you to type will have a comment at the top telling you which file they should go in, with the exception of JSON files. (Official JSON does not support comments, and it's pretty clear from context anyway.) 79 | 80 | ```cpp 81 | /* main.cpp */ 82 | // This is the entire file main.cpp 83 | int main() 84 | { 85 | int foo = 10; 86 | return foo; 87 | } 88 | ``` 89 | 90 | ```cpp 91 | /* main.cpp */ 92 | // This is a snippet from main.cpp 93 | int foo = 10; 94 | return foo; 95 | ``` 96 | 97 | When it comes to describing code in text instead of in boxes, I will refer to all C++ keywords, types, classes, objects, and so on, in `code formatting`. For example, later in this tutorial we'll be using an area system made of `Area`s to move the player character of type `Player`. I'll be a little loose with this, and may occasionally call the player the `Player`, but generally when referring to the code version of a term I'll use code formatting and will not otherwise. Important terms will also be *italicised* the first time they are used; I will rarely---if ever---use italics for emphasis. 98 | 99 | Oh, and whilst the code boxes are set up to allow you to copy and paste, **please** don't do that and instead type it out yourself. It's amazing how much simply writing things out improves your understanding of them! If instead you actually want a completed game that you don't understand, just download the source code and be done with it. Anyway with that over, let's begin! 100 | -------------------------------------------------------------------------------- /tutorial/03-items.md: -------------------------------------------------------------------------------- 1 | # Items 2 | 3 | The first type of entity that we will make is the `Item` class, which 4 | will be used for anything the player can pick up and collect in their 5 | adventure through the dungeon. We will also be using it as a base class 6 | for `Weapon`s and `Armor`, which as well as being collectible will also 7 | have more useful properties such as damage done or damage reduced. The 8 | `Item` class will define just the attributes they share, specifically a 9 | name and a description. 10 | 11 | ```cpp 12 | /* item.hpp */ 13 | #ifndef ITEM_HPP 14 | #define ITEM_HPP 15 | 16 | #include 17 | #include 18 | 19 | #include "entity.hpp" 20 | 21 | class EntityManager; 22 | 23 | class Item : public Entity 24 | { 25 | public: 26 | 27 | // Name and description of the item 28 | std::string name; 29 | std::string description; 30 | 31 | // Constructors 32 | Item(std::string id, std::string name, std::string description); 33 | Item(std::string id, JsonBox::Value& v, EntityManager* mgr); 34 | 35 | // Load the item information from the JSON value 36 | virtual void load(JsonBox::Value& v, EntityManager* mgr); 37 | }; 38 | 39 | #endif /* ITEM_HPP */ 40 | ``` 41 | 42 | `Item` of course inherits from the `Entity` base class, and so shares 43 | the `load` function as well as taking an `id` variable in its 44 | constructor arguments. The second constructor takes the same arguments 45 | as `load` and is required by the `EntityManager` as we discussed 46 | previously. We won't be using the first constructor because all `Item`s 47 | will be defined in JSON files, but its useful if you want to hardcode an 48 | `Item` into the game so I've included it. 49 | 50 | Note that like in `entity.hpp` we've forward declared the 51 | `EntityManager` class, because we need to know of its existence. Whilst 52 | `Item` isn't used directly by `EntityManager`, it is used in a template 53 | instantiation so one of the files has to forward declare instead of 54 | `#include`. 55 | 56 | The definitions of these functions are all rather simple, and are 57 | contained in the `item.cpp` file. 58 | 59 | ```cpp 60 | /* item.cpp */ 61 | #include 62 | #include 63 | 64 | #include "item.hpp" 65 | #include "entity.hpp" 66 | #include "entity_manager.hpp" 67 | 68 | Item::Item(std::string id, std::string name, std::string description) : Entity(id) 69 | { 70 | this->name = name; 71 | this->description = description; 72 | } 73 | 74 | Item::Item(std::string id, JsonBox::Value& v, EntityManager* mgr) : Entity(id) 75 | { 76 | this->load(v, mgr); 77 | } 78 | 79 | void Item::load(JsonBox::Value& v, EntityManager* mgr) 80 | { 81 | JsonBox::Object o = v.getObject(); 82 | this->name = o["name"].getString(); 83 | this->description = o["description"].getString(); 84 | 85 | return; 86 | } 87 | ``` 88 | 89 | The first constructor simply sets the name and description of the item 90 | according to the given arguments, and also calls the `Entity` 91 | constructor with the given id. The second calls the `Entity` constructor 92 | before calling the `load()` function in order to load the `Item` from 93 | the given JSON value. This will be a common pattern shared by all our 94 | derived entities. You might wonder why we're bothering to create a 95 | `load` function at all if its only ever called by one constructor, which 96 | does nothing else! Well we want to ensure that each derived entity has a 97 | way to load from JSON files, and C++ unfortunately doesn't let us create 98 | (pure) virtual constructors so we have to force it some other way. Sadly 99 | this means that we can still create derived classes which don't have a 100 | constructor compatible with the `EntityManager`, but the compilation 101 | error that occurs is pretty easy to understand so it isn't much of an 102 | issue. 103 | 104 | Note that the `JsonBox::Value` will not be an entire JSON file, but 105 | instead just the value corresponding to a single key extracted by the 106 | `EntityManager`. By converting the value into a `JsonBox::Object` we can 107 | access that value like an `std::map` and extract all the information we 108 | want, namely the item name and description. We then convert the returned 109 | values into strings (because `o["name"]` returns a `JsonBox::Value`), 110 | which are assigned to the `Item`. 111 | 112 | Note that here we are simply assuming the passed `JsonBox::Value` has a 113 | certain structure, which in general use is not guaranteed to be the 114 | case. To reduce complexity I haven't included any error handling, but 115 | really you should check here for correct input, as JsonBox doesn't do 116 | that for you. 117 | 118 | There's no use being able to load `Item`s if you haven't made any 119 | though! You'll need to create at least one JSON file containing the 120 | items and then load them using the `EntityManager`. I used a single 121 | `items.json` file. 122 | 123 | ```json 124 | { 125 | "item_gold_coin": { 126 | "name": "Gold Coin", 127 | "description": "A small disc made of lustrous metal" 128 | }, 129 | 130 | "item_iron_key": { 131 | "name": "Iron Key", 132 | "description": "A heavy iron key with a simple cut" 133 | } 134 | } 135 | ``` 136 | 137 | Now that the `Item` class is complete, we an implement two more 138 | entities; `Weapon` and `Armor`. These are pretty much identical to 139 | `Item` but each have an additional member variable corresponding to 140 | either the amount of damage they deal or the amount of incoming damage 141 | they reduce. The follow the same pattern as `Item`, but instead of 142 | calling the `Entity` constructor they call `Item`s. 143 | 144 | ```cpp 145 | /* armor.hpp */ 146 | #ifndef ARMOR_HPP 147 | #define ARMOR_HPP 148 | 149 | #include 150 | #include 151 | 152 | #include "item.hpp" 153 | 154 | class EntityManager; 155 | 156 | class Armor : public Item 157 | { 158 | public: 159 | 160 | int defense; 161 | 162 | // Constructors 163 | Armor(std::string id, std::string name, std::string description, int defense); 164 | Armor(std::string id, JsonBox::Value& v, EntityManager* mgr); 165 | 166 | // Load the armor from the Json value 167 | void load(JsonBox::Value& v, EntityManager* mgr); 168 | }; 169 | 170 | #endif /* ARMOR_HPP */ 171 | ``` 172 | 173 | ```cpp 174 | /* armor.cpp */ 175 | #include 176 | #include 177 | 178 | #include "armor.hpp" 179 | #include "item.hpp" 180 | #include "entity_manager.hpp" 181 | 182 | Armor::Armor(std::string id, std::string name, std::string description, int defense) : 183 | Item(id, name, description) 184 | { 185 | this->defense = defense; 186 | } 187 | 188 | Armor::Armor(std::string id, JsonBox::Value& v, EntityManager* mgr) : Item(id, v, mgr) 189 | { 190 | this->load(v, mgr); 191 | } 192 | 193 | void Armor::load(JsonBox::Value& v, EntityManager* mgr) 194 | { 195 | JsonBox::Object o = v.getObject(); 196 | this->defense = o["defense"].getInteger(); 197 | 198 | return; 199 | } 200 | ``` 201 | 202 | Additionally instead of using the `getString` function on the 203 | `JsonBox::Value` we've used `getInteger`, because `defense` is an `int` 204 | and not a `string`. JsonBox also provides `getFloat`, `getDouble`, 205 | `getBoolean`, and `getArray`, the last of which we will use later. 206 | 207 | `Weapon` is pretty much identical but with `defense` replaced with 208 | `damage`, so I won't waste space putting it here. For their JSON files I 209 | used separate `weapon.json` and `armor.json` files. (The last weapon is 210 | not for use by the player, but by a rat enemy that we will be creating 211 | later.) 212 | 213 | ```json 214 | { 215 | "weapon_dagger": { 216 | "name": "Dagger", 217 | "damage": 2, 218 | "description": "A small blade, probably made of iron. Keep the sharp end away from your body." 219 | }, 220 | 221 | "weapon_rat_claw": { 222 | "name": "_rat_claw", 223 | "damage": 1, 224 | "description": "" 225 | } 226 | } 227 | ``` 228 | 229 | ```json 230 | { 231 | "armor_leather": { 232 | "name": "Leather Armor", 233 | "defense": 1, 234 | "description": "Armor made from tanned animal hide. It smells a bit funny, but it'd do the job." 235 | } 236 | } 237 | ``` 238 | 239 | It's time now to start `main.cpp` I think, where we'll just create an 240 | instance of `EntityManager` and load the items. Before you do, make sure 241 | you add those explicit function template instantiations! 242 | 243 | ```cpp 244 | /* main.cpp */ 245 | #include 246 | #include 247 | #include 248 | 249 | #include "item.hpp" 250 | #include "weapon.hpp" 251 | #include "armor.hpp" 252 | #include "entity_manager.hpp" 253 | 254 | // Keeps track of items, weapons, creatures etc. 255 | EntityManager entityManager; 256 | 257 | int main() 258 | { 259 | // Load the entities 260 | entityManager.loadJson("items.json"); 261 | entityManager.loadJson("weapons.json"); 262 | entityManager.loadJson("armor.json"); 263 | 264 | // Seed the random number generator with the system time, so the 265 | // random numbers produced by rand() will be different each time 266 | std::srand(std::time(nullptr)); 267 | 268 | return 0; 269 | } 270 | ``` 271 | 272 | We've also seeded the random number generator, which we'll be using 273 | later. Usually the `rand` function returns the same set of random 274 | numbers every time the program is run (doesn't sound very random does 275 | it?), by giving it a different *seed* each time we can guarantee that 276 | the random numbers will be different. C++ now has its own more powerful 277 | [random number 278 | generator](http://en.cppreference.com/w/cpp/header/random) which we 279 | could use instead of C's `rand` function, but its needlessly complicated 280 | when we don't actually care how random our random numbers are. 281 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "item.hpp" 14 | #include "weapon.hpp" 15 | #include "armor.hpp" 16 | #include "inventory.hpp" 17 | #include "creature.hpp" 18 | #include "player.hpp" 19 | #include "dialogue.hpp" 20 | #include "area.hpp" 21 | #include "door.hpp" 22 | #include "battle.hpp" 23 | #include "entity_manager.hpp" 24 | 25 | // New character menu 26 | Player startGame(); 27 | 28 | // Character information menu, displays the items the player has, their 29 | // current stats etc. 30 | void dialogueMenu(Player& player); 31 | 32 | // Keeps track of items, weapons, creatures etc. 33 | EntityManager entityManager; 34 | 35 | int main() 36 | { 37 | // Load the entities 38 | entityManager.loadJson("items.json"); 39 | entityManager.loadJson("weapons.json"); 40 | entityManager.loadJson("armor.json"); 41 | entityManager.loadJson("creatures.json"); 42 | entityManager.loadJson("doors.json"); 43 | entityManager.loadJson("areas.json"); 44 | 45 | // Seed the random number generator with the system time, so the 46 | // random numbers produced by rand() will be different each time 47 | std::srand(std::time(nullptr)); 48 | 49 | Player player = startGame(); 50 | 51 | // Set the current area to be the first area in the atlas, 52 | // placing the player there upon game start 53 | player.currentArea = "area_01"; 54 | 55 | // Play the game until a function breaks the loop and closes it 56 | while(1) 57 | { 58 | // Mark the current player as visited 59 | player.visitedAreas.insert(player.currentArea); 60 | 61 | // Pointer to to the current area for convenience 62 | Area* areaPtr = player.getAreaPtr(&entityManager); 63 | 64 | // Autosave the game 65 | player.save(&entityManager); 66 | 67 | // If the area has any creatures in it, start a battle with them 68 | if(areaPtr->creatures.size() > 0) 69 | { 70 | // Create a vector of pointers to the creatures in the area 71 | std::vector combatants; 72 | std::cout << "You are attacked by "; 73 | for(int i = 0; i < areaPtr->creatures.size(); ++i) 74 | { 75 | Creature* c = &(areaPtr->creatures[i]); 76 | combatants.push_back(c); 77 | std::cout << c->name << (i == areaPtr->creatures.size()-1 ? "!\n" : ", "); 78 | } 79 | // Add the player to the combatant vector 80 | combatants.push_back(&player); 81 | // Run the battle 82 | Battle battle(combatants); 83 | battle.run(); 84 | 85 | // If the player is still alive, grant them some experience, assuming 86 | // that every creature was killed 87 | if(player.hp > 0) 88 | { 89 | // Or use std::accumulate, but that requires an additional header 90 | unsigned int xp = 0; 91 | for(auto creature : areaPtr->creatures) xp += creature.xp; 92 | std::cout << "You gained " << xp << " experience!\n"; 93 | player.xp += xp; 94 | // Remove the creatures from the area 95 | areaPtr->creatures.clear(); 96 | // Restart the loop to force a save, then the game will carry on 97 | // as usual 98 | continue; 99 | } 100 | // Otherwise player is dead, so end the program 101 | else 102 | { 103 | std::cout << "\t----YOU DIED----\n Game Over\n"; 104 | return 0; 105 | } 106 | } 107 | 108 | // Add the search and movement options to the dialogue 109 | Dialogue roomOptions = areaPtr->dialogue; 110 | for(auto door : areaPtr->doors) 111 | { 112 | roomOptions.addChoice("Go through the " + door->description); 113 | } 114 | roomOptions.addChoice("Search"); 115 | 116 | // Activate the current area's dialogue 117 | int result = roomOptions.activate(); 118 | 119 | if(result == 0) 120 | { 121 | dialogueMenu(player); 122 | } 123 | else if(result <= areaPtr->dialogue.size()) 124 | { 125 | // Add more events here 126 | } 127 | else if(result < roomOptions.size()) 128 | { 129 | Door* door = areaPtr->doors.at(result-areaPtr->dialogue.size()-1); 130 | int flag = player.traverse(door); 131 | 132 | switch(flag) 133 | { 134 | default: 135 | case 0: 136 | std::cout << "The " << door->description << " is locked." << std::endl; 137 | break; 138 | case 1: 139 | std::cout << "You unlock the " << door->description << " and go through it." << std::endl; 140 | break; 141 | case 2: 142 | std::cout << "You go through the " << door->description << "." << std::endl; 143 | break; 144 | } 145 | } 146 | else 147 | { 148 | std::cout << "You find:" << std::endl; 149 | areaPtr->items.print(); 150 | player.inventory.merge(&(areaPtr->items)); 151 | areaPtr->items.clear(); 152 | } 153 | } 154 | 155 | return 0; 156 | } 157 | 158 | // Create a new character or load an existing one 159 | Player startGame() 160 | { 161 | // Ask for a name and class 162 | // Name does not use a dialogue since dialogues only request options, 163 | // not string input. Could be generalised into its own TextInput 164 | // class, but not really necessary 165 | std::cout << "What's your name?" << std::endl; 166 | std::string name; 167 | std::cin >> name; 168 | 169 | // Check for existence then open using JsonBox if it exists 170 | std::ifstream f((name + ".json").c_str()); 171 | if(f.good()) 172 | { 173 | f.close(); 174 | // Load the player 175 | JsonBox::Value saveData; 176 | JsonBox::Value areaData; 177 | saveData.loadFromFile(name + ".json"); 178 | areaData.loadFromFile(name + "_areas.json"); 179 | Player player = Player(saveData, areaData, &entityManager); 180 | 181 | // Return the player 182 | return player; 183 | } 184 | else 185 | { 186 | f.close(); 187 | int result = Dialogue( 188 | "Choose your class", 189 | {"Fighter", "Rogue"}).activate(); 190 | 191 | switch(result) 192 | { 193 | // Fighter class favours strength 194 | case 1: 195 | return Player(name, 15, 5, 4, 1.0/64.0, 0, 1, "Fighter"); 196 | 197 | // Rogue class favours agility 198 | case 2: 199 | return Player(name, 15, 4, 5, 1.0/64.0, 0, 1, "Rogue"); 200 | 201 | // Default case that should never happen, but it's good to be safe 202 | default: 203 | return Player(name, 15, 4, 4, 1.0/64.0, 0, 1, "Adventurer"); 204 | } 205 | } 206 | } 207 | 208 | void dialogueMenu(Player& player) 209 | { 210 | // Output the menu 211 | int result = Dialogue( 212 | "Menu\n====", 213 | {"Items", "Equipment", "Character"}).activate(); 214 | 215 | switch(result) 216 | { 217 | // Print the items that the player owns 218 | case 1: 219 | std::cout << "Items\n=====\n"; 220 | player.inventory.print(); 221 | std::cout << "----------------\n"; 222 | break; 223 | // Print the equipment that the player is wearing (if they are 224 | // wearing anything) and then ask if they want to equip a weapon 225 | // or some armor 226 | case 2: 227 | { 228 | std::cout << "Equipment\n=========\n"; 229 | std::cout << "Armor: " 230 | << (player.equippedArmor != nullptr ? 231 | player.equippedArmor->name : "Nothing") 232 | << std::endl; 233 | std::cout << "Weapon: " 234 | << (player.equippedWeapon != nullptr ? 235 | player.equippedWeapon->name : "Nothing") 236 | << std::endl; 237 | 238 | int result2 = Dialogue( 239 | "", 240 | {"Equip Armor", "Equip Weapon", "Close"}).activate(); 241 | 242 | // Equipping armor 243 | if(result2 == 1) 244 | { 245 | int userInput = 0; 246 | 247 | // Cannot equip armor if they do not have any 248 | // Print a list of the armor and retrieve the amount 249 | // of armor in one go 250 | int numItems = player.inventory.print(true); 251 | if(numItems == 0) break; 252 | 253 | while(!userInput) 254 | { 255 | // Choose a piece of armor to equip 256 | std::cout << "Equip which item?" << std::endl; 257 | std::cin >> userInput; 258 | // Equipment is numbered but is stored in a list, 259 | // so the number must be converted into a list element 260 | if(userInput >= 1 && userInput <= numItems) 261 | { 262 | player.equipArmor(player.inventory.get(userInput-1)); 263 | } 264 | } 265 | } 266 | // Equip a weapon, using the same algorithms as for armor 267 | else if(result2 == 2) 268 | { 269 | int userInput = 0; 270 | int numItems = player.inventory.print(true); 271 | 272 | if(numItems == 0) break; 273 | 274 | while(!userInput) 275 | { 276 | std::cout << "Equip which item?" << std::endl; 277 | std::cin >> userInput; 278 | if(userInput >= 1 && userInput <= numItems) 279 | { 280 | player.equipWeapon(player.inventory.get(userInput-1)); 281 | } 282 | } 283 | } 284 | std::cout << "----------------\n"; 285 | break; 286 | } 287 | // Output the character information, including name, class (if 288 | // they have one), stats, level, and experience 289 | case 3: 290 | std::cout << "Character\n=========\n"; 291 | std::cout << player.name; 292 | if(player.className != "") std::cout << " the " << player.className; 293 | std::cout << std::endl; 294 | 295 | std::cout << "Health: " << player.hp << " / " << player.maxHp << std::endl; 296 | std::cout << "Strength: " << player.strength << std::endl; 297 | std::cout << "Agility: " << player.agility << std::endl; 298 | std::cout << "Level: " << player.level << " (" << player.xp; 299 | std::cout << " / " << player.xpToLevel(player.level+1) << ")" << std::endl; 300 | std::cout << "----------------\n"; 301 | break; 302 | default: 303 | break; 304 | } 305 | 306 | return; 307 | } 308 | -------------------------------------------------------------------------------- /tutorial/02-entities.md: -------------------------------------------------------------------------------- 1 | # Entities 2 | 3 | Now that we know what we're working towards, we can begin to build the 4 | program. At this stage it doesn't matter too much where we start, but 5 | since a lot of things will be using the entity manager we'll create that 6 | and the `Entity` class first. 7 | 8 | ```cpp 9 | /* entity.hpp */ 10 | #ifndef ENTITY_HPP 11 | #define ENTITY_HPP 12 | 13 | #include 14 | #include 15 | 16 | class EntityManager; 17 | 18 | class Entity 19 | { 20 | public: 21 | 22 | std::string id; 23 | 24 | Entity(std::string id) 25 | { 26 | this->id = id; 27 | } 28 | 29 | // Destructor must be made virtual as all derived classes are 30 | // treated as Entity in the EntityManager 31 | virtual ~Entity() {} 32 | 33 | // Pure virtual function stops Entity from being instantiated and forces it 34 | // to be implemented in all derived types 35 | virtual void load(JsonBox::Value& v, EntityManager* mgr) = 0; 36 | }; 37 | 38 | #endif /* ENTITY_HPP */ 39 | ``` 40 | 41 | First we have a forward declaration of the `EntityManager` class; this 42 | is necessary because whilst the `EntityManager` needs to know about the 43 | `Entity` class, each `Entity` may also need access to the 44 | `EntityManager`! The first JSON example shows a case when this occurs; 45 | to make a new door we need to know about another item found in the 46 | entity manager, namely the iron key. Because two files can't `#include` 47 | each other we use a forward declaration to tell the compiler that 48 | `EntityManager` will exist, even if it doesn't yet. 49 | 50 | The `id` member variable is a string that helps identify the object, and 51 | will be used as the entity's key in the JSON file as well as internally 52 | when getting an entity from the `EntityManager`. Next we have a 53 | constructor and a virtual destructor, neither of which do anything 54 | interesting. We have to make the destructor virtual because of how the 55 | `EntityManager` stores the entities, but we'll get to that soon. 56 | 57 | Finally we have a pure virtual function called `load`, which every 58 | entity derived from `Entity` must implement. By making it virtual we're 59 | allowing it to be overridden in the derived classes, and by making it 60 | pure (the `= 0`) we turn `Entity` into an *abstract class*. Abstract 61 | classes cannot have any objects, so whilst the `Item`, `Area`, 62 | `Creature` etc. entities we define in the future will all be a kind of 63 | `Entity`, we can't create an `Entity` by itself. One of the arguments is 64 | a `JsonBox::Value`, which is a representation of the entity being loaded 65 | in JSON form. In order to load `"door_01_02"` for example, we'll pass 66 | the `JsonBox::Value` described by the JSON value 67 | 68 | ```json 69 | { 70 | "description": "sturdy wooden door", 71 | "areas": ["area_01", "area_02"], 72 | "locked": 1, 73 | "key": "item_iron_key" 74 | } 75 | ``` 76 | 77 | Now for the `EntityManager` itself. 78 | 79 | ```cpp 80 | /* entity_manager.hpp */ 81 | #ifndef ENTITY_MANAGER_HPP 82 | #define ENTITY_MANAGER_HPP 83 | 84 | #include 85 | #include 86 | 87 | #include "entity.hpp" 88 | 89 | class EntityManager 90 | { 91 | private: 92 | 93 | std::map data; 94 | 95 | public: 96 | 97 | // Load the JSON file and determine which map to save the data to 98 | // according to the type T 99 | template 100 | void loadJson(std::string filename); 101 | 102 | // Return the entity given by id 103 | template 104 | T* getEntity(std::string id); 105 | 106 | // Constructor 107 | EntityManager(); 108 | 109 | // Destructor 110 | ~EntityManager(); 111 | }; 112 | 113 | // Convert a derived entity type to its id prefix. e.g. Item -> "item" 114 | template 115 | std::string entityToString(); 116 | 117 | #endif /* ENTITY_MANAGER_HPP */ 118 | ``` 119 | 120 | Each entity is stored in an `std::map`, which is accessible in ways very 121 | similar to an array, but instead of indexing each element by an integer 122 | each element (in this case a pointer to an `Entity`) is indexed by a 123 | string. This very nicely mirrors the structure of a JSON file, where 124 | each unique key gives a JSON value. An important thing to note is that 125 | whilst abstract classes such as `Entity` can't exist exist as objects, 126 | they can exist as pointers. But if we can't have any actual `Entity` 127 | objects, what do the pointers point to? 128 | 129 | C++ has a nice feature where a pointer to a derived class can be 130 | stripped back and reduced to a pointer to its base class. This means 131 | that we can create `Item` or `Door` entities and then store pointers to 132 | them in the `std::map`, even though the types don't match up. The 133 | problem is, we won't then be able to access any member variables or 134 | functions specific to `Item` or `Door`, because an `Entity` doesn't know 135 | about those. Luckily, C++ allows us to convert a pointer to a base class 136 | back to a pointer to a derived class, giving us access to all those 137 | member variables! 138 | 139 | We'll use that functionality in the `.cpp` file soon, but for now lets 140 | continue looking at `EntityManager`. Next we have a *function template* 141 | called `loadJson` which will read the JSON file `filename` and add all 142 | the entities described in that file to the `std::map`. The template 143 | argument passed to `loadJson` determines what kind of entity it should 144 | try and load from the file. `loadJson` won't handle any loading of 145 | individual entities, but instead will find all the keys in the JSON file 146 | and create a new entity by passing the corresponding value to the 147 | entity's constructor. This is where we'll be converting derived 148 | pointers to base pointers. 149 | 150 | Next we have `getEntity`, which when given an entity type and an id will 151 | find the entity with that id and return a pointer to it. This is where 152 | we'll be converting base pointers back to derived pointers. 153 | 154 | Finally there's a constructor and a destructor, which don't need much 155 | said about them! 156 | 157 | Outside of the `EntityManager` class we have an additional function 158 | template called `entityToString`. This function is specialised to each 159 | possible template argument `T`, and given an entity type---such as 160 | `Door` or `Item`---will return a string corresponding to that 161 | entity---such as `"door"` or `"item"`. 162 | 163 | ```cpp 164 | /* entity_manager.cpp */ 165 | #include 166 | #include 167 | 168 | #include "entity_manager.hpp" 169 | 170 | template 171 | void EntityManager::loadJson(std::string filename) 172 | { 173 | JsonBox::Value v; 174 | v.loadFromFile(filename); 175 | 176 | JsonBox::Object o = v.getObject(); 177 | for(auto entity : o) 178 | { 179 | std::string key = entity.first; 180 | this->data[key] = dynamic_cast(new T(key, entity.second, this)); 181 | } 182 | } 183 | 184 | template 185 | T* EntityManager::getEntity(std::string id) 186 | { 187 | // The id prefix should match to the type T, so take the 188 | // first characters of the id up to the length of the 189 | // prefix and compare the two 190 | if(id.substr(0, entityToString().size()) == entityToString()) 191 | return dynamic_cast(this->data.at(id)); 192 | else 193 | return nullptr; 194 | } 195 | 196 | EntityManager::EntityManager() {} 197 | 198 | EntityManager::~EntityManager() 199 | { 200 | for(auto& entity : this->data) 201 | { 202 | delete entity.second; 203 | } 204 | } 205 | 206 | ``` 207 | 208 | `loadJson` loads the JSON file as a JSON value (the entire set of data 209 | between the `{}`) and then converts it into a JSON object that will have 210 | keys and corresponding values. A `JsonBox::Object` is very similar to an 211 | `std::map`, where each element contains not just a value, but also the 212 | key. As we iterate over the object `entity` will contain both the key 213 | and the value, so we extract the key and then create a new entity using 214 | the corresponding value (of type `JsonBox::Value`). To create the entity 215 | we assume that the type `T` (which will be some class derived from 216 | `Entity` such as `Item` or `Door`) has a constructor with the same 217 | arguments as `Entity::load` and call it, passing to it the key, value, 218 | and the `EntityManager` itself. Because `data` contains pointers we use 219 | the `new` keyword to allocate memory for the new entity and then use 220 | `dynamic_cast` to covert the resulting pointer of type `T*` to 221 | a pointer of type `Entity*` which can then be stored in `data`. 222 | 223 | `getEntity` is shorter, and simply gets the entity with key `id` from 224 | `data` before using `dynamic_cast` to convert it from a pointer of 225 | type `Entity*` back to a pointer of type `T*`, which it then returns. 226 | Before doing so however it uses the `entityToString` function to check 227 | that the `id` matches up to the type `T`. It assumes that all 228 | `Item`s---for example---have an `id` beginning with `"item"`, and if 229 | they don't it will return a `nullptr` instead. This still assumes that 230 | the data is named correctly, of course, which we aren't enforcing! 231 | 232 | Then comes the constructor---which does nothing at all---and the 233 | destructor, which deallocates all the memory allocated by `new` in 234 | `loadJson`. That's all with this class for now, but whenever we add a 235 | new entity class you must make sure to add an explicit instantiation or 236 | specialisation for each of the function templates that corresponds to 237 | the new entity. When adding an `Item` class for example, you should add 238 | 239 | ```cpp 240 | // Specialisation 241 | template <> std::string entityToString() { return "item"; } 242 | 243 | // Instantiation 244 | template void EntityManager::loadJson(std::string); 245 | template Item* EntityManager::getEntity(std::string); 246 | ``` 247 | -------------------------------------------------------------------------------- /tutorial/07-player.md: -------------------------------------------------------------------------------- 1 | # Player 2 | 3 | The player can do everything that an ordinary creature can do, and so we 4 | will create a `Player` class that inherits from the `Creature` class but 5 | has a few player-specific properties. For example, ordinary `Creature`s 6 | do not need to be able to level up (the `xp` value is just the amount 7 | they will give to the player upon defeat, in this case) but `Player`s 8 | do. To that end we will add a `level` variable as well as appropriate 9 | `xpToLevel` and `levelUp` functions, which will calculate how much 10 | experience the player needs to level up, and will actually perform the 11 | levelling itself, respectively. 12 | 13 | By using JSON files we've made it very easy to extend the game, but 14 | we've also gained the ability to easily save and load game data. Whilst 15 | we don't want to modify the game files themselves, we can easily create 16 | a similar file that contains all the changes the player has made to the 17 | game world. We can then load this file using functions much like the 18 | usual JSON load functions, and use it to overwrite the original loaded 19 | game files, thereby incorporating all the changes that the player has 20 | made and creating a very simple save/load system! Because only the 21 | player can make these changes, we will add these loading functions to 22 | the `Player` class. This also allows us to have a separate save game per 23 | character, simply by naming the save file with the same name as the 24 | player. 25 | 26 | ```cpp 27 | /* player.hpp */ 28 | #ifndef PLAYER_HPP 29 | #define PLAYER_HPP 30 | 31 | #include 32 | #include 33 | #include 34 | 35 | #include "creature.hpp" 36 | 37 | class EntityManager; 38 | 39 | class Player : public Creature 40 | { 41 | public: 42 | 43 | // Name of the player's class 44 | // Class may be Fighter, Rogue etc 45 | std::string className; 46 | 47 | // Level of the player 48 | unsigned int level; 49 | 50 | // Ids of areas visited by the player 51 | std::unordered_set visitedAreas; 52 | 53 | // Constructors 54 | Player(std::string name, int hp, int strength, int agility, double evasion, 55 | unsigned int xp, unsigned int level, std::string className); 56 | Player(); 57 | Player(JsonBox::Value& saveData, JsonBox::Value& areaData, EntityManager* mgr); 58 | 59 | // Calculates the total experience required to reach a certain level 60 | unsigned int xpToLevel(unsigned int level); 61 | 62 | // Level the player to the next level if it has enough experience 63 | // to do so, returning true if it could level up and false otherwise. 64 | bool levelUp(); 65 | 66 | // Create a Json object representation of the player 67 | JsonBox::Object toJson(); 68 | 69 | // Save the player to a file named after them 70 | void save(EntityManager* mgr); 71 | 72 | // Attempt to load all data from the JSON value 73 | void load(JsonBox::Value& saveData, EntityManager* mgr); 74 | void loadArea(JsonBox::Value& areaData, EntityManager* mgr); 75 | }; 76 | 77 | #endif /* PLAYER_HPP */ 78 | ``` 79 | 80 | Two additional things to note are the `className` and `visitedAreas` 81 | variables. Like many RPGs, ours will contain a *class system* which is 82 | used to assign a role to the player, in this case either that of a 83 | *Rogue* or a *Fighter*. The player will be able to choose their class 84 | (not the same as the C++ `class`, sorry for the confusing terminology) 85 | when they first start the game, and this choice will affect which of 86 | their attributes increases the fastest when they level up. In our very 87 | simple system the Fighter will have their strength increase faster than 88 | their agility, and the Rogue will have their agility increase faster 89 | than their strength. This small choice is very easy to implement and 90 | adds a bit of depth to the game. The `visitedAreas` variable is tied to 91 | the saving system and will store the ids of each `Area` the player has 92 | visited (and hence possible affected). Each of these `Area`s is assumed 93 | to have been modified by the player, and so will be saved to the save 94 | file. We use the `std::unordered_set` class to store the ids because it 95 | ensures that each id is only stored once, and it also doesn't care about 96 | the order the ids are entered (and neither should we because JSON 97 | doesn't either). 98 | 99 | First we will look at the saving and loading functions. 100 | 101 | ```cpp 102 | /* player.cpp */ 103 | #include 104 | #include 105 | #include "JsonBox.h" 106 | 107 | #include "area.hpp" 108 | #include "player.hpp" 109 | #include "creature.hpp" 110 | #include "entity_manager.hpp" 111 | 112 | Player::Player(std::string name, int hp, int strength, int agility, double evasion, 113 | unsigned int xp, unsigned int level, std::string className) : 114 | Creature("player", name, hp, strength, agility, evasion, xp) 115 | { 116 | this->level = level; 117 | this->className = className; 118 | } 119 | 120 | Player::Player() : Player::Player("", 0, 0, 0, 0.0, 0, 1, "nullid") 121 | { 122 | } 123 | 124 | Player::Player(JsonBox::Value& saveData, JsonBox::Value& areaData, EntityManager* mgr) : Player::Player() 125 | { 126 | this->load(saveData, mgr); 127 | this->loadArea(areaData, mgr); 128 | } 129 | 130 | JsonBox::Object Player::toJson() 131 | { 132 | JsonBox::Object o = Creature::toJson(); 133 | 134 | o["className"] = JsonBox::Value(this->className); 135 | o["level"] = JsonBox::Value(int(this->level)); 136 | 137 | return o; 138 | } 139 | 140 | void Player::save(EntityManager* mgr) 141 | { 142 | // Construct JSON representation of the player 143 | // and save it to a file 144 | JsonBox::Value v(this->toJson()); 145 | v.writeToFile(this->name + ".json"); 146 | 147 | // Construct a JSON object containing the areas 148 | // the player has visited 149 | JsonBox::Object o; 150 | for(auto area : this->visitedAreas) 151 | { 152 | o[area] = mgr->getEntity(area)->getJson(); 153 | } 154 | JsonBox::Value v2(o); 155 | // Write the object to a file similar to the player data 156 | v2.writeToFile(this->name + "_areas.json"); 157 | 158 | return; 159 | } 160 | 161 | // Attempt to load all data from the JSON value 162 | void Player::load(JsonBox::Value& saveData, EntityManager* mgr) 163 | { 164 | // Load data shared with Creature 165 | Creature::load(saveData, mgr); 166 | 167 | // Load optional variables 168 | JsonBox::Object o = saveData.getObject(); 169 | 170 | this->className = o["className"].getString(); 171 | this->level = o["level"].getInteger(); 172 | 173 | return; 174 | } 175 | 176 | void Player::loadArea(JsonBox::Value& areaData, EntityManager* mgr) 177 | { 178 | // Load the area 179 | JsonBox::Object o = areaData.getObject(); 180 | for(auto area : o) 181 | { 182 | std::string key = area.first; 183 | mgr->getEntity(key)->load(area.second, mgr); 184 | this->visitedAreas.insert(key); 185 | } 186 | 187 | return; 188 | } 189 | ``` 190 | 191 | The constructors are quite self-explanatory, and just initialise the new 192 | member variables. The third constructor takes two `JsonBox::Value`s 193 | instead of one however, and calls two functions; the first is the actual 194 | save data for the player (their `hp`, `inventory`, and so on), whereas 195 | the second is for saving and loading the modified areas, as mentioned 196 | above. The `toJson` functions is also very simple, and just calls the 197 | `Creature` version of `toJson` before appending the new variables on the 198 | end. 199 | 200 | The `save` function takes care of both the standard and area saving. To 201 | save the modified areas, the function iterates over each of the 202 | `visitedAreas` and creates JSON representations of them, which are then 203 | added to a new JSON object using their id as the key. By using 204 | `std::unordered_set` we have assured that all the ids are unique, and so 205 | no overwriting will occur here. Finally the JSON object is written to a 206 | file named the same as the player but with an `"_areas"` suffix appended 207 | to it. Finally, the `loadArea` function does the reverse and iterates 208 | over all the areas saved in the `"player-name_areas.json"` file, adding 209 | each of them to the the `visitedAreas` list. It also uses the 210 | `EntityManager` to call the `load` function on each new area; this 211 | overwrites the original `Area` (which will have already been loaded by 212 | the `EntityManager` when the program started) with the changes made by 213 | the player, essentially loading the save. 214 | 215 | Next we have the two levelling functions. 216 | 217 | ```cpp 218 | /* player.cpp */ 219 | // Calculates the total experience required to reach a certain level 220 | unsigned int Player::xpToLevel(unsigned int level) 221 | { 222 | return (unsigned int)(1.5 * std::pow(this->level, 3)); 223 | } 224 | 225 | // Level the player to the next level if it has enough experience 226 | // to do so, returning true if it could level up and false otherwise. 227 | bool Player::levelUp() 228 | { 229 | // Can't level up if there's not enough experience 230 | if(this->xp < xpToLevel(this->level+1)) 231 | { 232 | return false; 233 | } 234 | 235 | // Advance to the next level 236 | ++level; 237 | 238 | // Variables to keep track of stat changes, and their associated 239 | // multipliers, which depend on the class. The multiplier affects 240 | // how much that stat increases each level, and is higher if the 241 | // class specialises in that stat 242 | // [hp, strength, agility] 243 | unsigned int statIncreases[3] = {0, 0, 0}; 244 | float statMultipliers[3] = {0, 0, 0}; 245 | statMultipliers[0] = 13.0; 246 | statMultipliers[1] = this->className == "Fighter" ? 8.0 : 6.0; 247 | statMultipliers[2] = this->className == "Rogue" ? 8.0 : 6.0; 248 | 249 | // Compute the stat increases for each stat 250 | for(int i = 0; i < 3; ++i) 251 | { 252 | float base = std::tanh(this->level / 30.0) * ((this->level % 2) + 1); 253 | statIncreases[i] += int(1 + statMultipliers[i] * base); 254 | } 255 | 256 | // Adjust all of the stats accordingly 257 | this->hp += statIncreases[0]; 258 | this->maxHp += statIncreases[0]; 259 | this->strength += statIncreases[1]; 260 | this->agility += statIncreases[2]; 261 | 262 | // Tell the user that they grew a level, what the increases were 263 | // and what their stats are now 264 | std::cout << this->name << " grew to level " << level << "!\n"; 265 | std::cout << "Health +" << statIncreases[0] << " -> " << this->maxHp << std::endl; 266 | std::cout << "Strength +" << statIncreases[1] << " -> " << this->strength << std::endl; 267 | std::cout << "Agility +" << statIncreases[2] << " -> " << this->agility << std::endl; 268 | std::cout << "----------------\n"; 269 | 270 | return true; 271 | } 272 | ``` 273 | 274 | In our RPG, the experience required to advance to level `n` is 275 | independent of class, and equal to `1.5 n^3`. (This is far from a 276 | perfect formula, and most systems are [far more 277 | complicated](http://bulbapedia.bulbagarden.net/wiki/Experience).) When 278 | the player levels up we first compute a *stat multiplier* for each 279 | attribute (hit points, strength, agility). The hit points multiplier is 280 | always 13.0, whereas the strength and agility multipliers are either 8.0 281 | or 6.0, depending on the player's class. Each multiplier `k` is then 282 | fed into the slightly scary formula 283 | 284 | ![Stat multiplier formula given by 1 + k * tanh(n / 30), all multiplied 285 | by n mod 2 + 1, then floored.](/tutorial/07-experience-formula.svg) 286 | 287 | This function and its cumulative form look like 288 | 289 | ![Attribute increases per level](/tutorial/increase-experience-graph.svg) 290 | ![Total attribute at given level](/tutorial/cumulative-experience-graph.svg) 291 | 292 | As you can see, the attribute increases spike at every other level but 293 | still slowly increases as the player's level goes up. This gives us a 294 | levelling system that seems a little bit random, but is actually quite 295 | regular. Each stat is then increased by the calculated value, and 296 | finally the new attributes and level are reported to the player. 297 | 298 | Don't add any functionality to load the player as an entity like with 299 | the `Creature`s, we'll be doing that in a different way to handle the 300 | save files. 301 | -------------------------------------------------------------------------------- /tutorial/06-creatures.md: -------------------------------------------------------------------------------- 1 | # Creatures 2 | 3 | We've almost got a playable game, I promise! I think that jumping 4 | backwards and forwards is quite a confusing way to learn, hence why 5 | we're building all the individual components separately and then joining 6 | them all together at the end. Anyway, in this section we'll begin to 7 | think about how fighting will work in this game, and we'll be creating a 8 | `Creature` class accordingly. 9 | 10 | The battle system in this game will be very simple; every fight will 11 | involve two sides---the player and the enemies---who will take turns 12 | attacking each other. Unlike in some RPGs a turn will not consist of all 13 | the enemies attacking and then the player attacking, but rather every 14 | creature involved will attack at a separate time independent of their 15 | side. To determine the order in which each creature attacks, we will use 16 | an *agility* value; the higher their agility, the earlier they attack. 17 | Once all creatures have attacked once, the turn starts again. 18 | 19 | When a creature attacks, they will not be guaranteed to hit and will 20 | instead have a chance to do damage that decreases as the defender's 21 | *evasion* increases. If the attacker hits the defender the amount of 22 | damage done will be determined by what weapon the attacker has equipped, 23 | how much *strength* the attacker has, and what armor the defender has 24 | equipped. It will also be modified by a small random factor to add a 25 | little bit of excitement into the mix! The amount of damage the attacker 26 | does will be subtracted from the defender's *hit points*, and when their 27 | *hit points* are reduced to zero the defender will die. 28 | 29 | With that in mind, the `Creature` class should have `hp`, `strength`, 30 | `agility`, `evasion`, `equippedWeapon`, and `equippedArmor` member 31 | variables. To allow for healing and restoration of hit points we should 32 | also have a `maxHp` value, and of course each `Creature` should also 33 | have a `name`. Finally, no RPG is complete without experience points and 34 | loot drops, so we'll also give each `Creature` an `xp` value and an 35 | `inventory`. It'll also be important to know which `Area` the `Creature` 36 | is in, and we should add a function to allow `Creature`s to move from 37 | one `Area` to another. 38 | 39 | ```cpp 40 | /* creature.hpp */ 41 | #ifndef CREATURE_HPP 42 | #define CREATURE_HPP 43 | 44 | #include 45 | #include 46 | #include 47 | 48 | #include "entity.hpp" 49 | #include "inventory.hpp" 50 | 51 | class Area; 52 | class EntityManager; 53 | class Weapon; 54 | class Armor; 55 | class Door; 56 | 57 | class Creature : public Entity 58 | { 59 | public: 60 | 61 | // Name of the creature 62 | std::string name; 63 | 64 | // Creature stats 65 | int hp; 66 | int maxHp; 67 | int strength; 68 | int agility; 69 | double evasion; 70 | unsigned int xp; 71 | 72 | // Items that the creature possesses 73 | Inventory inventory; 74 | 75 | // Currently equipped weapon. Used as a pointer to an atlas entry, 76 | // but not necessary. nullptr denotes that no weapon is equipped 77 | Weapon* equippedWeapon; 78 | 79 | // Currently equipped armor 80 | Armor* equippedArmor; 81 | 82 | // Area the creature resides in. Used for player motion but also could 83 | // be used for enemy AI 84 | std::string currentArea; 85 | 86 | // Constructors 87 | Creature(std::string id, std::string name, int hp, int strength, int agility, double evasion, 88 | unsigned int xp); 89 | Creature(std::string id, JsonBox::Value& v, EntityManager* mgr); 90 | 91 | // Equip a weapon by setting the equipped weapon pointer. Currently 92 | // a pointless function (simple enough to be rewritten each time) 93 | // but handy if dual wielding is ever added, or shields etc 94 | void equipWeapon(Weapon* weapon); 95 | 96 | // Equip the armor into it's correct slot. A slightly more useful 97 | // function! 98 | void equipArmor(Armor* armor); 99 | 100 | // Convert internal area id into a pointer 101 | Area* getAreaPtr(EntityManager* mgr); 102 | 103 | // Go through a door 104 | // 0 = Door is locked 105 | // 1 = Door unlocked using key 106 | // 2 = Door is open 107 | int traverse(Door* door); 108 | 109 | // Create a JSON object containing the creature data 110 | virtual JsonBox::Object toJson(); 111 | 112 | // Attempt to load all data from the JSON value 113 | virtual void load(JsonBox::Value& v, EntityManager* mgr); 114 | }; 115 | 116 | #endif /* CREATURE_HPP */ 117 | ``` 118 | 119 | Note that we've forward-declared each class that we're only storing a 120 | pointer to, but we've had to `#include "inventory.hpp"` because we are 121 | storing an actual instance of `Inventory` instead of a pointer. The 122 | usual constructors and JSON-related functions are present, but we've got 123 | a few more functions as well to make dealing with the `Creature` a 124 | little easier, most notably the `getAreaPtr` function. So far we've 125 | almost exclusively stored pointers to entities instead of their ids, but 126 | here we've stored the id instead, and have added a function to convert 127 | it to a pointer when necessary. Honestly there isn't much of a reason 128 | for this besides making things a little bit simpler later on, so feel 129 | free to change it back to the usual method. 130 | 131 | ```cpp 132 | /* creature.cpp */ 133 | #include 134 | #include 135 | #include 136 | 137 | #include "creature.hpp" 138 | #include "entity.hpp" 139 | #include "inventory.hpp" 140 | #include "weapon.hpp" 141 | #include "armor.hpp" 142 | #include "door.hpp" 143 | #include "area.hpp" 144 | #include "entity_manager.hpp" 145 | 146 | Creature::Creature(std::string id, std::string name, int hp, int strength, int agility, double evasion, 147 | unsigned int xp) : Entity(id) 148 | { 149 | this->name = name; 150 | this->hp = hp; 151 | this->maxHp = hp; 152 | this->strength = strength; 153 | this->agility = agility; 154 | this->evasion = evasion; 155 | this->equippedArmor = nullptr; 156 | this->equippedWeapon = nullptr; 157 | this->xp = xp; 158 | } 159 | 160 | Creature::Creature(std::string id, JsonBox::Value& v, EntityManager* mgr) : 161 | Creature(id, "", 0, 0, 0, 0, 0) 162 | { 163 | this->load(v, mgr); 164 | } 165 | 166 | void Creature::equipWeapon(Weapon* weapon) 167 | { 168 | this->equippedWeapon = weapon; 169 | 170 | return; 171 | } 172 | 173 | void Creature::equipArmor(Armor* armor) 174 | { 175 | this->equippedArmor = armor; 176 | 177 | return; 178 | } 179 | 180 | Area* Creature::getAreaPtr(EntityManager* mgr) 181 | { 182 | return mgr->getEntity(this->currentArea); 183 | } 184 | 185 | int Creature::traverse(Door* door) 186 | { 187 | int flag = 2; 188 | // Open the door if it is shut 189 | if(door->locked == 0) 190 | { 191 | door->locked = -1; 192 | flag = 2; 193 | } 194 | else if(door->locked > 0) 195 | { 196 | // Unlock and open the door if the creature has the key 197 | if(this->inventory.count(door->key)) 198 | { 199 | door->locked = -1; 200 | flag = 1; 201 | } 202 | // Creature does not have key so door remains locked 203 | else 204 | { 205 | return 0; 206 | } 207 | } 208 | if(door->areas.first == this->currentArea) 209 | { 210 | this->currentArea = door->areas.second; 211 | } 212 | else if(door->areas.second == this->currentArea) 213 | { 214 | this->currentArea = door->areas.first; 215 | } 216 | 217 | return flag; 218 | } 219 | 220 | JsonBox::Object Creature::toJson() 221 | { 222 | JsonBox::Object o; 223 | o["name"] = JsonBox::Value(this->name); 224 | o["hp"] = JsonBox::Value(this->hp); 225 | o["hp_max"] = JsonBox::Value(this->maxHp); 226 | o["strength"] = JsonBox::Value(this->strength); 227 | o["agility"] = JsonBox::Value(this->agility); 228 | o["evasion"] = JsonBox::Value(this->evasion); 229 | o["xp"] = JsonBox::Value(int(this->xp)); 230 | o["inventory"] = JsonBox::Value(this->inventory.getJson()); 231 | o["equipped_weapon"] = JsonBox::Value(this->equippedWeapon == nullptr ? "nullptr" : this->equippedWeapon->id); 232 | o["equipped_armor"] = JsonBox::Value(this->equippedArmor == nullptr ? "nullptr" : this->equippedArmor->id); 233 | 234 | return o; 235 | } 236 | 237 | void Creature::load(JsonBox::Value& v, EntityManager* mgr) 238 | { 239 | JsonBox::Object o = v.getObject(); 240 | this->name = o["name"].getString(); 241 | this->hp = o["hp"].getInteger(); 242 | if(o.find("hp_max") != o.end()) 243 | { 244 | this->maxHp = o["hp_max"].getInteger(); 245 | } 246 | else 247 | { 248 | this->maxHp = hp; 249 | } 250 | this->strength = o["strength"].getInteger(); 251 | this->agility = o["agility"].getInteger(); 252 | this->evasion = o["evasion"].getDouble(); 253 | this->xp = o["xp"].getInteger(); 254 | 255 | if(o.find("inventory") != o.end()) 256 | { 257 | this->inventory = Inventory(o["inventory"], mgr); 258 | } 259 | if(o.find("equipped_weapon") != o.end()) 260 | { 261 | std::string equippedWeaponName = o["equipped_weapon"].getString(); 262 | this->equippedWeapon = equippedWeaponName == "nullptr" ? nullptr : mgr->getEntity(equippedWeaponName); 263 | } 264 | if(o.find("equipped_armor") != o.end()) 265 | { 266 | std::string equippedArmorName = o["equipped_armor"].getString(); 267 | this->equippedArmor = equippedArmorName == "nullptr" ? nullptr : mgr->getEntity(equippedArmorName); 268 | } 269 | 270 | return; 271 | } 272 | ``` 273 | 274 | Most of the functions here are just variations on ones we've seen before 275 | (though there's a lot more data involved here), but `traverse` is more 276 | interesting. The `traverse` function requires a `Door` that the 277 | `Creature` is attempting to travel through. First it checks the `locked` 278 | property of the `door`. If the `door` is closed but unlocked or locked 279 | but the `Creature` has the key then it is opened. Once the door is 280 | opened the `Creature` is moved to the other `Area` linked to by the 281 | `door`. It makes sense to make `traverse` a member function of 282 | `Creature` and not `Door` because the `Creature` is the one doing the 283 | action (though it's perfectly possible to do it the other way around). 284 | 285 | Before moving on, don't forget to load the creatures from a JSON file 286 | similarly to the other entities we've created, and also add the template 287 | specialisations and instantiations to `entity_manager.cpp`. Since the 288 | `Creature` class makes use of `Weapon`s and `Armor`, load 289 | `creatures.json` after those. 290 | 291 | ```json 292 | { 293 | "creature_rat": { 294 | "name": "Rat", 295 | "hp": 3, 296 | "strength": 5, 297 | "agility": 3, 298 | "evasion": 0.015625, 299 | "xp": 1, 300 | "equipped_weapon": "weapon_rat_claw" 301 | } 302 | } 303 | ``` 304 | 305 | ### Creatures and Areas ### 306 | 307 | We've said that we want a battle system, but how is the player going to 308 | encounter all those other `Creature`s in the first place? Since they're 309 | entities that appear in the game world, it makes sense to add them to 310 | the `Area` class like everything else. This isn't very hard to do so 311 | I'll leave it mostly to you, but there is something you have to take 312 | into account that's easy to miss. 313 | 314 | Note that `Creature`s are the only entities that are both *modifiable* 315 | (`Area`s also are) and *multi-instance* (`Item`s also are). Modifying an 316 | `Area` was fine because each `Area` was only ever used once, and using 317 | the same `Item` multiple times was fine because they were never 318 | modified. `Creature`s however will be reused many times (imagine having 319 | to create a separate JSON entry every time a Rat appeared) but will also 320 | take damage, and so will be modified. If we store pointers to their 321 | positions in the Entity Manager, every Rat will be identical and when 322 | one Rat takes damage, they all will! Obviously this would be disastrous, 323 | so instead of storing pointers to `Creature`s in the `Area` we should 324 | store actual `Creature`s. The `Creature` constructor can still use 325 | pointers though, because we can leave the duplication up to the `Area` 326 | class. Hence 327 | 328 | ```cpp 329 | std::vector creatures; 330 | 331 | // But... 332 | 333 | Area(std::string id, Dialogue dialogue, Inventory items, 334 | std::vector creatures); 335 | ``` 336 | 337 | To build the member `creatures` list, iterate over the argument 338 | `creatures` list and add use `*` to dereference the creature and add it 339 | to the list. Now for the JSON changes. These aren't much harder, you 340 | just have to account for the presence of a `"creatures"` key, e.g. for 341 | `load` 342 | 343 | ```cpp 344 | // Build the creature list 345 | this->creatures.clear(); 346 | for(auto creature : o["creatures"].getArray()) 347 | { 348 | // Create a new creature instance indentical to the version 349 | // in the entity manager 350 | Creature c(*mgr->getEntity(creature.getString())); 351 | this->creatures.push_back(c); 352 | } 353 | ``` 354 | -------------------------------------------------------------------------------- /tutorial/08-tying-it-together.md: -------------------------------------------------------------------------------- 1 | # Tying It Together 2 | 3 | Finally, we are finished with the individual components of the game (at 4 | least up to the complexity suitable for this tutorial), and can move on 5 | and actually create something playable! Now then, most of this section 6 | will deal with integrating all the classes we've built into `main.cpp`. 7 | 8 | After the game has started and the preliminary setup is complete 9 | (entities loaded and prng seeded), the player needs to be able to select 10 | their save file, or create one if they haven't played before. The 11 | `startGame` function will do this for us by first asking for the name of 12 | the player's character. It will then attempt to open a JSON file with 13 | that name (remember each `Player` is saved to a JSON file named as they 14 | are). If it can't open the file it will ask the player for their class 15 | and will create a new `Player`, otherwise it will load the existing one 16 | from the file. 17 | 18 | ```cpp 19 | /* main.cpp */ 20 | // Create a new character or load an existing one 21 | Player startGame() 22 | { 23 | // Ask for a name and class 24 | // Name does not use a dialogue since dialogues only request options, 25 | // not string input. Could be generalised into its own TextInput 26 | // class, but not really necessary 27 | std::cout << "What's your name?" << std::endl; 28 | std::string name; 29 | std::cin >> name; 30 | 31 | // Check for existence then open using JsonBox if it exists 32 | std::ifstream f((name + ".json").c_str()); 33 | if(f.good()) 34 | { 35 | f.close(); 36 | // Load the player 37 | JsonBox::Value saveData; 38 | JsonBox::Value areaData; 39 | saveData.loadFromFile(name + ".json"); 40 | areaData.loadFromFile(name + "_areas.json"); 41 | Player player = Player(saveData, areaData, &entityManager); 42 | 43 | // Return the player 44 | return player; 45 | } 46 | else 47 | { 48 | f.close(); 49 | int result = Dialogue( 50 | "Choose your class", 51 | {"Fighter", "Rogue"}).activate(); 52 | 53 | switch(result) 54 | { 55 | // Fighter class favours strength 56 | case 1: 57 | return Player(name, 15, 5, 4, 1.0/64.0, 0, 1, "Fighter"); 58 | 59 | // Rogue class favours agility 60 | case 2: 61 | return Player(name, 15, 4, 5, 1.0/64.0, 0, 1, "Rogue"); 62 | 63 | // Default case that should never happen, but it's good to be safe 64 | default: 65 | return Player(name, 15, 4, 4, 1.0/64.0, 0, 1, "Adventurer"); 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | We've made use of a few more headers here---specifically `iostream`, 72 | `fstream` (for `std::ifstream` which we use to test the existence of the 73 | JSON file before opening it properly with JsonBox), `"dialogue.hpp"`, 74 | and `"player.hpp"`---so make sure to include them and also declare a 75 | prototype for the `startGame` function. A very simple 76 | 77 | ```cpp 78 | /* main.cpp */ 79 | Player player = startGame(); 80 | ``` 81 | 82 | in `main` and the player can create their character! Now we can create 83 | the *game loop*, which is where all the actual gameplay will take place. 84 | All game loops share a very similar structure; they read input from the 85 | user, process the input, update the player and environment accordingly, 86 | relay those changes back to the user, and then start again. The amount 87 | of time spent in each stage depends on the game (a chess game will 88 | barely use the first stage but will dedicate a lot of time to the third, 89 | whereas an FPS would spend a lot of time in the last stage), and here we 90 | care mostly about understanding and processing the user's input. 91 | 92 | Here is `main` in its entirety. 93 | 94 | ```cpp 95 | /* main.cpp */ 96 | int main() 97 | { 98 | // Load the entities 99 | entityManager.loadJson("items.json"); 100 | entityManager.loadJson("weapons.json"); 101 | entityManager.loadJson("armor.json"); 102 | entityManager.loadJson("creatures.json"); 103 | entityManager.loadJson("doors.json"); 104 | entityManager.loadJson("areas.json"); 105 | 106 | // Seed the random number generator with the system time, so the 107 | // random numbers produced by rand() will be different each time 108 | srand(time(NULL)); 109 | 110 | Player player = startGame(); 111 | 112 | // Set the current area to be the first area in the atlas, 113 | // placing the player there upon game start 114 | player.currentArea = "area_01"; 115 | 116 | // Play the game until a function breaks the loop and closes it 117 | while(1) 118 | { 119 | // Mark the current player as visited 120 | player.visitedAreas.insert(player.currentArea); 121 | 122 | // Pointer to to the current area for convenience 123 | Area* areaPtr = player.getAreaPtr(&entityManager); 124 | 125 | // Autosave the game 126 | player.save(&entityManager); 127 | 128 | // If the player has died then inform them as such and close 129 | // the program 130 | if(player.hp <= 0) 131 | { 132 | std::cout << "\t----YOU DIED----\n Game Over\n"; 133 | return 0; 134 | } 135 | 136 | // Add the search and movement options to the dialogue 137 | Dialogue roomOptions = areaPtr->dialogue; 138 | for(auto door : areaPtr->doors) 139 | { 140 | roomOptions.addChoice("Go through the " + door->description); 141 | } 142 | roomOptions.addChoice("Search"); 143 | 144 | // Activate the current area's dialogue 145 | int result = roomOptions.activate(); 146 | 147 | if(result == 0) 148 | { 149 | dialogueMenu(player); 150 | } 151 | else if(result <= areaPtr->dialogue.size()) 152 | { 153 | // Add more events here 154 | } 155 | else if(result < roomOptions.size()) 156 | { 157 | Door* door = areaPtr->doors.at(result-areaPtr->dialogue.size()-1); 158 | int flag = player.traverse(door); 159 | 160 | switch(flag) 161 | { 162 | default: 163 | case 0: 164 | std::cout << "The " << door->description << " is locked." << std::endl; 165 | break; 166 | case 1: 167 | std::cout << "You unlock the " << door->description << " and go through it." << std::endl; 168 | break; 169 | case 2: 170 | std::cout << "You go through the " << door->description << "." << std::endl; 171 | break; 172 | } 173 | } 174 | else 175 | { 176 | std::cout << "You find:" << std::endl; 177 | areaPtr->items.print(); 178 | player.inventory.merge(&(areaPtr->items)); 179 | areaPtr->items.clear(); 180 | } 181 | } 182 | 183 | return 0; 184 | } 185 | ``` 186 | 187 | After creating (or loading) the player we position them in the starting 188 | area, and start the game loop. Firstly we add the `Area` the player is 189 | in to their list of visited areas so that any changes they make to the 190 | area will be recorded in their save file. Instead of implementing a save 191 | menu or save location mechanic, we just automatically save the game on 192 | the next line. We don't want the game continuing if the player has run 193 | out of hit points, so next we check that they're still alive and end the 194 | program if they aren't. Now that we know they're alive, they can start 195 | to move their character around the world. 196 | 197 | Each iteration of the game loop will be another action that the player 198 | can take; attack an enemy, go through a door, and so on. We therefore 199 | present to them the `Dialogue` of the `Area` that they're currently in, 200 | but make sure to add options for all the `Door`s that are in the `Area`, 201 | because the `Area` class didn't handle that itself. We also add an 202 | option to search the `Area`, which will move any items in the `Area`'s 203 | inventory into the player's. That's the first data presentation stage 204 | done (ours is split into a before and after stages), now we read the 205 | input from the user. 206 | 207 | Thanks to the wonders of the `Dialogue` class (it's so small but it's so 208 | useful!), parsing the user's input is extremely easy. And what's more, 209 | because `Area` didn't add the `Door`s to its dialogue we automatically 210 | know how many non-door options the `Area` has, as well as how many door 211 | options. This makes determining the type of action that the user is 212 | performing very simple. (Although we still have to leave it up to the 213 | `Area` to deal with non-door and non-search actions.) 214 | 215 | If the player chose to go through a door, we use the `traverse` member 216 | function to move them to the next area (if possible) and report back how 217 | their traversal attempt went. If they tried searching, we print a list 218 | of all the items in the `Area` and move them to the player's inventory. 219 | Finally, note that we've allowed the program to understand an input of 220 | 0, even though that wouldn't be an option on the `Dialogue`. We briefly 221 | mentioned this in a previous section, but now we're putting it into 222 | practise by calling a `dialogueMenu` function which will open a menu 223 | displaying some useful information about the player. Before we define 224 | that, try commenting it out and running the program. Hopefully you'll 225 | have a nice little RPG game! 226 | 227 | ```bash 228 | > $ ./rpg 229 | What's your name? 230 | Gentoo 231 | Choose your class 232 | 1: Fighter 233 | 2: Rogue 234 | 2 235 | You are in room 1 236 | 1: Go through the sturdy wooden door 237 | 2: Search 238 | 2 239 | You find: 240 | Gold Coin (5) - A small disc made of lustrous metal 241 | Iron Key (1) - A heavy iron key with a simple cut 242 | Dagger (1) - A small blade, probably made of iron. Keep the sharp end away from your body. 243 | Leather Armor (1) - Armor made from tanned animal hide. It smells a bit funny, but it'd do the job. 244 | You are in room 1 245 | 1: Go through the sturdy wooden door 246 | 2: Search 247 | 1 248 | You unlock the sturdy wooden door and go through it. 249 | You are in room 2 250 | 1: Go through the sturdy wooden door 251 | 2: Search 252 | ``` 253 | 254 | Did it work? If it still didn't work after fixing compilation errors, 255 | compare yours to the 256 | [source](https://github.com/Piepenguin1995/cpp-rpg-tutorial). We're not 257 | done yet though, there's still that menu to create, for starters. It's a 258 | bit of a long function, but it's really quite simple. 259 | 260 | ```cpp 261 | /* main.cpp */ 262 | void dialogueMenu(Player& player) 263 | { 264 | // Output the menu 265 | int result = Dialogue( 266 | "Menu\n====", 267 | {"Items", "Equipment", "Character"}).activate(); 268 | 269 | switch(result) 270 | { 271 | // Print the items that the player owns 272 | case 1: 273 | std::cout << "Items\n=====\n"; 274 | player.inventory.print(); 275 | std::cout << "----------------\n"; 276 | break; 277 | // Print the equipment that the player is wearing (if they are 278 | // wearing anything) and then ask if they want to equip a weapon 279 | // or some armor 280 | case 2: 281 | { 282 | std::cout << "Equipment\n=========\n"; 283 | std::cout << "Armor: " 284 | << (player.equippedArmor != nullptr ? 285 | player.equippedArmor->name : "Nothing") 286 | << std::endl; 287 | std::cout << "Weapon: " 288 | << (player.equippedWeapon != nullptr ? 289 | player.equippedWeapon->name : "Nothing") 290 | << std::endl; 291 | 292 | int result2 = Dialogue( 293 | "", 294 | {"Equip Armor", "Equip Weapon", "Close"}).activate(); 295 | 296 | // Equipping armor 297 | if(result2 == 1) 298 | { 299 | int userInput = 0; 300 | 301 | // Cannot equip armor if they do not have any 302 | // Print a list of the armor and retrieve the amount 303 | // of armor in one go 304 | int numItems = player.inventory.print(true); 305 | if(numItems == 0) break; 306 | 307 | while(!userInput) 308 | { 309 | // Choose a piece of armor to equip 310 | std::cout << "Equip which item?" << std::endl; 311 | std::cin >> userInput; 312 | // Equipment is numbered but is stored in a list, 313 | // so the number must be converted into a list element 314 | if(userInput >= 1 && userInput <= numItems) 315 | { 316 | player.equipArmor(player.inventory.get(userInput-1)); 317 | } 318 | } 319 | } 320 | // Equip a weapon, using the same algorithms as for armor 321 | else if(result2 == 2) 322 | { 323 | int userInput = 0; 324 | int numItems = player.inventory.print(true); 325 | 326 | if(numItems == 0) break; 327 | 328 | while(!userInput) 329 | { 330 | std::cout << "Equip which item?" << std::endl; 331 | std::cin >> userInput; 332 | if(userInput >= 1 && userInput <= numItems) 333 | { 334 | player.equipWeapon(player.inventory.get(userInput-1)); 335 | } 336 | } 337 | } 338 | std::cout << "----------------\n"; 339 | break; 340 | } 341 | // Output the character information, including name, class (if 342 | // they have one), stats, level, and experience 343 | case 3: 344 | std::cout << "Character\n=========\n"; 345 | std::cout << player.name; 346 | if(player.className != "") std::cout << " the " << player.className; 347 | std::cout << std::endl; 348 | 349 | std::cout << "Health: " << player.hp << " / " << player.maxHp << std::endl; 350 | std::cout << "Strength: " << player.strength << std::endl; 351 | std::cout << "Agility: " << player.agility << std::endl; 352 | std::cout << "Level: " << player.level << " (" << player.xp; 353 | std::cout << " / " << player.xpToLevel(player.level+1) << ")" << std::endl; 354 | std::cout << "----------------\n"; 355 | break; 356 | default: 357 | break; 358 | } 359 | 360 | return; 361 | } 362 | ``` 363 | 364 | The menu has three separate submenus to it, accessed via a `Dialogue`; 365 | `"Items"`, `"Equipment"`, and `"Character"`. The first simply prints a 366 | list of all the items that the player. The second is more involved, and 367 | as well as printing all the armour and weapons that the player has 368 | equipped, it also asks the player what armor or weapon they would like 369 | to equip. Entering 0 again will cancel the action, and take them out of 370 | the menu. The third and final submenu prints a nicely formatted 371 | information page about the player character, listing their various 372 | attributes and the amount required to level up. 373 | 374 | It's at this point we really have nothing more than a walking simulator, 375 | to be honest it was a bit of a stretch to call this an RPG. For that, we 376 | need the battle system! 377 | -------------------------------------------------------------------------------- /tutorial/05-areas.md: -------------------------------------------------------------------------------- 1 | # Areas 2 | 3 | After all that code we still don't have any actual gameplay! Sorry about 4 | that, we have to do a lot of setting up at the beginning, but things 5 | will get more interesting soon. 6 | 7 | The user interface for this game is going to be pretty simple; the 8 | player enters a room and is given a short description of what they can 9 | see before being given a numbered list of actions for them to choose 10 | from. They choose one of these options by entering a number, and move on 11 | to the next room (or fight a monster, open a treasure chest, etc.). 12 | 13 | Such a system is so simple we could easily rewrite it to suit our needs 14 | every time we needed to ask the player something, but things quickly get 15 | complicated when we remember that players aren't very predictable; you 16 | might ask for a number between 1 and 3, but you can be sure that someone 17 | will give you the answer `4` or perhaps even `"watermelon"`. We need to 18 | cope with cases like that, which will quickly become tedious when we 19 | have to do it every time we want the player to make a choice. A solution 20 | is the `Dialogue` class. 21 | 22 | ```cpp 23 | /* dialogue.hpp */ 24 | #ifndef DIALOGUE_HPP 25 | #define DIALOGUE_HPP 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | // Gameplay is expressed using dialogues, which present a piece of 33 | // information and some responses, and the ask the user to pick one. If 34 | // they do not pick a valid one then the dialogue loops until they do 35 | class Dialogue 36 | { 37 | private: 38 | 39 | // Initial piece of information that the dialogue displays 40 | std::string description; 41 | 42 | // A vector of choices that will be outputted. No numbering is 43 | // necessary, the dialogue does that automatically 44 | std::vector choices; 45 | 46 | public: 47 | 48 | // Run the dialogue 49 | int activate() 50 | { 51 | // Output the information 52 | std::cout << description << std::endl; 53 | 54 | // Output and number the choices 55 | for(int i = 0; i < this->choices.size(); ++i) 56 | std::cout << i+1 << ": " << this->choices[i] << std::endl; 57 | 58 | // Repeatedly read input from stdin until a valid option is 59 | // chosen 60 | int userInput = -1; 61 | while(true) 62 | { 63 | std::cin >> userInput; 64 | // 'Valid' means within the range of numbers outputted 65 | if(userInput >= 0 && userInput <= this->choices.size()) 66 | { 67 | return userInput; 68 | } 69 | } 70 | } 71 | 72 | // Note that the vector is not passed by reference. Whilst that would 73 | // be more efficient, it forces us to create a vector outside of the 74 | // constructor. By passing by value we can call the constructor using 75 | // an initialisation list such as 76 | // Dialogue my_dialogue("Hello", {"Choice1", "Choice"}); 77 | Dialogue(std::string description, std::vector choices) 78 | { 79 | this->description = description; 80 | this->choices = choices; 81 | } 82 | 83 | // Create a dialogue from a JSON value 84 | Dialogue(JsonBox::Value& v) 85 | { 86 | JsonBox::Object o = v.getObject(); 87 | description = o["description"].getString(); 88 | for(auto choice : o["choices"].getArray()) 89 | choices.push_back(choice.getString()); 90 | } 91 | 92 | Dialogue() {} 93 | 94 | void addChoice(std::string choice) 95 | { 96 | this->choices.push_back(choice); 97 | } 98 | 99 | unsigned int size() 100 | { 101 | return this->choices.size(); 102 | } 103 | }; 104 | 105 | #endif /* DIALOGUE_HPP */ 106 | ``` 107 | 108 | Each `Dialogue` has a `description` string and an `std::vector` of 109 | `choices`. These are set by the constructor, but extra choices can be 110 | added using the `addChoice` function. Additionally, the number of 111 | choices can be queried using `size`. When the `activate` function is 112 | called, the `description` and each of the choices are printed and 113 | labelled numerically, starting from 1. (The description isn't labelled.) 114 | The function then loops until the user inputs a sensible response, 115 | namely a number between 0 and the number of choices, inclusive. By 116 | accepting 0 as a possible answer, we gain an option that is not 117 | displayed but always accepted regardless of the dialogue (though the 118 | rest of the program can ignore it); we will use this to pause the game 119 | and open the menu. The menu will be used to display the player's items 120 | and let them manage their equipment. 121 | 122 | Each room of the dungeon will have an associated `Dialogue`, 123 | which---like the rest of the game---will be defined in a JSON file. The 124 | syntax for the JSON representation of a `Dialogue` is pretty 125 | simple---you can infer it from the constructor---but here's an example. 126 | 127 | ```json 128 | { 129 | "description": "There is a cooked mackerel here", 130 | "choices": ["Eat the mackerel", "Save the mackerel for later"] 131 | } 132 | ``` 133 | 134 | To actually represent the dungeon, we'll need an `Area` class. Each room 135 | in the dungeon will be represented by one or more areas, where only one 136 | area can be active at a time. (For example, there might be one area for 137 | before the player presses a switch, and one for afterwards, even though 138 | they are both in the same physical room.) Each area will contain a 139 | `Dialogue`, but also an `Inventory` of items that would be lying around 140 | and an array of `Door`s connecting it to other areas. Later on we'll 141 | also introduce `Creature`s for the player to fight and interact with. 142 | 143 | ```cpp 144 | /* area.hpp */ 145 | #ifndef AREA_HPP 146 | #define AREA_HPP 147 | 148 | #include 149 | #include 150 | #include 151 | 152 | #include "entity.hpp" 153 | #include "inventory.hpp" 154 | #include "dialogue.hpp" 155 | 156 | class EntityManager; 157 | class Door; 158 | 159 | // Movement is achieved through the use of areas, which are contained 160 | // units of space consisting of an inventory, a list of creatures and 161 | // a dialogue 162 | class Area : public Entity 163 | { 164 | public: 165 | 166 | // Dialogue is run whenever the area is entered 167 | Dialogue dialogue; 168 | 169 | // Items contained within the area. Not split into individual containers 170 | // for simplicity 171 | Inventory items; 172 | 173 | // Links between rooms. Every door should have this as one of its area 174 | // pointers 175 | std::vector doors; 176 | 177 | // Constructors 178 | Area(std::string id, Dialogue dialogue, Inventory items); 179 | Area(std::string id, JsonBox::Value& v, EntityManager* mgr); 180 | 181 | // Load the area from the given Json value 182 | void load(JsonBox::Value& v, EntityManager* mgr); 183 | 184 | // Return a Json object representing the area 185 | JsonBox::Object getJson(); 186 | }; 187 | 188 | #endif /* AREA_HPP */ 189 | ``` 190 | 191 | The `Area` class is quite simple, but notice that it inherits from 192 | `Entity`. Because we'll be defining each area in a JSON file, they'll 193 | have an id and will be managed by the entity manager. With that in mind, 194 | the constructor and `load` declarations should make sense, with the only 195 | new part being the `getJson` function (although this has the same job as 196 | the equivalent function in `Inventory`). We've also forward declared the 197 | `Door` class and used it to construct an `std::vector` of doors; in this 198 | case we could include `door.hpp` for reasons you'll see below, but we 199 | don't need to so we'll forward declare instead. 200 | 201 | ### Doors ### 202 | 203 | Now's the time to say a few things about how `Door` will work, and why 204 | we have a separate `Door` class at all. What we'd like to do is be able 205 | to join each area together so that the player can move between them. An 206 | initial solution to this might be to store a list of pointers to `Area`s 207 | which are the areas the current area is connected to. But we can't store 208 | pointers in JSON files, so we have to store the ids and then convert 209 | them to areas. That's fine, but when loading the areas we will instantly 210 | encounter an id that does not yet have an associated room, and so which 211 | cannot be turned into a pointer. We can solve this problem by reading 212 | `areas.json` twice; first to create each `Area` without the connections, 213 | and the second to add those connections. This isn't very nice though, 214 | because now the constructor doesn't actually construct the `Area`. We 215 | can fix this by making the `Area` store the ids of other areas instead 216 | of pointers, but now another problem presents itself; how do we describe 217 | the connections to the player? Each `Area` only has an id (which is 218 | hardly player friendly) and a description---which is far too lengthy to 219 | use---and so we would have no choice but to just list the doors 220 | numerically. A terrible idea I'm sure you'll agree, as it would be 221 | completely unusable for the player. 222 | 223 | An alternate solution---and the one we'll be using---is to use a 224 | separate `Door` class which contains a pair of `Area` ids that it 225 | connects. This allows us to give all sorts of properties to the 226 | connections, such as descriptions or locks, and has the added bonus of 227 | affecting the connection in both areas when only when is changed (e.g. 228 | opened)! By using ids we've kept things to a single pass of 229 | `areas.json`, although now we have to load a separate file instead. We 230 | also have to make sure that we load `doors.json` before `areas.json`, 231 | because each `Area` has pointers to `Door`s. 232 | 233 | ```cpp 234 | /* door.hpp */ 235 | #ifndef DOOR_HPP 236 | #define DOOR_HPP 237 | 238 | #include 239 | #include 240 | 241 | #include "entity.hpp" 242 | 243 | class Item; 244 | class EntityManager; 245 | 246 | class Door : public Entity 247 | { 248 | public: 249 | 250 | // Door description e.g. large wooden door, rusted iron gate 251 | std::string description; 252 | 253 | // < 0 is open 254 | // 0 is unlocked but closed 255 | // > 0 is locked and needs key to open 256 | int locked; 257 | 258 | // If the player has the required key then they can unlock the door. 259 | Item* key; 260 | 261 | std::pair areas; 262 | 263 | Door(std::string id, std::string description, std::pair areas, 264 | int locked, Item* key = nullptr); 265 | Door(std::string id, JsonBox::Value& v, EntityManager* mgr); 266 | 267 | void load(JsonBox::Value& v, EntityManager* mgr); 268 | }; 269 | 270 | #endif /* DOOR_HPP */ 271 | ``` 272 | 273 | Once again the `Door` is an entity, and so inherits from the `Entity` 274 | class. Each `Door` also has a description, a status flag to denote 275 | whether it is locked or open, a pointer to an `Item` which acts as a key 276 | for the door (it could be `nullptr` if no key is required), and an 277 | `std::pair` of strings which contain the ids of the `Area`s the `Door` 278 | connects. 279 | 280 | ```cpp 281 | /* door.cpp */ 282 | #include 283 | #include 284 | 285 | #include "door.hpp" 286 | #include "item.hpp" 287 | #include "entity.hpp" 288 | #include "entity_manager.hpp" 289 | 290 | Door::Door(std::string id, std::string description, std::pair areas, 291 | int locked, Item* key) : Entity(id) 292 | { 293 | this->description = description; 294 | this->areas = areas; 295 | this->locked = locked; 296 | this->key = key; 297 | } 298 | 299 | Door::Door(std::string id, JsonBox::Value& v, EntityManager* mgr) : Entity(id) 300 | { 301 | this->load(v, mgr); 302 | } 303 | 304 | void Door::load(JsonBox::Value& v, EntityManager* mgr) 305 | { 306 | JsonBox::Object o = v.getObject(); 307 | this->description = o["description"].getString(); 308 | this->locked = o["locked"].getInteger(); 309 | if(o.find("key") != o.end()) 310 | { 311 | this->key = mgr->getEntity(o["key"].getString()); 312 | } 313 | JsonBox::Array a = o["areas"].getArray(); 314 | if(a.size() == 2) 315 | { 316 | this->areas.first = a[0].getString(); 317 | this->areas.second = a[1].getString(); 318 | } 319 | 320 | return; 321 | } 322 | ``` 323 | 324 | `Door` has the usual constructor definitions and also a `load` function 325 | which behaves much like any other we've created. A typical JSON file for 326 | doors would be 327 | 328 | ```json 329 | { 330 | "door_01_02": { 331 | "description": "sturdy wooden door", 332 | "areas": ["area_01", "area_02"], 333 | "locked": 1, 334 | "key": "item_iron_key" 335 | } 336 | } 337 | ``` 338 | 339 | The `load` function therefore reads a string for the description and an 340 | integer for the locked status, before loading the key as a string and 341 | looking it up in the entity manager if it was specified in the JSON 342 | file. Finally the two id strings are loaded into the `std::pair`. 343 | 344 | ```cpp 345 | /* area.cpp */ 346 | #include 347 | #include 348 | #include 349 | 350 | #include "area.hpp" 351 | #include "door.hpp" 352 | #include "entity.hpp" 353 | #include "inventory.hpp" 354 | #include "dialogue.hpp" 355 | #include "entity_manager.hpp" 356 | 357 | Area::Area(std::string id, Dialogue dialogue, Inventory items) : Entity(id) 358 | { 359 | this->dialogue = dialogue; 360 | this->items = items; 361 | for(auto creature : creatures) 362 | { 363 | this->creatures.push_back(*creature); 364 | } 365 | } 366 | 367 | Area::Area(std::string id, JsonBox::Value& v, EntityManager* mgr) : Entity(id) 368 | { 369 | this->load(v, mgr); 370 | } 371 | 372 | void Area::load(JsonBox::Value& v, EntityManager* mgr) 373 | { 374 | JsonBox::Object o = v.getObject(); 375 | 376 | // Build the dialogue 377 | // This is an optional parameter because it will not be saved 378 | // when the area is modified 379 | if(o.find("dialogue") != o.end()) 380 | this->dialogue = Dialogue(o["dialogue"]); 381 | 382 | // Build the inventory 383 | this->items = Inventory(o["inventory"], mgr); 384 | 385 | // Attach doors 386 | if(o.find("doors") != o.end()) 387 | { 388 | this->doors.clear(); 389 | for(auto door : o["doors"].getArray()) 390 | { 391 | Door* d = nullptr; 392 | // Each door is either an array of the type [id, locked] or 393 | // a single id string. 394 | if(door.isString()) 395 | { 396 | d = mgr->getEntity(door.getString()); 397 | } 398 | else 399 | { 400 | d = mgr->getEntity(door.getArray()[0].getString()); 401 | d->locked = door.getArray()[1].getInteger(); 402 | } 403 | this->doors.push_back(d); 404 | } 405 | } 406 | 407 | return; 408 | } 409 | 410 | JsonBox::Object Area::getJson() 411 | { 412 | JsonBox::Object o; 413 | // We don't need to save the dialogue because it doesn't change 414 | 415 | // Save the inventory 416 | o["inventory"] = this->items.getJson(); 417 | 418 | // Save the doors 419 | a.clear(); 420 | for(auto door : this->doors) 421 | { 422 | JsonBox::Array d; 423 | d.push_back(door->id); 424 | d.push_back(door->locked); 425 | a.push_back(d); 426 | } 427 | o["doors"] = a; 428 | 429 | return o; 430 | } 431 | ``` 432 | 433 | Finally we have `area.cpp`. When loading the area we first check to see 434 | if a `"dialogue"` key is present in the JSON value, and if it is we 435 | create a new `Dialogue` using the appropriate `Dialogue` constructor. We 436 | check for the key's existence instead of assuming it's there (as we do 437 | when loading the `Inventory`) because the `Dialogue` is not a compulsory 438 | key in the JSON file. This is because we're going to have two separate 439 | files which contain `Area` definitions. The first will contain all of 440 | the area used in the game, and will be loaded normally like `items.json` 441 | and `weapons.json` are. The second will contain only those areas which 442 | the player has visited, and will contain the changes that the player 443 | made to those areas. This way we can easily support multiple save games 444 | by loading the first file and then applying the changes in the second. 445 | Since there'll be no way for a player to directly influence a 446 | `Dialogue`, we don't bother including them in the second file, and hence 447 | why they're optional and aren't included in the `getJson` function. 448 | 449 | The `"doors"` key is also optional (it might be a bit odd to have a room 450 | with doors but we can't ignore the possibility), but additionally each 451 | `Door` has two JSON forms. The first is simply a string id referring to 452 | the `Door` itself, and the second is that of an array with two elements. 453 | The first is the string id of the `Door`, and the second is an 454 | overriding `locked` status which will be used to remember whether a door 455 | has been opened or unlocked by the player. When saving the `Area` using 456 | the `getJson` function we will use the second form (preventing doors 457 | from mysteriously locking again when the game is loaded) but because the 458 | locked status is defined in `doors.json` we don't want to force the 459 | override to exist, and so allow for the first form. 460 | 461 | Since `Area` and `Door` are kinds of entity, don't forget to add the 462 | template specialisations and instantiations for them and make sure you 463 | load an `areas.json` file (after `doors.json`) in `main.cpp` which 464 | contains the areas, such as this one. 465 | 466 | ```json 467 | { 468 | "area_01": { 469 | "dialogue": { 470 | "description": "You are in room 1", 471 | "choices": [] 472 | }, 473 | "doors": ["door_01_02"], 474 | "inventory": { 475 | "items": [ 476 | ["item_gold_coin", 5], 477 | ["item_iron_key", 1] 478 | ], 479 | "weapons": [ 480 | ["weapon_dagger", 1] 481 | ], 482 | "armor": [ 483 | ["armor_leather", 1] 484 | ] 485 | }, 486 | "creatures": [] 487 | }, 488 | 489 | "area_02": { 490 | "dialogue": { 491 | "description": "You are in room 2", 492 | "choices": [] 493 | }, 494 | "doors": ["door_01_02"], 495 | "inventory": { 496 | "items": [ 497 | ["item_gold_coin", 100] 498 | ], 499 | "weapons": [], 500 | "armor": [] 501 | }, 502 | "creatures": 503 | [ 504 | "creature_rat" 505 | ] 506 | } 507 | } 508 | ``` 509 | -------------------------------------------------------------------------------- /tutorial/04-inventories.md: -------------------------------------------------------------------------------- 1 | # Inventories 2 | 3 | Now that we've got a few entities that the player will be able to pick 4 | up and collect, we need a way for the player (and others) to store them. 5 | The simplest method to do this is to just give whatever needs to store 6 | items a few arrays (or `std::vector`s, more likely) and let them manage 7 | the entities. The problem with this is there'll be a lot of repeated 8 | code, and it isn't very elegant. Instead what we'll do is create an 9 | `Inventory` class that manages any number of entities derived from and 10 | including `Item`---henceforth all called items---in a manner similar to 11 | the `EntityManager`. Instead of using an `std::map`, we will use an 12 | `std::list` of `std::pair`s, where each pair contains a pointer to the 13 | item (in the `EntityManager`) and a quantity. We'll also create a 14 | selection of helper functions which make it easier to manage the 15 | `Inventory` instead of working directly with the `std::list`. 16 | 17 | Every time we need to represent multiple items, we'll use the 18 | `Inventory` class; the items scattered about a room, in a treasure 19 | chest, or held by the player. We'll want a few functions to make life 20 | easier for us when dealing with the `Inventory`s, namely functions to 21 | add and remove items, check if an inventory contains and item and if so 22 | how many, and a function to merge two inventories together. 23 | 24 | Now brace yourself, there are a lot of functions in this class and I'm 25 | going to give all their declarations to you in one go! 26 | 27 | ```cpp 28 | /* inventory.hpp */ 29 | #ifndef INVENTORY_HPP 30 | #define INVENTORY_HPP 31 | 32 | #include 33 | #include 34 | #include 35 | 36 | #include "entity_manager.hpp" 37 | 38 | class Item; 39 | class Weapon; 40 | class Armor; 41 | 42 | class Inventory 43 | { 44 | private: 45 | 46 | // We use a similar method here to in EntityManager where we store 47 | // a list of base pointers. We use a list and not a vector as inventories 48 | // are highly mutable. This way they can also be efficiently sorted. 49 | // The first element of the pair stores a pointer to the item in 50 | // the EntityManager, and the second element stores the quantity of the item 51 | std::list> items; 52 | 53 | // Given the Json value v which contains a list of items, weapons, or armor of type T 54 | // load the Ts into the storage list (either items, weapons, or armor) 55 | template 56 | void load(JsonBox::Value& v, EntityManager* mgr); 57 | 58 | // Return a JSON representation of all the items of the type T 59 | template 60 | JsonBox::Array jsonArray(); 61 | 62 | public: 63 | 64 | // Add an item to the inventory 65 | void add(Item* item, int count); 66 | 67 | // Remove the specified number of items from the inventory 68 | void remove(Item* item, int count); 69 | 70 | // Returns the count of the specified item 71 | int count(Item* item); 72 | template 73 | int count(unsigned int n); 74 | 75 | // Return the nth item in the storage list 76 | template 77 | T* get(unsigned int n); 78 | 79 | // Output a list of the items onto stdout, formatted nicely and 80 | // numbered if required 81 | template 82 | int print(bool label = false); 83 | 84 | // Remove all items from the inventory 85 | void clear(); 86 | 87 | // Merge the specified inventory with the current one, adding 88 | // item quantities together if they already exist and adding the item 89 | // into a new slot if they do not 90 | void merge(Inventory* inventory); 91 | 92 | // Load the inventory from a JSON value 93 | Inventory(JsonBox::Value& v, EntityManager* mgr); 94 | Inventory() {} 95 | 96 | // Print the entire inventory; items, then weapons, then armor, 97 | // but if the inventory is empty then output "Nothing" 98 | int print(bool label = false); 99 | 100 | // Get a Json object representation of the inventory 101 | JsonBox::Object getJson(); 102 | }; 103 | 104 | #endif /* INVENTORY_HPP */ 105 | ``` 106 | 107 | The comments should make most things clear, but basic usage of the 108 | `Inventory` class is as follows; first either the empty constructor or 109 | the JSON constructor are called. If the JSON constructor is called, the 110 | value is taken to have three keys---`"items"`, `"weapons"`, and 111 | `"armor"`---which are then loaded by the `load` function. The 112 | `Inventory` is then queried using the `get`, and `count` functions, and 113 | modified using `add`, `remove`, and `merge`. It's contents can be 114 | displayed on a per-type basis using the `print` function template, or 115 | all together using the `print` function, and then the `Inventory` can be 116 | converted back to a JSON representation using `getJson` (which calls 117 | `jsonArray` on each item type). 118 | 119 | Firstly we have the `load` function and the headers. 120 | 121 | ```cpp 122 | /* inventory.cpp */ 123 | #include 124 | #include 125 | #include 126 | #include 127 | #include "JsonBox.h" 128 | 129 | #include "inventory.hpp" 130 | #include "item.hpp" 131 | #include "weapon.hpp" 132 | #include "armor.hpp" 133 | #include "entity_manager.hpp" 134 | 135 | template 136 | void Inventory::load(JsonBox::Value& v, EntityManager* mgr) 137 | { 138 | for(auto item : v.getArray()) 139 | { 140 | std::string itemId = item.getArray()[0].getString(); 141 | int quantity = item.getArray()[1].getInteger(); 142 | this->items.push_back(std::make_pair(mgr->getEntity(itemId), quantity)); 143 | } 144 | } 145 | ``` 146 | 147 | Before we understand the `load` function it makes sense to see the 148 | syntax of a JSON representation of an `Inventory`. 149 | 150 | ```json 151 | { 152 | "items": [ 153 | ["item_gold_coin", 5], 154 | ["item_iron_key", 1] 155 | ], 156 | "weapons": [ 157 | ["weapon_dagger", 1] 158 | ], 159 | "armor": [ 160 | ["armor_leather", 1] 161 | ] 162 | } 163 | ``` 164 | 165 | The `load` function operates on an individual key---either `"items"`, 166 | `"weapons"`, or `"armor"`---and so assumes that the `JsonBox::Value` is 167 | an array, where each element of that is another array containing the 168 | item id and its quantity, in that order. `load` therefore converts `v` 169 | to an array and iterates over each of its elements (which will be 170 | `JsonBox::Value`s). For each element it converts it to an array and 171 | extracts the `itemId` and `quantity` from the relevant positions. In one 172 | step it then uses `std::make_pair` and `EntityManager::getEntity` to 173 | obtain a pointer to the item with that id and construct a new 174 | `std::pair` containing that pointer and the quantity. This 175 | `std::pair` is then appended to the end of the item list. 176 | 177 | ```cpp 178 | /* inventory.cpp */ 179 | template 180 | JsonBox::Array Inventory::jsonArray() 181 | { 182 | JsonBox::Array a; 183 | for(auto item : this->items) 184 | { 185 | // Skip if the id does not match to the type T 186 | if(item.first->id.substr(0, entityToString().size()) != entityToString()) 187 | continue; 188 | // Otherwise add the item to the array 189 | JsonBox::Array pair; 190 | pair.push_back(JsonBox::Value(item.first->id)); 191 | pair.push_back(JsonBox::Value(item.second)); 192 | a.push_back(JsonBox::Value(pair)); 193 | } 194 | 195 | return a; 196 | } 197 | ``` 198 | 199 | To go in the opposite direction and convert the inventory to a 200 | `JsonBox::Array` we use the `jsonArray` function, which iterates over 201 | all the items in the item list and for each item constructs a new array 202 | which it stores the id and quantity of that item in. This array is then 203 | appended to the main array, which is returned when all items have been 204 | added. Note that because of the structure of the JSON file, if we want 205 | to properly output an `Inventory` we have to be able to output `Item`s, 206 | `Weapon`s, and `Armor` separately. Hence this function accepts a 207 | template argument `T` which it compares each item's id against in order 208 | to confirm that they are the correct type (like the `EntityManager` 209 | does). If they aren't it ignores then and continues to the next element. 210 | 211 | ```cpp 212 | /* inventory.cpp */ 213 | JsonBox::Object Inventory::getJson() 214 | { 215 | JsonBox::Object o; 216 | 217 | o["items"] = JsonBox::Value(jsonArray()); 218 | o["weapons"] = JsonBox::Value(jsonArray()); 219 | o["armor"] = JsonBox::Value(jsonArray()); 220 | 221 | return o; 222 | } 223 | ``` 224 | 225 | To output the entire inventory we use the `getJson` function, which 226 | simply calls `jsonArray` for each each item type and creates a new 227 | `JsonBox::Object` from them. 228 | 229 | ```cpp 230 | /* inventory.cpp */ 231 | void Inventory::add(Item* item, int count) 232 | { 233 | for(auto& it : this->items) 234 | { 235 | if(it.first->id == item->id) 236 | { 237 | it.second += count; 238 | return; 239 | } 240 | } 241 | this->items.push_back(std::make_pair(item, count)); 242 | } 243 | 244 | void Inventory::remove(Item* item, int count) 245 | { 246 | // Iterate through the items, and if they are found then decrease 247 | // the quantity by the quantity removed 248 | for(auto it = this->items.begin(); it != this->items.end(); ++it) 249 | { 250 | if((*it).first->id == item->id) 251 | { 252 | (*it).second -= count; 253 | if((*it).second < 1) this->items.erase(it); 254 | return; 255 | } 256 | } 257 | } 258 | ``` 259 | 260 | `add` takes a pointer to an `Item` and a quantity and checks to see if 261 | the inventory already contains an item with the same id as the given 262 | `Item` pointer. If it does it increases that item's quantity by the 263 | given value and then returns, otherwise the inventory doesn't contain 264 | the item yet and so it creates a new `std::pair` and adds it 265 | to the item list. C++ implicitly converts pointers to derived pointers 266 | to pointers to base classes (no need for `dynamic_cast`) and so making 267 | this function accept `Item*` instead of `T*` and converting it is 268 | perfectly fine. 269 | 270 | `remove` is slightly more complicated, because in order to delete an 271 | element of an `std::list` we have to use the `std::list::erase` 272 | function, which takes an *iterator* as an argument. Iterators are 273 | similar to pointers in that they represent a location, but they're 274 | restricted to a single data structure---in this case the item 275 | list---instead of an arbitrary location. 276 | 277 | Whilst a `range-for` loop like in the `add` function has a nice syntax, 278 | it doesn't give us an iterator, and so we use an uglier `for` loop which 279 | does. Iterators also don't give us direct element access via `->` like 280 | pointers do, so we have to use the indirection operator `*` and then `.` 281 | instead. If the id matches then we decrease the quantity of that item by 282 | the `count` and remove it from the list if its new quantity is 283 | non-positive. Note that we aren't using `unsigned int`s for the item 284 | quantity (because JsonBox only supports `int`), and so we don't have any 285 | risk of overflow here. 286 | 287 | ```cpp 288 | /* inventory.cpp */ 289 | template 290 | T* Inventory::get(unsigned int n) 291 | { 292 | // Using a list so we don't have random access, and must 293 | // step through n times from the start instead 294 | unsigned int i = 0; 295 | auto it = this->items.begin(); 296 | for(; it != this->items.end(); ++it) 297 | { 298 | if((*it).first->id.substr(0, entityToString().size()) != entityToString()) 299 | continue; 300 | if(i++ == n) break; 301 | } 302 | if(it != this->items.end()) 303 | return dynamic_cast((*it).first); 304 | else 305 | return nullptr; 306 | } 307 | ``` 308 | 309 | The `get` function returns a `T*` pointer to the `n`th element in the 310 | item list that is of the same type as `T`. This function is quite 311 | complicated because we've used an `std::list`---which doesn't have 312 | random element access---instead of an `std::vector`---which does. We 313 | therefore can't just access the `n`th element, and must step through 314 | using iterators instead. Having said that, because we want the `n`the 315 | element of a given type, and not the `n`th element overall, there's no 316 | escaping this even with an `std::vector`. 317 | 318 | We iterate over each element in the list, and increment `i` every time 319 | we find a item of type `T`. When we have found `n` such items, we break 320 | from the loop, at which point `it` will be an iterator to the `n`th 321 | element, or will be an iterator past the end of the list 322 | (`std::list::end`) if no element was found. We then return either the 323 | pointer to the item or `nullptr` accordingly. 324 | 325 | ```cpp 326 | /* inventory.cpp */ 327 | int Inventory::count(Item* item) 328 | { 329 | for(auto it : this->items) 330 | { 331 | if(it.first->id == item->id) 332 | return it.second; 333 | } 334 | return 0; 335 | } 336 | 337 | template 338 | int Inventory::count(unsigned int n) 339 | { 340 | return count(get(n)); 341 | } 342 | ``` 343 | 344 | The two `count` functions are much simpler. The first returns the 345 | quantity of `item` that the inventory contains by finding the first (and 346 | only) `std::pair` in the item list whose first element has the same id 347 | as `item`. The second uses the `get` function to find a pointer of type 348 | `T*` to the `n`th item, which is then implicitly converted to an `Item*` 349 | pointer before being sent to the first `count` function. Note the 350 | difference here; the first `count` returns the quantity for any item, 351 | including those that aren't in the inventory, whereas the second returns 352 | the count for the `n`th element of a given type, not the `n`th element 353 | overall. You could easily overload the function to do that though. 354 | 355 | ```cpp 356 | /* inventory.cpp */ 357 | template 358 | int Inventory::print(bool label) 359 | { 360 | unsigned int i = 1; 361 | 362 | for(auto it : this->items) 363 | { 364 | // Skip if the id does not match to the type T 365 | if(it.first->id.substr(0, entityToString().size()) != entityToString()) 366 | continue; 367 | // Number the items if asked 368 | if(label) std::cout << i++ << ": "; 369 | // Output the item name, quantity and description, e.g. 370 | // Gold Piece (29) - Glimmering discs of wealth 371 | std::cout << it.first->name << " (" << it.second << ") - "; 372 | std::cout << it.first->description << std::endl; 373 | } 374 | 375 | // Return the number of items outputted, for convenience 376 | return this->items.size(); 377 | } 378 | 379 | // Overload of print to print all items when the template argument is empty 380 | int Inventory::print(bool label) 381 | { 382 | unsigned int i = 0; 383 | 384 | if(items.empty()) 385 | { 386 | std::cout << "Nothing" << std::endl; 387 | } 388 | else 389 | { 390 | i += print(label); 391 | i += print(label); 392 | i += print(label); 393 | } 394 | 395 | return i; 396 | } 397 | ``` 398 | 399 | Next we have the two `print` functions which format and output the names 400 | and descriptions of the items in the inventory. The first (templated) 401 | function only prints an item if it has the same type as `T`, and has an 402 | optional feature to numerically label each item. By using this label in 403 | conjunction with the templated `get` we'll be able to create a simple 404 | user interface for managing items, where the player selects an item 405 | based on its number. The second `print` function prints all the 406 | different item types in sequence, or `"Nothing"` if the inventory is 407 | empty. Both functions also return the number of items outputted, which 408 | will come in handy later. 409 | 410 | ```cpp 411 | /* inventory.cpp */ 412 | void Inventory::clear() 413 | { 414 | this->items.clear(); 415 | } 416 | 417 | void Inventory::merge(Inventory* inventory) 418 | { 419 | // You can't merge an inventory with itself! 420 | if(inventory == this) return; 421 | 422 | // Loop through the items to be added, and add them. Our addition 423 | // function will take care of everything else for us 424 | for(auto it : inventory->items) this->add(it.first, it.second); 425 | 426 | return; 427 | } 428 | 429 | Inventory::Inventory(JsonBox::Value& v, EntityManager* mgr) 430 | { 431 | JsonBox::Object o = v.getObject(); 432 | load(o["items"], mgr); 433 | load(o["weapons"], mgr); 434 | load(o["armor"], mgr); 435 | } 436 | ``` 437 | 438 | Finally we have the `clear` and `merge` functions, and the JSON 439 | constructor. All of these are pretty self-explanatory in how they work; 440 | `clear` removes all the items from the inventory, `merge` takes the 441 | items in one inventory and adds (not moves) them to the calling one, and 442 | the constructor uses `load` to load the inventory from the 443 | `JsonBox::Value`. 444 | 445 | ```cpp 446 | /* inventory.cpp */ 447 | // Template instantiations 448 | template void Inventory::load(JsonBox::Value&, EntityManager*); 449 | template void Inventory::load(JsonBox::Value&, EntityManager*); 450 | template void Inventory::load(JsonBox::Value&, EntityManager*); 451 | 452 | template JsonBox::Array Inventory::jsonArray(); 453 | template JsonBox::Array Inventory::jsonArray(); 454 | template JsonBox::Array Inventory::jsonArray(); 455 | 456 | template int Inventory::count(unsigned int); 457 | template int Inventory::count(unsigned int); 458 | template int Inventory::count(unsigned int); 459 | 460 | template Item* Inventory::get(unsigned int); 461 | template Weapon* Inventory::get(unsigned int); 462 | template Armor* Inventory::get(unsigned int); 463 | 464 | template int Inventory::print(bool); 465 | template int Inventory::print(bool); 466 | template int Inventory::print(bool); 467 | ``` 468 | 469 | Right at the end of the file we have the explicit instantiations of the 470 | template functions for each item type, and with that the inventory 471 | system is done! 472 | -------------------------------------------------------------------------------- /tutorial/07-experience-formula.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /tutorial/09-battle-system.md: -------------------------------------------------------------------------------- 1 | # Battle System 2 | 3 | We spent quite a long time discussing how a battle system worked a few 4 | tutorials ago, but we didn't atually write one. Until now! This tutorial 5 | will be the last of the series, and once we're done you should have a 6 | very simple but still playable---and decently fun, I hope---RPG. Since 7 | we've already decided how battles will be fought, we can jump right into 8 | the technical details. This is good, because they actually require a 9 | decent bit of thought. 10 | 11 | Our aim is to construct a system where each participant (player or 12 | enemy) attacks in sequence depending on their agility. At the very least 13 | then, we'll need a list of participants that we can easily sort into 14 | agility order. This list will need to contain variables of type 15 | `Creature*`, because it has to store both a pointer to the player (of 16 | class `Player`) and to the enemies (of class `Creature`). Note that we 17 | don't lose any information this way, because we can still tell if a 18 | `Creature*` points to a `Player` by checking the id, which will be 19 | `"player"` if and only if the pointer points to a `Player` class. 20 | 21 | We'll also need some way of describing a general action that a 22 | participant can make during a turn. In our system we will just have 23 | *attack* and *defend* (though *use*, *cast*, *throw*, *flee*, etc. would 24 | probably be wanted later), and to make matters even simpler *defend* 25 | won't even do anything! Since all actions involve participant A doing 26 | something to participant B, it makes sense for one of these 27 | actions---called *battle events*---to have a `source` participant, a 28 | `target` participant, and an event `type`. By using a `BattleEvent` 29 | class we can easily combine the necessary variables together and also 30 | add a handy `run` function to actually make the `source` perform the 31 | `type` on the `target`. 32 | 33 | ```cpp 34 | /* battle.hpp */ 35 | class Creature; 36 | 37 | // Possible event types, should equate to what the player 38 | // can do in a battle 39 | enum class BattleEventType { ATTACK, DEFEND }; 40 | 41 | class BattleEvent 42 | { 43 | public: 44 | 45 | // Creature that initiated the event, e.g. the attacker 46 | Creature* source; 47 | // Creature being affected, e.g. the one being attacked 48 | Creature* target; 49 | // Type of event, e.g. attack, or defense 50 | BattleEventType type; 51 | 52 | // Constructor 53 | BattleEvent(Creature* source, Creature* target, BattleEventType type); 54 | 55 | // Convert the event type to the corresponding function and call it 56 | // on the source and target 57 | int run(); 58 | }; 59 | ``` 60 | 61 | Now we'll still need some kind of list of participants, but because the 62 | player needs to be able to select a target, it actually makes more sense 63 | to have a list that has a fixed and numerically accessible order. In 64 | order words, we want an `std::vector`. Since we like classes so much 65 | we'll create a `Battle` class that will handle each battle using `run` 66 | and `nextTurn` functions. `run` will be called once when the battle is 67 | started (we don't use the constructor for this so that the `Battle` can 68 | be set up in advance if desired), and `nextTurn` will be called at the 69 | start of each combat turn (i.e. after every participant has attacked the 70 | same number of times). 71 | 72 | ```cpp 73 | /* battle.hpp */ 74 | class Battle 75 | { 76 | private: 77 | 78 | // All the creatures that are participating in the fight. 79 | // We assume the player is a Creature with id "player". 80 | // A vector is used because we need to get the nth element 81 | // for use with a Dialogue 82 | std::vector combatants; 83 | 84 | // Actions that the player can take in the battle 85 | Dialogue battleOptions; 86 | 87 | // Remove a creature from the combatants list, and report that it's dead 88 | void kill(Creature* creature); 89 | 90 | // Run the next turn for the enemies and the player. 91 | // Computes what the enemies should do and asks for the player's 92 | // action, then compiles an event queue of the actions before 93 | // proceeding through the queue and running each action. 94 | void nextTurn(); 95 | 96 | public: 97 | 98 | // Constructor 99 | Battle(std::vector& combatants); 100 | 101 | // Run the battle until either the player dies, or all the opposing 102 | // combatants do 103 | void run(); 104 | }; 105 | ``` 106 | 107 | The `BattleEvent` member functions definitions are some of the simplest 108 | yet. 109 | 110 | ```cpp 111 | /* battle.cpp */ 112 | BattleEvent::BattleEvent(Creature* source, Creature* target, BattleEventType type) 113 | { 114 | this->source = source; 115 | this->target = target; 116 | this->type = type; 117 | } 118 | 119 | int BattleEvent::run() 120 | { 121 | switch(type) 122 | { 123 | case BattleEventType::ATTACK: 124 | return source->attack(target); 125 | case BattleEventType::DEFEND: 126 | return 0; 127 | default: 128 | return 0; 129 | } 130 | } 131 | ``` 132 | 133 | The return value of `run` can be used to pass useful information back to 134 | the `Battle`, in the case of attack and defend actions the amount of 135 | damage dealt is returned so that it can be reported back to the player; 136 | this means that we need to write an `attack` function in the `Creature` 137 | class that does just that! Heading back to `creature.cpp` then (don't 138 | forget the prototype in `creature.hpp`) we'll create the `attack` 139 | function, which will calculate if an attack hits and if it does, how 140 | much damage it deals. It will also subtract the damage to save us doing 141 | that in the `Battle`. 142 | 143 | ```cpp 144 | /* creature.hpp */ 145 | // Note that 'x in range [a, b]' in the comments means that 146 | // a <= x <= b. This is called interval notation, and is a 147 | // common mathematical shorthand. Using ( or ) instead of [ or ] 148 | // means that there's no = on the inequality on that side of the x. 149 | int Creature::attack(Creature* target) 150 | { 151 | // Damage done 152 | int damage = 0; 153 | 154 | if(double(std::rand()) / RAND_MAX > target->evasion) 155 | { 156 | // Calculate attack based on strength and weapon damage 157 | int attack = this->strength + (this->equippedWeapon == nullptr ? 0 : this->equippedWeapon->damage); 158 | // Calculate defense based on agility and armor defense 159 | int defense = target->agility + (target->equippedArmor == nullptr ? 0 : target->equippedArmor->defense); 160 | // 1/32 chance of a critical hit 161 | if(std::rand() % 32 == 0) 162 | { 163 | // Ignore defense and do damage in range [attack/2, attack] 164 | damage = attack / 2 + std::rand() % (attack / 2 + 1); 165 | } 166 | else 167 | { 168 | // Normal hit so factor in defense 169 | int baseDamage = attack - defense / 2; 170 | // Do damage in range [baseDamage/4, baseDamage/2] 171 | damage = baseDamage / 4 + std::rand() % (baseDamage / 4 + 1); 172 | // If the damage is zero then have a 50% chance to do 1 damage 173 | if(damage < 1) 174 | { 175 | damage = std::rand() % 2; 176 | } 177 | } 178 | // Damage the target 179 | target->hp -= damage; 180 | } 181 | 182 | return damage; 183 | } 184 | ``` 185 | 186 | The comments in the code describe the mechanics of the `attack` function 187 | and how it calculates damage, but in short the weapon damage and 188 | strength of the attacker are compared against the agility and armor 189 | defense of the target. The greater the difference the greater the 190 | average damage dealt will be, but also the greater the range of damage 191 | will be. If an attack does less than 1 point of damage then there is a 192 | 50% chance that the attack will do 0 damage or 1 damage (this stops 193 | battles from lasting forever), and each attack has a 1/32 chance of 194 | dealing a *critical hit*, which ignores the target's agility and armor 195 | to deal more damage. This is all assuming the attack hits, which has a 196 | chance equal to `1-target->evasion` of occuring. If you haven't seen 197 | `RAND_MAX` before, it is a macro that evaluates to the largest integer 198 | that `std::rand()` will generate. By casting the value returned by 199 | `rand()` to a `double` and dividing by `RAND_MAX`, we obtain a `double` 200 | in the range `[0.0,1.0]` which has the highest possible precision 201 | achievable using `rand()`. It's a neat trick! 202 | 203 | Moving back to `battle.cpp`, we can begin to implement the `Battle` 204 | member functions. First up is the constructor. 205 | 206 | ```cpp 207 | /* battle.cpp */ 208 | Battle::Battle(std::vector& combatants) 209 | { 210 | this->combatants = combatants; 211 | 212 | // Construct the menu 213 | this->battleOptions = Dialogue("What will you do?", 214 | { 215 | "Attack", 216 | "Defend" 217 | }); 218 | 219 | // Store the unique creature names and whether there is 220 | // only one or more of them. This code assumes that the 221 | // creatures have not already been assigned unique names, 222 | // which should be the case as a battle cannot be left 223 | // and then resumed again 224 | std::map names; 225 | for(auto com : this->combatants) 226 | { 227 | // Skip the player, who shouldn't be renamed 228 | if(com->id == "player") continue; 229 | // If the name hasn't been recorded and the creature 230 | // isn't the player, record the name 231 | if(names.count(com->name) == 0) 232 | { 233 | names[com->name] = 0; 234 | } 235 | // If there is already one creature, record there are being 236 | // more than one. We don't want the actual number, simply 237 | // that there's more and so we should label them. 238 | else if(names[com->name] == 0) 239 | { 240 | names[com->name] = 1; 241 | } 242 | } 243 | 244 | // Creature unique names for the combatants 245 | for(auto& com : this->combatants) 246 | { 247 | std::string newName = com->name; 248 | // If the name is marked as being shared by more than 249 | // one creature 250 | if(names.count(com->name) > 0 && names[com->name] > 0) 251 | { 252 | // Append (1) to the end of the name, and then increase the 253 | // number for the next creature 254 | newName += " (" + std::to_string(names[com->name]) + ")"; 255 | names[com->name] += 1; 256 | } 257 | // Change the creature name to the new one, which might just be 258 | // the same as the original 259 | com->name = newName; 260 | } 261 | } 262 | ``` 263 | 264 | The `std::vector` passed to the constructor should contain pointers to 265 | unique creatures---such as those stored in each `Area`---but although 266 | each creature is unique (in the sense that no two pointers point to the 267 | same location in memory), they do not have unique names. Because this is 268 | a text-based game, the names are the only way to distinguish between two 269 | creatures, and so without unique names the player will not be able to 270 | tell two `"Rat"`s apart. As such, the bulk of the constructor is 271 | dedicated to creating unique names for the different participants; if 272 | there are two `"Rat"`s in the battle, then they will be renamed `"Rat 273 | (1)"` and `"Rat (2)"`, for example. 274 | 275 | To do this, we first construct an `std::map` which maps each of the 276 | (non-unique) names of the participants to a number; 0 if there is only 277 | one creature with that name, and 1 otherwise. This tells us whether each 278 | name should be labelled or not. We skip the player because in our game 279 | the player cannot attack themselves; if you expanded the game to allow 280 | for that (perhaps the player can heal themselves with magic) you should 281 | have a more robust system that deals with the player and a creature 282 | sharing a name. 283 | 284 | Once the `std::map` is constructed, we take each participant and check 285 | if their name should be labelled or not. If they should be, we label 286 | them with the corresponding number in the `std::map` and then increase 287 | that number, ensuring that it's always equal to the next label 288 | necessary. 289 | 290 | After the constructor has been called and the unique names have been 291 | created, the battle can be started using the `run` function. 292 | 293 | ```cpp 294 | /* battle.cpp */ 295 | void Battle::run() 296 | { 297 | std::vector::iterator player; 298 | std::vector::iterator end; 299 | do 300 | { 301 | // Continue the battle until either the player dies, 302 | // or there is only the player left 303 | player = std::find_if(this->combatants.begin(), this->combatants.end(), 304 | [](Creature* a) { return a->id == "player"; }); 305 | end = this->combatants.end(); 306 | 307 | this->nextTurn(); 308 | } 309 | while(player != end && this->combatants.size() > 1); 310 | 311 | return; 312 | } 313 | ``` 314 | 315 | OK, this part will be unfamiliar if you haven't seen iterators before. 316 | An iterator is similar to a pointer in that it commonly refers to a 317 | entry in some data structure (in this case and `std::vector` containing 318 | `Creature` pointers), but they're actually objects with some additional 319 | properties and restrictions. Here we're making use of them because 320 | they're the return type of the `std::find_if` function (which is 321 | included in the `algorithm` header), which takes an starting iterator, 322 | an ending iterator, and a *lambda*. `std::find_if` then scans all the 323 | elements in the data structure between the starting and ending iterator 324 | (in this case the beginning and end of the combatants list) and passes 325 | them to the lambda function. `std::find_if` returns an iterator to the 326 | first element it scanned where the lambda returned `true`. 327 | 328 | The [syntax for a 329 | lambda](http://en.cppreference.com/w/cpp/language/lambda) is a little 330 | weird and can get quite complicated, but here we have the simplest kind 331 | of lambda, one that has an empty *capture list* `[]`. Refer to the link 332 | for the capture list syntax, but essentially the capture list acts like 333 | an additional function argument list that is usable when the actual 334 | function argument list is restricted. In this case, `std::find_if` 335 | demands that the lambda has a single argument (which is the element in 336 | the data structure it is currently scanning) and we don't want to pass 337 | anything else, so we leave the capture list empty. After that the lambda 338 | is just like a normal function, which in this case simply returns `true` 339 | if the scanned `Creature` is the player, and `false` otherwise. So 340 | actually, all this piece of code does is find the player in the 341 | combatant list! 342 | 343 | So why did we go through all that when we could have just used a simple 344 | `for` loop and pointer combination? Actually I just wanted to show you 345 | something new, there wasn't really a reason! 346 | 347 | Anyway, this `do-while` loop just finds the player, starts the next 348 | turn, and continues until the player is no longer in the list, i.e. has 349 | died, or the list of combatants is reduced to 1 (in which case the 350 | player will either be dead and there be one enemy left, or the player is 351 | the last participant alive). Looking closely though, aren't we checking 352 | to see if the player is at the end of the combatants list, not that they 353 | aren't in it? Of course we aren't, and that's because the `end` iterator 354 | (as given by `this->combatants.end()`) does not point to the last 355 | element in a data structure, it actually points to the element after the 356 | last one. This allows iterators to contain existence information without 357 | any extra complications. See, they're nice and useful when you get used 358 | to them! 359 | 360 | Now for the big one, the `nextTurn` function. 361 | 362 | ```cpp 363 | /* battle.cpp */ 364 | void Battle::nextTurn() 365 | { 366 | // Queue of battle events. Fastest combatants will be 367 | // at the start of the queue, and so will go first, 368 | // whereas slower ones will be at the back 369 | std::queue events; 370 | 371 | // Sort the combatants in agility order 372 | std::sort(combatants.begin(), combatants.end(), [](Creature* a, Creature* b) { return a->agility > b->agility; }); 373 | 374 | // Iterate over the combatants and decide what they should do, 375 | // before adding the action to the event queue. 376 | for(auto com : this->combatants) 377 | { 378 | if(com->id == "player") 379 | { 380 | // Create the target selection dialogue 381 | Dialogue targetSelection = Dialogue("Who?", {}); 382 | // Created every turn because some combatants may die 383 | for(auto target : this->combatants) 384 | { 385 | if(target->id != "player") 386 | { 387 | targetSelection.addChoice(target->name); 388 | } 389 | } 390 | 391 | // Ask the player for their action (attack or defend) 392 | int choice = this->battleOptions.activate(); 393 | 394 | switch(choice) 395 | { 396 | default: 397 | case 1: 398 | { 399 | // Player is attacking, so ask for the target. 400 | // Dialogue returns the number of the choice but with 401 | // the player removed, so we have to do some fancy 402 | // arithmetic to find the actual location of the target 403 | // and then convert that to a pointer 404 | int position = targetSelection.activate(); 405 | for(int i = 0; i < position; ++i) 406 | { 407 | if(this->combatants[i]->id == "player") ++position; 408 | } 409 | Creature* target = this->combatants[position-1]; 410 | // Add the attack command to the event queue 411 | events.push(BattleEvent(com, target, BattleEventType::ATTACK)); 412 | break; 413 | } 414 | case 2: 415 | { 416 | // Player is defending, so do nothing 417 | events.push(BattleEvent(com, nullptr, BattleEventType::DEFEND)); 418 | break; 419 | } 420 | } 421 | } 422 | else 423 | { 424 | // Simple enemy AI where enemy constantly attacks player 425 | Creature* player = *std::find_if(this->combatants.begin(), this->combatants.end(), 426 | [](Creature* a) { return a->id == "player"; }); 427 | 428 | events.push(BattleEvent(com, player, BattleEventType::ATTACK)); 429 | } 430 | } 431 | ``` 432 | 433 | First we sort the combatants into agility order using the `std::sort` 434 | function and another lambda. `std::sort` needs starting and ending 435 | iterators like `std::find_if`, but its lambda takes two arguments and 436 | must return `true` if the first is "less than" the second, whatever that 437 | might mean. Does that mean less agility though? We're using an 438 | `std::queue` to store our `BattleEvent`s, which means the events 439 | processed first will be at the start of the queue and hence will occur 440 | first. This means that the combatants with higher agility need to be 441 | placed earlier in the combatants list, and so actually "less than" means 442 | **greater** agility, not less. 443 | 444 | Once the combatants have been sorted we iterate over them (in decreasing 445 | agility order) and allow them to choose their actions. Standard 446 | `Creature`s will just blindly attack the player (defending does nothing 447 | anyway), but when it's the player's turn we need to ask them what they 448 | want to do. After creating a `Dialogue` and populating it with all the 449 | combatants who aren't the player, we ask the player what action they 450 | want to take; attack or defend. Defending of course does nothing, but if 451 | they attack we use the `Dialogue` to ask them which combatant they're 452 | attacking. 453 | 454 | Now we don't really want to let the player attack themselves, so we 455 | omitted the player from the dialogue. But the player hasn't been removed 456 | from the combatant list, so the number they entered into the `Dialogue` 457 | is not necessarily the actual location of the combatant in the data 458 | structure. To get around that, we use a `for` loop to scan the 459 | combatants before the one chosen by the player and if the player is 460 | encountered, we skip them by extending the length of the loop. 461 | 462 | The second part of the function involves processing the events 463 | themselves. 464 | 465 | ```cpp 466 | /* battle.cpp - nextTurn continued*/ 467 | // Take each event from the queue in turn and process them, 468 | // displaying the results 469 | while(!events.empty()) 470 | { 471 | // Take event from the front of the queue 472 | BattleEvent event = events.front(); 473 | switch(event.type) 474 | { 475 | case BattleEventType::ATTACK: 476 | { 477 | // The event can't be run if either the source or the 478 | // target were slain previously in this turn, so we 479 | // must check that they're valid first 480 | auto a = this->combatants.begin(); 481 | auto b = this->combatants.end(); 482 | if(std::find(a, b, event.source) == b || std::find(a, b, event.target) == b) 483 | { 484 | break; 485 | } 486 | std::cout << event.source->name 487 | << " attacks " 488 | << event.target->name 489 | << " for " 490 | << event.run() 491 | << " damage!\n"; 492 | // Delete slain enemies 493 | if(event.target->hp <= 0) 494 | { 495 | this->kill(event.target); 496 | } 497 | break; 498 | } 499 | case BattleEventType::DEFEND: 500 | std::cout << event.source->name 501 | << " defends!\n"; 502 | break; 503 | default: 504 | break; 505 | } 506 | events.pop(); 507 | } 508 | } 509 | ``` 510 | 511 | We made the `BattleEvent` class so we didn't have to deal with 512 | processing the events inside the `nextTurn` function, but we still need 513 | to tell the player what happened and deal with combatant deaths. Instead 514 | of iterating over the events (we can't do that because they're in an 515 | `std::queue`) we repeatedly take the event from the front of the 516 | `std::queue`, deal with it, then delete it. Dealing with defend actions 517 | is easy, to deal with attack actions we first check that the `source` 518 | and `target` of the event still exist and that neither was killed 519 | earlier on. `std::find` returns an iterator to element that matches its 520 | third argument, and remember that it will return an ending iterator if 521 | the element wasn't found. If both still exist then we run the event and 522 | print how much damage was dealt before cleaning up all the slain enemies 523 | using the `kill` function. 524 | 525 | ```cpp 526 | /* battle.cpp */ 527 | void Battle::kill(Creature* creature) 528 | { 529 | // Find the creature's position in the combatants vector 530 | auto pos = std::find(this->combatants.begin(), this->combatants.end(), creature); 531 | 532 | // Don't try and delete the creature if it doesn't exist 533 | if(pos != this->combatants.end()) 534 | { 535 | std::cout << creature->name << " is slain!\n"; 536 | 537 | // Health == 0 is used in main as a condition to check if the creature is 538 | // dead, but this function could be called when the creature is not killed 539 | // by reducing their health to zero (by a death spell, for example), so we 540 | // ensure the creature's health is 0 and is marked as dead 541 | creature->hp = 0; 542 | // Remove the creature 543 | this->combatants.erase(pos); 544 | } 545 | 546 | return; 547 | } 548 | ``` 549 | 550 | Nothing too complicated here, we use the `std::find` function to get an 551 | iterator to the creature we want to delete and then use the `erase` 552 | member function (part of `std::vector`) to delete the creature after 553 | setting its health to zero. Unfortunately `erase` requires an iterator 554 | and not a pointer, which is why we have to do the conversion using 555 | `std::find`. 556 | 557 | Almost there! All that's left now is to incorporate this system into 558 | `main`, and we'll be done! Back to `main.cpp` then, add this directly 559 | before we check to see if the player's dead or not. 560 | 561 | ```cpp 562 | /* main.cpp */ 563 | // If the area has any creatures in it, start a battle with them 564 | if(areaPtr->creatures.size() > 0) 565 | { 566 | // Create a vector of pointers to the creatures in the area 567 | std::vector combatants; 568 | std::cout << "You are attacked by "; 569 | for(int i = 0; i < areaPtr->creatures.size(); ++i) 570 | { 571 | Creature* c = &(areaPtr->creatures[i]); 572 | combatants.push_back(c); 573 | std::cout << c->name << (i == areaPtr->creatures.size()-1 ? "!\n" : ", "); 574 | } 575 | // Add the player to the combatant vector 576 | combatants.push_back(&player); 577 | // Run the battle 578 | Battle battle(combatants); 579 | battle.run(); 580 | 581 | // If the player is still alive, grant them some experience, assuming 582 | // that every creature was killed 583 | if(player.hp > 0) 584 | { 585 | // Or use std::accumulate, but that requires an additional header 586 | unsigned int xp = 0; 587 | for(auto creature : areaPtr->creatures) xp += creature.xp; 588 | std::cout << "You gained " << xp << " experience!\n"; 589 | player.xp += xp; 590 | // Remove the creatures from the area 591 | areaPtr->creatures.clear(); 592 | // Restart the loop to force a save, then the game will carry on 593 | // as usual 594 | continue; 595 | } 596 | // Otherwise player is dead, so end the program 597 | else 598 | { 599 | std::cout << "\t----YOU DIED----\n Game Over\n"; 600 | return 0; 601 | } 602 | } 603 | ``` 604 | 605 | After checking that there are creatures to fight, we create an 606 | `std::vector` containing pointers to each of the creatures in the `Area` 607 | (which we already know are unique). We also tell the player that they've 608 | started a battle by printing the names of each the creatures (note that 609 | this is before they've been assigned a unique name. A better system than 610 | the one here would be to count the totals of each and then say "`3 Rats, 611 | 2 Goblins, and a Bat`" for example.) and then separating them with 612 | commas. We then add the player into the combatants list, and create the 613 | `Battle`. Since our system has no way to flee, we can safely assume that 614 | once the battle is over, either all the creatures are dead and so they 615 | can be removed from the `Area`, or the player is dead in which case it 616 | doesn't matter because, well, the player is dead! 617 | 618 | OK, give it a compile and run, fingers crossed you get something akin to 619 | the absolutely riveting game shown below. 620 | 621 | ```bash 622 | > $ ./rpg 623 | What's your name? 624 | Gentoo 625 | You are in room 1 626 | 1: Go through the sturdy wooden door 627 | 2: Search 628 | 2 629 | 630 | You find: 631 | Nothing 632 | You are in room 1 633 | 1: Go through the sturdy wooden door 634 | 2: Search 635 | 0 636 | Menu 637 | ==== 638 | 1: Items 639 | 2: Equipment 640 | 3: Character 641 | 3 642 | Character 643 | ========= 644 | Gentoo the Rogue 645 | Health: 15 / 15 646 | Strength: 4 647 | Agility: 5 648 | Level: 1 (0 / 1) 649 | ---------------- 650 | You are in room 1 651 | 1: Go through the sturdy wooden door 652 | 2: Search 653 | 0 654 | Menu 655 | ==== 656 | 1: Items 657 | 2: Equipment 658 | 3: Character 659 | 2 660 | Equipment 661 | ========= 662 | Armor: Nothing 663 | Weapon: Nothing 664 | 665 | 1: Equip Armor 666 | 2: Equip Weapon 667 | 3: Close 668 | 2 669 | 1: Dagger (1) - A small blade, probably made of iron. Keep the sharp end away from your body. 670 | Equip which item? 671 | 1 672 | ---------------- 673 | You are in room 1 674 | 1: Go through the sturdy wooden door 675 | 2: Search 676 | 1 677 | You go through the sturdy wooden door. 678 | You are attacked by Rat! 679 | What will you do? 680 | 1: Attack 681 | 2: Defend 682 | 1 683 | Who? 684 | 1: Rat 685 | 1 686 | Gentoo attacks Rat for 2 damage! 687 | Rat attacks Gentoo for 0 damage! 688 | What will you do? 689 | 1: Attack 690 | 2: Defend 691 | 1 692 | Who? 693 | 1: Rat 694 | 1 695 | Gentoo attacks Rat for 2 damage! 696 | Rat is slain! 697 | You are in room 2 698 | 1: Go through the sturdy wooden door 699 | 2: Search 700 | ``` 701 | 702 | Did it work? Hopefully it did and you now have a fully 703 | functioning---albeit simple---RPG game. If you do, keep going, you're 704 | not done! There's a whole bunch of stuff for you to implement next, such 705 | as adding more areas/enemies/items, improving the battle system 706 | (fleeing, magic, others fighting with the player, better AI), making the 707 | area system more powerful (currently only doors are possible without 708 | editing the source code, how about switches, NPCs, shops, or even a 709 | scripting system using something like [Lua](http://www.lua.org/) or 710 | [Ruby](https://www.ruby-lang.org/)?), adding more depth to the levelling 711 | system (more classes and attributes, let the player allocate points into 712 | their attributes instead of doing it automatically), or even adding 713 | graphics (2D with [SFML](http://sfml-dev.org/) or 714 | [SDL](https://www.libsdl.org/), 3D with 715 | [OpenGL](https://www.opengl.org/)) or multiplayer! 716 | 717 | I hope you've enjoyed the tutorial! 718 | --------------------------------------------------------------------------------