This commit is contained in:
Andrew Pamment 2025-04-20 14:24:02 +10:00
parent a6f7aacdfb
commit 7d6094a049
14 changed files with 402 additions and 60 deletions

View File

@ -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()) {

View File

@ -1,10 +1,23 @@
#include <cstdio>
#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);
}
}

12
Actor.h
View File

@ -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();
};

129
Ai.cpp Normal file
View File

@ -0,0 +1,129 @@
#include <SDL3/SDL.h>
#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);
}
}

25
Ai.h Normal file
View File

@ -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);
};

22
Attacker.cpp Normal file
View File

@ -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());
}
}

11
Attacker.h Normal file
View File

@ -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);
};

View File

@ -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

52
Destructible.cpp Normal file
View File

@ -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;
}

31
Destructible.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <string>
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);
};

View File

@ -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);
}

View File

@ -6,17 +6,30 @@ class Map;
class Engine {
public:
enum GameStatus {
STARTUP,
IDLE,
NEW_TURN,
VICTORY,
DEFEAT,
QUIT
} gameStatus;
int screenWidth;
int screenHeight;
TCODList<Actor*> 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;
};

53
Map.cpp
View File

@ -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);
}
}

3
Map.h
View File

@ -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);
};