Files
aee-arcade/source/game/entities/concrete/whip_attack.cpp
T

280 lines
10 KiB
C++

#include "game/entities/concrete/whip_attack.hpp"
#include <algorithm>
#include <filesystem>
#include <string>
#include <vector>
#include "game/entities/concrete/mummy.hpp"
#include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp"
#include "game/map/map.hpp"
namespace {
// Llig una entrada `sprites.<dir>: { path, size: [w, h] }` del YAML.
// Si manquen camps, retorna un DirSprite buit (queda invalidat al
// configure() per `surface_key.empty()`).
auto readDirSprite(const fkyaml::node& s) -> WhipAttack::DirSprite {
WhipAttack::DirSprite sprite;
if (s.contains("path")) {
const auto REL = s["path"].get_value<std::string>();
sprite.surface_key = std::filesystem::path(REL).filename().string();
}
if (s.contains("size")) {
const auto& sz = s["size"];
if (sz.is_sequence() && sz.size() == 2) {
sprite.frame_w = static_cast<float>(sz[0].get_value<int>());
sprite.frame_h = static_cast<float>(sz[1].get_value<int>());
}
}
return sprite;
}
// Construeix l'`AnimationComponent` del swing a partir del bloc
// `animations.swing`. Convenció igual que la resta d'entitats:
// - `frames` : llista d'índexs dins l'spritesheet (obligatori).
// - `frame_ms` : durada uniforme per frame (ms). Alternativament `fps`.
// - `loop` : per defecte true; al látigo el volem false (mor en
// completar).
auto buildSwingAnim(const fkyaml::node& yaml, std::vector<int>& active_out)
-> AnimationComponent {
std::vector<int> frames = {0};
float fps = 8.0F;
bool loop = false;
if (yaml.contains("animations") && yaml["animations"].contains("swing")) {
const auto& sw = yaml["animations"]["swing"];
if (sw.contains("frames")) {
frames.clear();
for (const auto& f : sw["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (sw.contains("frame_ms")) {
const auto MS = static_cast<float>(sw["frame_ms"].get_value<int>());
if (MS > 0.0F) { fps = 1000.0F / MS; }
} else if (sw.contains("fps")) {
fps = sw["fps"].get_value<float>();
}
if (sw.contains("loop")) { loop = sw["loop"].get_value<bool>(); }
if (sw.contains("active_frames")) {
for (const auto& f : sw["active_frames"]) {
// cppcheck-suppress useStlAlgorithm
active_out.push_back(f.get_value<int>());
}
}
}
return {frames, fps, loop};
}
} // namespace
WhipAttack::WhipAttack(const fkyaml::node& yaml) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("whip_attack");
collides_ = false;
interactable_ = false;
if (yaml.contains("sprites")) {
const auto& s = yaml["sprites"];
if (s.contains("north")) { sprite_north_ = readDirSprite(s["north"]); }
if (s.contains("south")) { sprite_south_ = readDirSprite(s["south"]); }
if (s.contains("east")) { sprite_east_ = readDirSprite(s["east"]); }
if (s.contains("west")) { sprite_west_ = readDirSprite(s["west"]); }
}
anim_swing_ = buildSwingAnim(yaml, active_frames_);
anim_ = anim_swing_;
}
void WhipAttack::configure(Direction dir, float spawn_x, float spawn_y, float range_px) {
direction_ = dir;
range_px_ = range_px;
const auto& chosen = spriteFor(dir);
setPixelRect({.x = spawn_x, .y = spawn_y, .w = chosen.frame_w, .h = chosen.frame_h});
computeDamageRect(dir, spawn_x, spawn_y, range_px);
if (anim_.has_value()) { anim_->reset(); }
}
void WhipAttack::setWorldContext(EntityManager* manager, const std::vector<Player*>* players) {
entity_manager_ = manager;
players_ = players;
}
void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px) {
// Caminem píxel a píxel des de la vora del jugador en la direcció
// d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`.
// L'amplada perpendicular és `tile_size` (els passadissos del joc
// tenen exactament aquesta mida i l'atac és lateral).
const auto TILE = static_cast<float>(Map::tile_size);
const int MAX_PX = static_cast<int>(std::max(0.0F, range_px));
const auto MAX = static_cast<float>(MAX_PX);
float length = 0.0F;
auto try_probe = [&](float l) -> bool {
SDL_FRect probe{0.0F, 0.0F, 0.0F, 0.0F};
switch (dir) {
case Direction::EAST:
probe = {.x = spawn_x, .y = spawn_y, .w = l, .h = TILE};
break;
case Direction::WEST:
// Per a W, la cara "del jugador" és la dreta; com creix
// `l`, la vora esquerra retrocedeix. spawn_x apunta al
// límit esquerre amb l = MAX (cas no retallat).
probe = {.x = spawn_x + (MAX - l), .y = spawn_y, .w = l, .h = TILE};
break;
case Direction::SOUTH:
probe = {.x = spawn_x, .y = spawn_y, .w = TILE, .h = l};
break;
case Direction::NORTH:
probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l};
break;
}
return Map::insidePlayfield(probe) && !Map::collides(probe);
};
for (int i = 1; i <= MAX_PX; ++i) {
const auto L = static_cast<float>(i);
if (try_probe(L)) {
length = L;
} else {
break;
}
}
switch (dir) {
case Direction::EAST:
damage_rect_ = {.x = spawn_x, .y = spawn_y, .w = length, .h = TILE};
return;
case Direction::WEST:
damage_rect_ = {.x = spawn_x + (MAX - length), .y = spawn_y, .w = length, .h = TILE};
return;
case Direction::SOUTH:
damage_rect_ = {.x = spawn_x, .y = spawn_y, .w = TILE, .h = length};
return;
case Direction::NORTH:
damage_rect_ = {.x = spawn_x, .y = spawn_y + (MAX - length), .w = TILE, .h = length};
return;
}
}
auto WhipAttack::getSurfaceKey() const -> const std::string& {
return spriteFor(direction_).surface_key;
}
auto WhipAttack::spriteFor(Direction dir) const -> const DirSprite& {
switch (dir) {
case Direction::NORTH:
return sprite_north_;
case Direction::SOUTH:
return sprite_south_;
case Direction::EAST:
return sprite_east_;
case Direction::WEST:
return sprite_west_;
}
return sprite_east_;
}
void WhipAttack::update(float delta_time) {
Entity::update(delta_time);
if (isFrameActive()) { applyDamage(); }
if (anim_.has_value() && anim_->completed()) { markDead(); }
}
namespace {
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
switch (d) {
case WhipAttack::Direction::NORTH:
return Mummy::Direction::UP;
case WhipAttack::Direction::SOUTH:
return Mummy::Direction::DOWN;
case WhipAttack::Direction::EAST:
return Mummy::Direction::RIGHT;
case WhipAttack::Direction::WEST:
return Mummy::Direction::LEFT;
}
return Mummy::Direction::NONE;
}
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
switch (d) {
case WhipAttack::Direction::NORTH:
return Player::Cardinal::NORTH;
case WhipAttack::Direction::SOUTH:
return Player::Cardinal::SOUTH;
case WhipAttack::Direction::EAST:
return Player::Cardinal::EAST;
case WhipAttack::Direction::WEST:
return Player::Cardinal::WEST;
}
return Player::Cardinal::NONE;
}
auto playerIsTargetable(const Player* p) -> bool {
if (p == nullptr) { return false; }
const auto STATE = p->getState();
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
}
// Tracta una mòmia candidata: si és whippable i la seua caja solapa
// amb `damage_rect`, la posa a HURT i la marca al set. Extret per
// acotar la complexitat cognitiva d'`applyDamage` (clang-tidy llindar).
void hitMummyIfApplicable(
Entity* e,
const SDL_FRect& damage_rect,
WhipAttack::Direction dir,
std::unordered_set<void*>& already_hit) {
if (e == nullptr || e->isDead()) { return; }
if (already_hit.contains(static_cast<void*>(e))) { return; }
auto* mummy = dynamic_cast<Mummy*>(e);
if (mummy == nullptr || !mummy->isWhippable()) { return; }
const auto BOX = mummy->getCollisionRect();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
mummy->enterHurt(whipDirToMummyDir(dir));
already_hit.insert(static_cast<void*>(e));
}
void hitPlayerIfApplicable(
Player* p,
const Player* owner,
const SDL_FRect& damage_rect,
WhipAttack::Direction dir,
std::unordered_set<void*>& already_hit) {
if (p == owner || !playerIsTargetable(p)) { return; }
if (already_hit.contains(static_cast<void*>(p))) { return; }
const auto BOX = p->getBaseBox();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
p->enterHurt(whipDirToPlayerCardinal(dir));
already_hit.insert(static_cast<void*>(p));
}
} // namespace
void WhipAttack::applyDamage() {
if (entity_manager_ != nullptr) {
for (auto* e : entity_manager_->getEntities()) {
hitMummyIfApplicable(e, damage_rect_, direction_, already_hit_);
}
}
if (players_ != nullptr) {
for (auto* p : *players_) {
hitPlayerIfApplicable(p, owner_, damage_rect_, direction_, already_hit_);
}
}
}
auto WhipAttack::isFrameActive() const -> bool {
if (!anim_.has_value()) { return false; }
const int FRAME = anim_->currentFrame();
return std::ranges::any_of(active_frames_, [FRAME](int f) { return f == FRAME; });
}
REGISTER_ENTITY("whip_attack", WhipAttack)