feat(whip): aplicació de dany amb retall de hitbox, alreadyHit i friendly fire

This commit is contained in:
JailDesigner committed 2026-04-28 16:20:22 +02:00
1 parent d208ba02df
commit df60709e6d
5 files changed
+217 -6

No files matched your search

@@ -5,7 +5,11 @@
#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 {
@@ -92,9 +96,73 @@ void WhipAttack::configure(Direction dir, float spawn_x, float spawn_y, float ra
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;
}
@@ -115,9 +183,93 @@ auto WhipAttack::spriteFor(Direction dir) const -> const DirSprite& {
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();