finished tutorial

This commit is contained in:
Andrew Pamment 2025-04-21 22:47:44 +10:00
parent 89b4bce734
commit 843606bcbf
11 changed files with 158 additions and 33 deletions

View File

@ -11,7 +11,7 @@
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), pickable(nullptr), container(nullptr) {
blocks(true), attacker(nullptr), destructible(nullptr), ai(nullptr), pickable(nullptr), container(nullptr), fovOnly(true) {
}

View File

@ -17,6 +17,7 @@ public:
TCOD_ColorRGB col; // color
std::string name;
bool blocks;
bool fovOnly; // only display when in fov
Attacker* attacker;
Destructible* destructible;
Ai* ai;

51
Ai.cpp
View File

@ -10,10 +10,43 @@
#include "Pickable.h"
#include "Container.h"
PlayerAi::PlayerAi() : xpLevel(1) {
}
const int LEVEL_UP_BASE = 200;
const int LEVEL_UP_FACTOR = 150;
int PlayerAi::getNextLevelXp() {
return LEVEL_UP_BASE + xpLevel * LEVEL_UP_FACTOR;
}
void PlayerAi::update(Actor* owner) {
SDL_Event event;
int dx = 0, dy = 0;
int levelUpXp = getNextLevelXp();
if (owner->destructible->xp >= levelUpXp) {
xpLevel++;
owner->destructible->xp -= levelUpXp;
engine->gui->message(TCOD_ColorRGB(255, 255, 100), "Your battle skills grow stronger! You reached level %d", xpLevel);
engine->gui->menu.clear();
engine->gui->menu.addItem(Menu::CONSTITUTION, "Constitution (+20HP)");
engine->gui->menu.addItem(Menu::STRENGTH, "Strength (+1 attack)");
engine->gui->menu.addItem(Menu::AGILITY, "Agility (+1 defense)");
Menu::MenuItemCode menuItem = engine->gui->menu.pick(engine->context, engine->console, Menu::PAUSE);
switch (menuItem) {
case Menu::CONSTITUTION:
owner->destructible->maxHp += 20;
owner->destructible->hp += 20;
break;
case Menu::STRENGTH:
owner->attacker->power += 1;
break;
case Menu::AGILITY:
owner->destructible->defense += 1;
break;
default:break;
}
}
if (owner->destructible && owner->destructible->isDead()) {
while (SDL_PollEvent(&event)) {
switch (event.type) {
@ -56,9 +89,7 @@ void PlayerAi::update(Actor* owner) {
engine->gameStatus = Engine::PAUSE;
break;
default:
if (event.key.key >= SDLK_A && event.key.key <= SDLK_Z) {
handleActionKey(owner, event.key.key);
}
handleActionKey(owner, event.key.key, event.key.mod);
break;
}
break;
@ -78,7 +109,7 @@ void PlayerAi::update(Actor* owner) {
}
}
void PlayerAi::handleActionKey(Actor* owner, int sdlkey) {
void PlayerAi::handleActionKey(Actor* owner, int sdlkey, int mod) {
switch (sdlkey) {
case SDLK_G: // pickup item
{
@ -126,6 +157,16 @@ void PlayerAi::handleActionKey(Actor* owner, int sdlkey) {
}
}
break;
case SDLK_PERIOD:
if (mod & SDL_KMOD_SHIFT) {
if (engine->stairs->x == owner->x && engine->stairs->y == owner->y) {
engine->nextLevel();
}
else {
engine->gui->message(TCOD_ColorRGB(200, 200, 200), "There are no stairs here.");
}
}
break;
}
}

6
Ai.h
View File

@ -20,9 +20,13 @@ public:
void update(Actor* owner);
void load(TCODZip& zip);
void save(TCODZip& zip);
int xpLevel;
PlayerAi();
int getNextLevelXp();
protected:
bool moveOrAttack(Actor* owner, int targetx, int targety);
void handleActionKey(Actor* owner, int sdlkey);
void handleActionKey(Actor* owner, int sdlkey, int mod);
Actor* choseFromInventory(Actor* owner);
};

View File

@ -3,8 +3,8 @@
#include "Engine.h"
#include "Gui.h"
Destructible::Destructible(float maxHp, float defense, std::string corpseName) :
maxHp(maxHp), hp(maxHp), defense(defense), corpseName(corpseName) {
Destructible::Destructible(float maxHp, float defense, std::string corpseName, int xp) :
maxHp(maxHp), hp(maxHp), defense(defense), corpseName(corpseName), xp(xp) {
}
float Destructible::takeDamage(Actor* owner, float damage) {
@ -39,12 +39,12 @@ void Destructible::die(Actor* owner) {
engine->sendToBack(owner);
}
MonsterDestructible::MonsterDestructible(float maxHp, float defense, std::string corpseName) :
Destructible(maxHp, defense, corpseName) {
MonsterDestructible::MonsterDestructible(float maxHp, float defense, std::string corpseName, int xp) :
Destructible(maxHp, defense, corpseName, xp) {
}
PlayerDestructible::PlayerDestructible(float maxHp, float defense, std::string corpseName) :
Destructible(maxHp, defense, corpseName) {
Destructible(maxHp, defense, corpseName, 1) {
}
void PlayerDestructible::save(TCODZip& zip) {
@ -60,7 +60,10 @@ void MonsterDestructible::save(TCODZip& zip) {
void MonsterDestructible::die(Actor* owner) {
// transform it into a nasty corpse! it doesn't block, can't be
// attacked and doesn't move
engine->gui->message(TCOD_ColorRGB(150, 150, 150), "%s is dead\n", owner->name.c_str());
engine->gui->message(TCOD_ColorRGB(150, 150, 150), "%s is dead. You gain %d xp",
owner->name.c_str(), xp);
engine->player->destructible->xp += xp;
Destructible::die(owner);
}
@ -75,6 +78,7 @@ void Destructible::load(TCODZip& zip) {
hp = zip.getFloat();
defense = zip.getFloat();
corpseName = zip.getString();
xp = zip.getInt();
}
void Destructible::save(TCODZip& zip) {
@ -82,13 +86,14 @@ void Destructible::save(TCODZip& zip) {
zip.putFloat(hp);
zip.putFloat(defense);
zip.putString(corpseName.c_str());
zip.putInt(xp);
}
Destructible* Destructible::create(TCODZip& zip) {
DestructibleType type = (DestructibleType)zip.getInt();
Destructible* destructible = NULL;
switch (type) {
case MONSTER: destructible = new MonsterDestructible(0, 0, ""); break;
case MONSTER: destructible = new MonsterDestructible(0, 0, "", 0); break;
case PLAYER: destructible = new PlayerDestructible(0, 0, ""); break;
}
destructible->load(zip);

View File

@ -13,7 +13,7 @@ public:
float defense; // hit points deflected
std::string corpseName; // the actor's name once dead/destroyed
Destructible(float maxHp, float defense, std::string corpseName);
Destructible(float maxHp, float defense, std::string corpseName, int xp);
virtual ~Destructible() {};
inline bool isDead() { return hp <= 0; }
float takeDamage(Actor* owner, float damage);
@ -23,6 +23,7 @@ public:
void load(TCODZip& zip);
void save(TCODZip& zip);
static Destructible* create(TCODZip& zip);
int xp; // XP gained when killing this monster (or player xp)
protected:
enum DestructibleType {
MONSTER, PLAYER
@ -31,7 +32,7 @@ protected:
class MonsterDestructible : public Destructible {
public:
MonsterDestructible(float maxHp, float defense, std::string corpseName);
MonsterDestructible(float maxHp, float defense, std::string corpseName, int xp);
void save(TCODZip& zip);
void die(Actor* owner);
};

View File

@ -14,6 +14,7 @@ Engine::Engine(int screenWidth, int screenHeight, tcod::Context *context, tcod::
this->console = console;
this->screenWidth = screenWidth;
this->screenHeight = screenHeight;
this->level = 1;
gui = nullptr;
map = nullptr;
player = nullptr;
@ -32,13 +33,36 @@ void Engine::init() {
player->ai = new PlayerAi();
player->container = new Container(26);
actors.push(player);
map = new Map(80, 45);
stairs = new Actor(0, 0, ">", "stairs", TCOD_ColorRGB(255,255,255));
stairs->blocks = false;
stairs->fovOnly = false;
actors.push(stairs);
map = new Map(80, 43);
map->init(true);
gui->message(TCOD_ColorRGB(150,0,0),
"Welcome stranger!\nPrepare to perish in the Tombs of Andrew's Dunegon.");
gameStatus = STARTUP;
}
void Engine::nextLevel() {
level++;
gui->message(TCOD_ColorRGB(255, 100, 255), "You take a moment to rest, and recover your strength.");
player->destructible->heal(player->destructible->maxHp / 2);
gui->message(TCOD_ColorRGB(255, 100, 100), "After a rare moment of peace, you descend\ndeeper into the heart of the dungeon...");
delete map;
// delete all actors but player and stairs
for (Actor** it = actors.begin(); it != actors.end(); it++) {
if (*it != player && *it != stairs) {
delete* it;
it = actors.remove(it);
}
}
// create a new map
map = new Map(80, 43);
map->init(true);
gameStatus = STARTUP;
}
void Engine::term() {
actors.clearAndDelete();
if (map) delete map;
@ -84,7 +108,9 @@ void Engine::render(bool present) {
for (Actor** iterator = actors.begin();
iterator != actors.end(); iterator++) {
Actor* actor = *iterator;
if (actor != player && map->isInFov(actor->x, actor->y)) {
if (actor != player && ((!actor->fovOnly && map->isExplored(actor->x, actor->y))
|| map->isInFov(actor->x, actor->y))) {
actor->render(*console);
}
}
@ -141,8 +167,8 @@ bool Engine::pickATile(int* x, int* y, float maxRange) {
engine->context->convert_event_coordinates(event);
switch (event.type) {
case SDL_EVENT_MOUSE_MOTION:
engine->mouse.cx = event.motion.x;
engine->mouse.cy = event.motion.y;
engine->mouse.cx = (int)event.motion.x;
engine->mouse.cy = (int)event.motion.y;
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
engine->mouse.lbutton_pressed = event.button.button == SDL_BUTTON_LEFT;
@ -189,15 +215,18 @@ void Engine::save() {
else {
TCODZip zip;
// save the map first
zip.putInt(level);
zip.putInt(map->width);
zip.putInt(map->height);
map->save(zip);
// then the player
player->save(zip);
// then the stairs
stairs->save(zip);
// then all the other actors
zip.putInt(actors.size() - 1);
zip.putInt(actors.size() - 2);
for (Actor** it = actors.begin(); it != actors.end(); it++) {
if (*it != player) {
if (*it != player && *it != stairs) {
(*it)->save(zip);
}
}
@ -232,6 +261,7 @@ void Engine::load() {
term();
zip.loadFromFile("game.sav");
// load the map
level = zip.getInt();
int width = zip.getInt();
int height = zip.getInt();
map = new Map(width, height);
@ -240,6 +270,10 @@ void Engine::load() {
player = new Actor(0, 0, "?", "", TCOD_ColorRGB(255, 255, 255));
player->load(zip);
actors.push(player);
// the stairs
stairs = new Actor(0, 0, "?", "", TCOD_ColorRGB(255, 255, 255));
stairs->load(zip);
actors.push(stairs);
// then all other actors
int nbActors = zip.getInt();
while (nbActors > 0) {

View File

@ -22,6 +22,9 @@ public:
TCOD_mouse_t mouse;
TCODList<Actor*> actors;
Actor* player;
Actor* stairs;
int level;
void nextLevel();
Map* map;
Gui* gui;
int fovRadius;

36
Gui.cpp
View File

@ -1,9 +1,11 @@
#include <SDL3/SDL.h>
#include "libtcod.hpp"
#include "Gui.h"
#include "Engine.h"
#include "Actor.h"
#include "Map.h"
#include "Destructible.h"
#include "Ai.h"
static const int PANEL_HEIGHT = 7;
static const int BAR_WIDTH = 20;
@ -30,6 +32,13 @@ void Gui::render() {
renderBar(1, 1, BAR_WIDTH, "HP", engine->player->destructible->hp,
engine->player->destructible->maxHp,
TCOD_ColorRGB(255,100,100), TCOD_ColorRGB(100, 0, 0));
// draw the XP bar
PlayerAi* ai = (PlayerAi*)engine->player->ai;
renderBar(1, 5, BAR_WIDTH, "XP(" + std::to_string(ai->xpLevel) + ")", engine->player->destructible->xp,
ai->getNextLevelXp(),
TCOD_ColorRGB(255, 100, 255), TCOD_ColorRGB(100,0,100));
tcod::print(con, { 3, 3 }, "Dungeon level " + std::to_string(engine->level), TCOD_ColorRGB(255, 255, 255), std::nullopt);
// draw the message log
int y = 1;
@ -158,20 +167,36 @@ void Menu::addItem(MenuItemCode code, std::string label) {
item->label = label;
items.push(item);
}
const int PAUSE_MENU_WIDTH = 30;
const int PAUSE_MENU_HEIGHT = 15;
Menu::MenuItemCode Menu::pick(tcod::Context *ctx, tcod::Console *con, DisplayMode mode) {
Menu::MenuItemCode Menu::pick(tcod::Context *ctx, tcod::Console *con) {
static TCODImage img("menu_background1.png");
int selectedItem = 0;
int menux, menuy;
while (true) {
if (mode == PAUSE) {
menux = engine->screenWidth / 2 - PAUSE_MENU_WIDTH / 2;
menuy = engine->screenHeight / 2 - PAUSE_MENU_HEIGHT / 2;
TCOD_ColorRGB fg(200, 180, 50);
TCOD_ColorRGB bg(0, 0, 0);
tcod::print_frame(*con, { menux, menuy, PAUSE_MENU_WIDTH, PAUSE_MENU_HEIGHT }, "Inventory", &fg, &bg, TCOD_BKGND_OVERLAY);
menux += 2;
menuy += 3;
}
else {
static TCODImage img("menu_background1.png");
img.blit2x(*con, 0, 0);
menux = 10;
menuy = con->get_height() / 3;
}
int currentItem = 0;
for (MenuItem** it = items.begin(); it != items.end(); it++) {
if (currentItem == selectedItem) {
tcod::print(*con, { 10, 10 + currentItem * 3 }, (*it)->label,TCOD_ColorRGB(255, 100, 255), std::nullopt);
tcod::print(*con, { menux, menuy + currentItem * 3 }, (*it)->label,TCOD_ColorRGB(255, 100, 255), std::nullopt);
}
else {
tcod::print(*con, { 10, 10 + currentItem * 3 }, (*it)->label, TCOD_ColorRGB(200, 200, 200), std::nullopt);
tcod::print(*con, { menux, menuy + currentItem * 3 }, (*it)->label, TCOD_ColorRGB(200, 200, 200), std::nullopt);
}
currentItem++;
}
@ -208,3 +233,4 @@ Menu::MenuItemCode Menu::pick(tcod::Context *ctx, tcod::Console *con) {
}
}
}

11
Gui.h
View File

@ -9,12 +9,19 @@ public:
NONE,
NEW_GAME,
CONTINUE,
EXIT
EXIT,
CONSTITUTION,
STRENGTH,
AGILITY
};
enum DisplayMode {
MAIN,
PAUSE
};
~Menu();
void clear();
void addItem(MenuItemCode code, std::string label);
MenuItemCode pick(tcod::Context* ctx, tcod::Console* con);
MenuItemCode pick(tcod::Context* ctx, tcod::Console* con, DisplayMode mode = MAIN);
protected:
struct MenuItem {
MenuItemCode code;

View File

@ -175,6 +175,9 @@ void Map::createRoom(bool first, int x1, int y1, int x2, int y2, bool withActors
nbItems--;
}
}
// set stairs position
engine->stairs->x = (x1 + x2) / 2;
engine->stairs->y = (y1 + y2) / 2;
}
void Map::addMonster(int x, int y) {
@ -183,7 +186,7 @@ void Map::addMonster(int x, int y) {
// 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->destructible = new MonsterDestructible(10, 0, "dead orc", 5);
orc->attacker = new Attacker(3);
orc->ai = new MonsterAi();
engine->actors.push(orc);
@ -192,7 +195,7 @@ void Map::addMonster(int x, int y) {
// 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->destructible = new MonsterDestructible(16, 1, "troll carcass", 10);
troll->attacker = new Attacker(4);
troll->ai = new MonsterAi();
engine->actors.push(troll);