347 lines
8.8 KiB
C++
347 lines
8.8 KiB
C++
#include <SDL3/SDL.h>
|
|
#include "libtcod.hpp"
|
|
#include "Ai.h"
|
|
#include "Actor.h"
|
|
#include "Engine.h"
|
|
#include "Map.h"
|
|
#include "Destructible.h"
|
|
#include "Attacker.h"
|
|
#include "Gui.h"
|
|
#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(engine->gui->lightYellow, "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) {
|
|
case SDL_EVENT_QUIT:
|
|
engine->gameStatus = Engine::QUIT;
|
|
break;
|
|
case SDL_EVENT_KEY_DOWN:
|
|
if (event.key.key == SDLK_ESCAPE) {
|
|
engine->gameStatus = Engine::PAUSE;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
while (SDL_PollEvent(&event)) {
|
|
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;
|
|
break;
|
|
case SDL_EVENT_KEY_UP:
|
|
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;
|
|
case SDLK_ESCAPE:
|
|
engine->gameStatus = Engine::PAUSE;
|
|
break;
|
|
default:
|
|
handleActionKey(owner, event.key.key, event.key.mod);
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlayerAi::handleActionKey(Actor* owner, int sdlkey, int mod) {
|
|
switch (sdlkey) {
|
|
case SDLK_G: // pickup item
|
|
{
|
|
bool found = false;
|
|
for (Actor** iterator = engine->actors.begin();
|
|
iterator != engine->actors.end(); iterator++) {
|
|
Actor* actor = *iterator;
|
|
if (actor->pickable && actor->x == owner->x && actor->y == owner->y) {
|
|
if (actor->pickable->pick(actor, owner)) {
|
|
found = true;
|
|
engine->gui->message(engine->gui->lightGrey, "You pick up the %s.",
|
|
actor->name.c_str());
|
|
break;
|
|
}
|
|
else if (!found) {
|
|
found = true;
|
|
engine->gui->message(engine->gui->red, "Your inventory is full.");
|
|
}
|
|
}
|
|
}
|
|
if (!found) {
|
|
engine->gui->message(engine->gui->lightGrey, "There's nothing here that you can pick up.");
|
|
}
|
|
if (engine->gameStatus != Engine::QUIT)
|
|
engine->gameStatus = Engine::NEW_TURN;
|
|
}
|
|
break;
|
|
case SDLK_I:
|
|
{
|
|
Actor* actor = choseFromInventory(owner);
|
|
if (actor) {
|
|
actor->pickable->use(actor, owner);
|
|
if (engine->gameStatus != Engine::QUIT)
|
|
engine->gameStatus = Engine::NEW_TURN;
|
|
}
|
|
}
|
|
break;
|
|
case SDLK_D: // drop item
|
|
{
|
|
Actor* actor = choseFromInventory(owner);
|
|
if (actor) {
|
|
actor->pickable->drop(actor, owner);
|
|
if (engine->gameStatus != Engine::QUIT)
|
|
engine->gameStatus = Engine::NEW_TURN;
|
|
}
|
|
}
|
|
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(engine->gui->lightGrey, "There are no stairs here.");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
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;
|
|
bool corpseOrItem = (actor->destructible && actor->destructible->isDead())
|
|
|| actor->pickable;
|
|
if (corpseOrItem
|
|
&& actor->x == targetx && actor->y == targety) {
|
|
engine->gui->message(engine->gui->lightGrey, "There's a %s here.", actor->name.c_str());
|
|
}
|
|
}
|
|
owner->x = targetx;
|
|
owner->y = targety;
|
|
return true;
|
|
}
|
|
|
|
Actor* PlayerAi::choseFromInventory(Actor* owner) {
|
|
static const int INVENTORY_WIDTH = 50;
|
|
static const int INVENTORY_HEIGHT = 28;
|
|
|
|
tcod::Console con = engine->context->new_console(INVENTORY_WIDTH, INVENTORY_HEIGHT);
|
|
|
|
TCOD_ColorRGB fg(200, 180, 50);
|
|
TCOD_ColorRGB bg(0, 0, 0);
|
|
tcod::print_frame(con, { 0, 0, INVENTORY_WIDTH, INVENTORY_HEIGHT }, "Inventory", &fg, &bg);
|
|
|
|
char shortcut = 'a';
|
|
int y = 1;
|
|
for (Actor** it = owner->container->inventory.begin();
|
|
it != owner->container->inventory.end(); it++) {
|
|
Actor* actor = *it;
|
|
tcod::print(con, { 1, y }, std::string("(") + shortcut + ") " + actor->name, TCOD_ColorRGB(255, 255, 255), std::nullopt, TCOD_LEFT);
|
|
y++;
|
|
shortcut++;
|
|
}
|
|
tcod::blit(*engine->console, con, { engine->screenWidth / 2 - INVENTORY_WIDTH / 2, engine->screenHeight / 2 - INVENTORY_HEIGHT / 2 }, { 0, 0, INVENTORY_WIDTH, INVENTORY_HEIGHT });
|
|
|
|
engine->context->present(*engine->console);
|
|
SDL_Event event;
|
|
while (1) {
|
|
SDL_WaitEvent(&event);
|
|
if (event.type == SDL_EVENT_KEY_UP) {
|
|
if (event.key.key >= SDLK_A && event.key.key < SDLK_A + owner->container->inventory.size()) {
|
|
return owner->container->inventory.get(event.key.key - SDLK_A);
|
|
}
|
|
return nullptr;
|
|
}
|
|
if (event.type == SDL_EVENT_QUIT) {
|
|
engine->gameStatus = Engine::QUIT;
|
|
return nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
ConfusedMonsterAi::ConfusedMonsterAi(int nbTurns, Ai* oldAi)
|
|
: nbTurns(nbTurns), oldAi(oldAi) {
|
|
}
|
|
|
|
void ConfusedMonsterAi::update(Actor* owner) {
|
|
TCODRandom* rng = TCODRandom::getInstance();
|
|
int dx = rng->getInt(-1, 1);
|
|
int dy = rng->getInt(-1, 1);
|
|
if (dx != 0 || dy != 0) {
|
|
int destx = owner->x + dx;
|
|
int desty = owner->y + dy;
|
|
if (engine->map->canWalk(destx, desty)) {
|
|
owner->x = destx;
|
|
owner->y = desty;
|
|
}
|
|
else {
|
|
Actor* actor = engine->getActor(destx, desty);
|
|
if (actor) {
|
|
owner->attacker->attack(owner, actor);
|
|
}
|
|
}
|
|
}
|
|
nbTurns--;
|
|
if (nbTurns == 0) {
|
|
owner->ai = oldAi;
|
|
delete this;
|
|
}
|
|
}
|
|
|
|
Ai* Ai::create(TCODZip& zip) {
|
|
AiType type = (AiType)zip.getInt();
|
|
Ai* ai = NULL;
|
|
switch (type) {
|
|
case PLAYER: ai = new PlayerAi(); break;
|
|
case MONSTER: ai = new MonsterAi(); break;
|
|
case CONFUSED_MONSTER: ai = new ConfusedMonsterAi(0, NULL); break;
|
|
}
|
|
ai->load(zip);
|
|
return ai;
|
|
}
|
|
void MonsterAi::load(TCODZip& zip) {
|
|
moveCount = zip.getInt();
|
|
}
|
|
|
|
void MonsterAi::save(TCODZip& zip) {
|
|
zip.putInt(MONSTER);
|
|
zip.putInt(moveCount);
|
|
}
|
|
|
|
void ConfusedMonsterAi::load(TCODZip& zip) {
|
|
nbTurns = zip.getInt();
|
|
oldAi = Ai::create(zip);
|
|
}
|
|
|
|
void ConfusedMonsterAi::save(TCODZip& zip) {
|
|
zip.putInt(CONFUSED_MONSTER);
|
|
zip.putInt(nbTurns);
|
|
oldAi->save(zip);
|
|
}
|
|
|
|
void PlayerAi::load(TCODZip& zip) {
|
|
}
|
|
|
|
void PlayerAi::save(TCODZip& zip) {
|
|
zip.putInt(PLAYER);
|
|
} |