#include "game/entities/concrete/whip_attack.hpp" #include #include #include #include #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/gameplay_config.hpp" #include "game/map/map.hpp" namespace { // Llig una entrada `sprites.: { 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(); 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(sz[0].get_value()); sprite.frame_h = static_cast(sz[1].get_value()); } } 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& active_out) -> AnimationComponent { std::vector 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()); } if (frames.empty()) { frames.push_back(0); } } if (sw.contains("frame_ms")) { const auto MS = static_cast(sw["frame_ms"].get_value()); if (MS > 0.0F) { fps = 1000.0F / MS; } } else if (sw.contains("fps")) { fps = sw["fps"].get_value(); } if (sw.contains("loop")) { loop = sw["loop"].get_value(); } if (sw.contains("active_frames")) { for (const auto& f : sw["active_frames"]) { // cppcheck-suppress useStlAlgorithm active_out.push_back(f.get_value()); } } } return {frames, fps, loop}; } } // namespace WhipAttack::WhipAttack(const fkyaml::node& yaml) { name_ = yaml.contains("name") ? yaml["name"].get_value() : 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, const SDL_FRect& owner_box, float range_px) { direction_ = dir; range_px_ = range_px; // Draw rect (= pixel_rect_): cobreix tot el sprite del látigo segons // la taula d'offsets del disseny: // E: (player.x - 16, player.y ), 80×32 // W: (player.x - 48, player.y ), 80×32 // N: (player.x, player.y - 48 ), 16×96 // S: (player.x, player.y - 16 ), 16×96 const auto& chosen = spriteFor(dir); SDL_FRect draw{0.0F, 0.0F, chosen.frame_w, chosen.frame_h}; switch (dir) { case Direction::EAST: draw.x = owner_box.x - 16.0F; draw.y = owner_box.y; break; case Direction::WEST: draw.x = owner_box.x - 48.0F; draw.y = owner_box.y; break; case Direction::NORTH: draw.x = owner_box.x; draw.y = owner_box.y - 48.0F; break; case Direction::SOUTH: draw.x = owner_box.x; draw.y = owner_box.y - 16.0F; break; } setPixelRect(draw); // Damage rect: 48×16 (E/W) o 16×48 (N/S) al nivell dels peus del // jugador (els últims 16 px del seu pixel_rect — convenció estàndard // del player AEE), pegat al borde en la direcció d'atac. // computeDamageRect rep la cantonada del rect MÀXIM (sense retallar) // i recorre internament la línia per topar amb el primer bloquejador. const float COLLISION_TOP_Y = owner_box.y + owner_box.h - 16.0F; const float OWNER_RIGHT = owner_box.x + owner_box.w; const float OWNER_BOTTOM = owner_box.y + owner_box.h; float spawn_x = 0.0F; float spawn_y = 0.0F; switch (dir) { case Direction::EAST: spawn_x = OWNER_RIGHT; spawn_y = COLLISION_TOP_Y; break; case Direction::WEST: spawn_x = owner_box.x - range_px; spawn_y = COLLISION_TOP_Y; break; case Direction::NORTH: spawn_x = owner_box.x; spawn_y = COLLISION_TOP_Y - range_px; break; case Direction::SOUTH: spawn_x = owner_box.x; spawn_y = OWNER_BOTTOM; break; } computeDamageRect(dir, spawn_x, spawn_y, range_px); // Snapshot del feet_y del jugador. La convenció estàndard del Player // AEE té el seu feet_y a la base inferior del pixel_rect (collision // 16×16 als últims 16 px del sprite 16×32), així que owner_box.y + // owner_box.h reprodueix `Player::getFeetY()` exactament. owner_feet_y_ = owner_box.y + owner_box.h; if (anim_.has_value()) { anim_->reset(); } } auto WhipAttack::getFeetY() const -> float { return owner_feet_y_; } void WhipAttack::setWorldContext(EntityManager* manager, const std::vector* 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). // // Si el tweak `whip.damage_pierces_walls` està actiu, la hitbox // ignora els sarcòfags (passa a través del bloc complet) però // continua respectant el `insidePlayfield`: fora de la sala no hi // ha res a colpejar i ampliar-ho generaria visuals/edge cases // estranys. El mur extern, doncs, sempre retalla. const auto TILE = static_cast(Map::tile_size); const int MAX_PX = static_cast(std::max(0.0F, range_px)); const auto MAX = static_cast(MAX_PX); const bool PIERCES = GameplayConfig::get().whipDamagePiercesWalls(); 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; } if (!Map::insidePlayfield(probe)) { return false; } if (PIERCES) { return true; } return !Map::collides(probe); }; for (int i = 1; i <= MAX_PX; ++i) { const auto L = static_cast(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::NORTH; case WhipAttack::Direction::SOUTH: return Mummy::Direction::SOUTH; case WhipAttack::Direction::EAST: return Mummy::Direction::EAST; case WhipAttack::Direction::WEST: return Mummy::Direction::WEST; } 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& already_hit) { if (e == nullptr || e->isDead()) { return; } if (already_hit.contains(static_cast(e))) { return; } auto* mummy = dynamic_cast(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(e)); } void hitPlayerIfApplicable( Player* p, const Player* owner, const SDL_FRect& damage_rect, WhipAttack::Direction dir, std::unordered_set& already_hit) { if (p == owner || !playerIsTargetable(p)) { return; } if (already_hit.contains(static_cast(p))) { return; } const auto BOX = p->getBaseBox(); if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; } p->enterHurt(whipDirToPlayerCardinal(dir)); already_hit.insert(static_cast(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)