diff --git a/ADG5.cpp b/ADG5.cpp index 555cb47..3149c62 100644 --- a/ADG5.cpp +++ b/ADG5.cpp @@ -15,8 +15,6 @@ int main(int argc, char **argv) auto console = tcod::Console(80, 50); auto params = TCOD_ContextParams{}; - int playerx = 40, playery = 25; - params.console = console.get(); params.window_title = "Andrew's Dungeon Game 5"; params.sdl_window_flags = SDL_WINDOW_RESIZABLE; @@ -25,7 +23,7 @@ int main(int argc, char **argv) params.argv = argv; auto context = tcod::Context(params); - engine = new Engine(&context, &console); + engine = new Engine(80, 50, &context, &console); engine->init(); while (engine->update()) { diff --git a/Actor.cpp b/Actor.cpp index 4e503b1..005826e 100644 --- a/Actor.cpp +++ b/Actor.cpp @@ -1,10 +1,23 @@ +#include #include "libtcod.hpp" #include "Actor.h" +#include "Map.h" +#include "Engine.h" +#include "Ai.h" +#include "Destructible.h" +#include "Attacker.h" -Actor::Actor(int x, int y, std::string_view ch, const TCOD_ColorRGB& col) : - x(x), y(y), ch(ch), col(col) { +Actor::Actor(int x, int y, std::string_view ch, std::string name, const TCOD_ColorRGB& col) : + x(x), y(y), ch(ch), col(col), name(name), + blocks(true),attacker(nullptr), destructible(nullptr), ai(nullptr) { } void Actor::render(TCOD_Console& cons) const { tcod::print(cons, { x, y }, ch, col, std::nullopt); +} + +void Actor::update() { + if (ai) { + ai->update(this); + } } \ No newline at end of file diff --git a/Actor.h b/Actor.h index dbb61c1..cfa79ef 100644 --- a/Actor.h +++ b/Actor.h @@ -2,12 +2,22 @@ #include "libtcod.hpp" +class Attacker; +class Destructible; +class Ai; + class Actor { public: int x, y; // position on map std::string_view ch; // ascii code TCOD_ColorRGB col; // color + std::string name; + bool blocks; + Attacker* attacker; + Destructible* destructible; + Ai* ai; - Actor(int x, int y, std::string_view ch, const TCOD_ColorRGB& col); + Actor(int x, int y, std::string_view ch, std::string name, const TCOD_ColorRGB& col); void render(TCOD_Console& cons) const; + void update(); }; \ No newline at end of file diff --git a/Ai.cpp b/Ai.cpp new file mode 100644 index 0000000..2e0d1ef --- /dev/null +++ b/Ai.cpp @@ -0,0 +1,129 @@ +#include +#include "Ai.h" +#include "Actor.h" +#include "Engine.h" +#include "Map.h" +#include "Destructible.h" +#include "Attacker.h" + +void PlayerAi::update(Actor* owner) { + SDL_Event event; + int dx = 0, dy = 0; + + if (owner->destructible && owner->destructible->isDead()) { + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_EVENT_QUIT: + engine->gameStatus = Engine::QUIT; + break; + default: + break; + } + } + return; + } + while (SDL_PollEvent(&event)) { + engine->context->convert_event_coordinates(event); + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + switch (event.key.key) { + case SDLK_UP: + dy = -1; + break; + case SDLK_DOWN: + dy = 1; + break; + case SDLK_LEFT: + dx = -1; + break; + case SDLK_RIGHT: + dx = 1; + break; + default: + break; + } + break; + case SDL_EVENT_QUIT: + engine->gameStatus = Engine::QUIT; + break; + default: + break; + } + + if (dx != 0 || dy != 0) { + engine->gameStatus = Engine::NEW_TURN; + if (moveOrAttack(owner, owner->x + dx, owner->y + dy)) { + engine->map->computeFov(); + } + } + } +} + +bool PlayerAi::moveOrAttack(Actor* owner, int targetx, int targety) { + if (engine->map->isWall(targetx, targety)) return false; + // look for living actors to attack + for (Actor** iterator = engine->actors.begin(); + iterator != engine->actors.end(); iterator++) { + Actor* actor = *iterator; + if (actor->destructible && !actor->destructible->isDead() + && actor->x == targetx && actor->y == targety) { + owner->attacker->attack(owner, actor); + return false; + } + } + for (Actor** iterator = engine->actors.begin(); + iterator != engine->actors.end(); iterator++) { + Actor* actor = *iterator; + if (actor->destructible && actor->destructible->isDead() + && actor->x == targetx && actor->y == targety) { + printf("There's a %s here\n", actor->name.c_str()); + } + } + owner->x = targetx; + owner->y = targety; + return true; +} + +static const int TRACKING_TURNS = 3; + +void MonsterAi::update(Actor* owner) { + if (owner->destructible && owner->destructible->isDead()) { + return; + } + if (engine->map->isInFov(owner->x, owner->y)) { + // we can see the player. move towards him + moveCount = TRACKING_TURNS; + } + else { + moveCount--; + } + if (moveCount > 0) { + // we can see the player. move towards him + moveOrAttack(owner, engine->player->x, engine->player->y); + } +} + +void MonsterAi::moveOrAttack(Actor* owner, int targetx, int targety) { + int dx = targetx - owner->x; + int dy = targety - owner->y; + int stepdx = (dx > 0 ? 1 : -1); + int stepdy = (dy > 0 ? 1 : -1); + float distance = sqrtf((float)dx * (float)dx + (float)dy * (float)dy); + if (distance >= 2) { + dx = (int)(round(dx / distance)); + dy = (int)(round(dy / distance)); + if (engine->map->canWalk(owner->x + dx, owner->y + dy)) { + owner->x += dx; + owner->y += dy; + } + else if (engine->map->canWalk(owner->x + stepdx, owner->y)) { + owner->x += stepdx; + } + else if (engine->map->canWalk(owner->x, owner->y + stepdy)) { + owner->y += stepdy; + } + } + else if (owner->attacker) { + owner->attacker->attack(owner, engine->player); + } +} \ No newline at end of file diff --git a/Ai.h b/Ai.h new file mode 100644 index 0000000..5a9dba8 --- /dev/null +++ b/Ai.h @@ -0,0 +1,25 @@ +#pragma once + +class Actor; + +class Ai { +public: + virtual void update(Actor* owner) = 0; +}; + +class PlayerAi : public Ai { +public: + void update(Actor* owner); + +protected: + bool moveOrAttack(Actor* owner, int targetx, int targety); +}; + +class MonsterAi : public Ai { +public: + void update(Actor* owner); + +protected: + int moveCount; + void moveOrAttack(Actor* owner, int targetx, int targety); +}; \ No newline at end of file diff --git a/Attacker.cpp b/Attacker.cpp new file mode 100644 index 0000000..41fff0a --- /dev/null +++ b/Attacker.cpp @@ -0,0 +1,22 @@ +#include "Attacker.h" +#include "Actor.h" +#include "Destructible.h" + +Attacker::Attacker(float power) : power(power) { +} + +void Attacker::attack(Actor* owner, Actor* target) { + if (target->destructible && !target->destructible->isDead()) { + if (power - target->destructible->defense > 0) { + printf("%s attacks %s for %g hit points.\n", owner->name.c_str(), target->name.c_str(), + power - target->destructible->defense); + } + else { + printf("%s attacks %s but it has no effect!\n", owner->name.c_str(), target->name.c_str()); + } + target->destructible->takeDamage(target, power); + } + else { + printf("%s attacks %s in vain.\n", owner->name.c_str(), target->name.c_str()); + } +} diff --git a/Attacker.h b/Attacker.h new file mode 100644 index 0000000..142f936 --- /dev/null +++ b/Attacker.h @@ -0,0 +1,11 @@ +#pragma once + +class Actor; + +class Attacker { +public: + float power; // hit points given + + Attacker(float power); + void attack(Actor* owner, Actor* target); +}; \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index d81d478..2715bcb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ project ("ADG5") find_package(libtcod CONFIG REQUIRED) # Add source to this project's executable. -add_executable (ADG5 "ADG5.cpp" "ADG5.h" "Actor.cpp" "Actor.h" "Map.h" "Map.cpp" "Engine.h" "Engine.cpp" ) +add_executable (ADG5 "ADG5.cpp" "ADG5.h" "Actor.cpp" "Actor.h" "Map.h" "Map.cpp" "Engine.h" "Engine.cpp" "Destructible.h" "Destructible.cpp" "Attacker.h" "Attacker.cpp" "Ai.h" "Ai.cpp") target_link_libraries(ADG5 PRIVATE diff --git a/Destructible.cpp b/Destructible.cpp new file mode 100644 index 0000000..3988182 --- /dev/null +++ b/Destructible.cpp @@ -0,0 +1,52 @@ +#include "Destructible.h" +#include "Actor.h" +#include "Engine.h" + +Destructible::Destructible(float maxHp, float defense, std::string corpseName) : + maxHp(maxHp), hp(maxHp), defense(defense), corpseName(corpseName) { +} + +float Destructible::takeDamage(Actor* owner, float damage) { + damage -= defense; + if (damage > 0) { + hp -= damage; + if (hp <= 0) { + die(owner); + } + } + else { + damage = 0; + } + return damage; +} + +void Destructible::die(Actor* owner) { + // transform the actor into a corpse! + owner->ch = "%"; + owner->col = TCOD_ColorRGB(100, 0, 0); + owner->name = corpseName; + owner->blocks = false; + // make sure corpses are drawn before living actors + engine->sendToBack(owner); +} + +MonsterDestructible::MonsterDestructible(float maxHp, float defense, std::string corpseName) : + Destructible(maxHp, defense, corpseName) { +} + +PlayerDestructible::PlayerDestructible(float maxHp, float defense, std::string corpseName) : + Destructible(maxHp, defense, corpseName) { +} + +void MonsterDestructible::die(Actor* owner) { + // transform it into a nasty corpse! it doesn't block, can't be + // attacked and doesn't move + printf("%s is dead\n", owner->name.c_str()); + Destructible::die(owner); +} + +void PlayerDestructible::die(Actor* owner) { + printf("You died!\n"); + Destructible::die(owner); + engine->gameStatus = Engine::DEFEAT; +} \ No newline at end of file diff --git a/Destructible.h b/Destructible.h new file mode 100644 index 0000000..a68079f --- /dev/null +++ b/Destructible.h @@ -0,0 +1,31 @@ +#pragma once + +#include +class Actor; + +class Destructible { +public: + float maxHp; // maximum health points + float hp; // current health points + float defense; // hit points deflected + std::string corpseName; // the actor's name once dead/destroyed + + Destructible(float maxHp, float defense, std::string corpseName); + inline bool isDead() { return hp <= 0; } + float takeDamage(Actor* owner, float damage); + virtual void die(Actor* owner); + + +}; + +class MonsterDestructible : public Destructible { +public: + MonsterDestructible(float maxHp, float defense, std::string corpseName); + void die(Actor* owner); +}; + +class PlayerDestructible : public Destructible { +public: + PlayerDestructible(float maxHp, float defense, std::string corpseName); + void die(Actor* owner); +}; \ No newline at end of file diff --git a/Engine.cpp b/Engine.cpp index 1beeac4..a968d01 100644 --- a/Engine.cpp +++ b/Engine.cpp @@ -3,20 +3,29 @@ #include "Actor.h" #include "Map.h" #include "Engine.h" +#include "Destructible.h" +#include "Attacker.h" +#include "Ai.h" - - -Engine::Engine(tcod::Context *context, tcod::Console *console) { +Engine::Engine(int screenWidth, int screenHeight, tcod::Context *context, tcod::Console *console) { this->context = context; this->console = console; + this->screenWidth = screenWidth; + this->screenHeight = screenHeight; + map = nullptr; player = nullptr; fovRadius = 10; computeFov = true; + gameStatus = STARTUP; } void Engine::init() { - player = new Actor(40, 25, "@", TCOD_ColorRGB(255, 255, 255)); + + player = new Actor(40, 25, "@", "player", TCOD_ColorRGB(255, 255, 255)); + player->destructible = new PlayerDestructible(30, 2, "player corpse"); + player->attacker = new Attacker(5); + player->ai = new PlayerAi(); actors.push(player); map = new Map(80, 45); } @@ -27,50 +36,21 @@ Engine::~Engine() { } bool Engine::update() { - SDL_Event event; - while (SDL_PollEvent(&event)) { - context->convert_event_coordinates(event); - switch (event.type) { - case SDL_EVENT_KEY_DOWN: - switch (event.key.key) { - case SDLK_UP: - if (!map->isWall(player->x, player->y - 1)) { - player->y--; - computeFov = true; - } - break; - case SDLK_DOWN: - if (!map->isWall(player->x, player->y + 1)) { - player->y++; - computeFov = true; - } - break; - case SDLK_LEFT: - if (!map->isWall(player->x - 1, player->y)) { - player->x--; - computeFov = true; - } - break; - case SDLK_RIGHT: - if (!map->isWall(player->x + 1, player->y)) { - player->x++; - computeFov = true; - } - break; - default: - break; - } - break; - case SDL_EVENT_QUIT: - return false; - default: - break; - } - if (computeFov) { - map->computeFov(); - computeFov = false; - } + if (gameStatus == STARTUP) map->computeFov(); + gameStatus = IDLE; + player->update(); + if (gameStatus == NEW_TURN) { + for (Actor** iterator = actors.begin(); iterator != actors.end(); + iterator++) { + Actor* actor = *iterator; + if (actor != player) { + actor->update(); + } + } + } + if (gameStatus == QUIT) { + return false; } return true; } @@ -84,9 +64,19 @@ void Engine::render() { for (Actor** iterator = actors.begin(); iterator != actors.end(); iterator++) { Actor* actor = *iterator; - if (map->isInFov(actor->x, actor->y)) { + if (actor != player && map->isInFov(actor->x, actor->y)) { actor->render(*console); } } + player->render(*console); + + std::string hp = "HP : " + std::to_string(player->destructible->hp) + "/" + std::to_string(player->destructible->maxHp); + + tcod::print(*console, { 1, 1 }, hp, TCOD_ColorRGB(255, 255, 255), std::nullopt); context->present(*console); } + +void Engine::sendToBack(Actor* actor) { + actors.remove(actor); + actors.insertBefore(actor, 0); +} \ No newline at end of file diff --git a/Engine.h b/Engine.h index 6c725fb..1dc7283 100644 --- a/Engine.h +++ b/Engine.h @@ -6,17 +6,30 @@ class Map; class Engine { public: + enum GameStatus { + STARTUP, + IDLE, + NEW_TURN, + VICTORY, + DEFEAT, + QUIT + } gameStatus; + + int screenWidth; + int screenHeight; + TCODList actors; Actor* player; Map* map; int fovRadius; tcod::Context *context; tcod::Console* console; - Engine(tcod::Context *context, tcod::Console *console); + Engine(int screenWidth, int screenHeight, tcod::Context *context, tcod::Console *console); void init(); ~Engine(); bool update(); void render(); + void sendToBack(Actor* actor); private: bool computeFov; }; diff --git a/Map.cpp b/Map.cpp index 6e9b933..0715834 100644 --- a/Map.cpp +++ b/Map.cpp @@ -2,9 +2,14 @@ #include "Map.h" #include "Actor.h" #include "Engine.h" +#include "Destructible.h" +#include "Attacker.h" +#include "Ai.h" + static const int ROOM_MAX_SIZE = 12; static const int ROOM_MIN_SIZE = 6; +static const int MAX_ROOM_MONSTERS = 3; class BspListener : public ITCODBspCallback { private: @@ -50,6 +55,21 @@ Map::~Map() { delete[] tiles; delete map; } +bool Map::canWalk(int x, int y) const { + if (isWall(x, y)) { + // this is a wall + return false; + } + for (Actor** iterator = engine->actors.begin(); + iterator != engine->actors.end();iterator++) { + Actor* actor = *iterator; + if (actor->blocks && actor->x == x && actor->y == y) { + // there is an actor there. cannot walk + return false; + } + } + return true; +} bool Map::isInFov(int x, int y) const { if (map->isInFov(x, y)) { @@ -122,9 +142,36 @@ void Map::createRoom(bool first, int x1, int y1, int x2, int y2) { } else { TCODRandom* rng = TCODRandom::getInstance(); - if (rng->getInt(0, 3) == 0) { - engine->actors.push(new Actor((x1 + x2) / 2, (y1 + y2) / 2, "@", - TCOD_ColorRGB(255, 255, 0))); + int nbMonsters = rng->getInt(0, MAX_ROOM_MONSTERS); + while (nbMonsters > 0) { + int x = rng->getInt(x1, x2); + int y = rng->getInt(y1, y2); + if (canWalk(x, y)) { + addMonster(x, y); + } + nbMonsters--; } } +} + +void Map::addMonster(int x, int y) { + TCODRandom* rng = TCODRandom::getInstance(); + if (rng->getInt(0, 100) < 80) { + // create an orc + Actor* orc = new Actor(x, y, "o", "orc", + TCOD_ColorRGB(0, 100, 0)); + orc->destructible = new MonsterDestructible(10, 0, "dead orc"); + orc->attacker = new Attacker(3); + orc->ai = new MonsterAi(); + engine->actors.push(orc); + } + else { + // create a troll + Actor* troll = new Actor(x, y, "T", "troll", + TCOD_ColorRGB(0, 255, 0)); + troll->destructible = new MonsterDestructible(16, 1, "troll carcass"); + troll->attacker = new Attacker(4); + troll->ai = new MonsterAi(); + engine->actors.push(troll); + } } \ No newline at end of file diff --git a/Map.h b/Map.h index b3f7ae3..b6c76e8 100644 --- a/Map.h +++ b/Map.h @@ -11,6 +11,7 @@ public: Map(int width, int height); ~Map(); + bool canWalk(int x, int y) const; bool isWall(int x, int y) const; bool isInFov(int x, int y) const; bool isExplored(int x, int y) const; @@ -22,6 +23,6 @@ protected: TCODMap* map; void dig(int x1, int y1, int x2, int y2); void createRoom(bool first, int x1, int y1, int x2, int y2); - + void addMonster(int x, int y); void setWall(int x, int y); }; \ No newline at end of file