feat(whip): aplicació de dany amb retall de hitbox, alreadyHit i friendly fire
This commit is contained in:
1 parent
d208ba02df
commit
df60709e6d
5 files changed
+217
-6
No files matched your search
+11
-2
@@ -33,7 +33,10 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi
|
||||
# Sources comuns del sistema d'entitats: s'afegeixen tant a l'executable
|
||||
# principal com al smoke test. Mantenir-los en variable separada evita
|
||||
# duplicació entre els dos targets.
|
||||
set(ENTITY_SOURCES
|
||||
# Entitats sense dependència directa amb Player.cpp: les enllacen tant
|
||||
# l'executable principal com el test_entities_smoke (que evita arrossegar
|
||||
# l'stack del jugador i les seues dependències pesades).
|
||||
set(ENTITY_SOURCES_BASE
|
||||
source/game/entities/entity.cpp
|
||||
source/game/entities/animation_component.cpp
|
||||
source/game/entities/event_bus.cpp
|
||||
@@ -48,6 +51,12 @@ set(ENTITY_SOURCES
|
||||
source/game/entities/concrete/wall_trap.cpp
|
||||
source/game/entities/concrete/floor_trap.cpp
|
||||
source/game/entities/concrete/dust.cpp
|
||||
)
|
||||
|
||||
# Entitats que criden directament símbols de Player (com WhipAttack via
|
||||
# `enterHurt`/`getBaseBox`): només van a l'executable principal.
|
||||
set(ENTITY_SOURCES
|
||||
${ENTITY_SOURCES_BASE}
|
||||
source/game/entities/concrete/whip_attack.cpp
|
||||
)
|
||||
|
||||
@@ -339,7 +348,7 @@ if(BUILD_TESTS)
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/game/map/map.cpp
|
||||
${ENTITY_SOURCES}
|
||||
${ENTITY_SOURCES_BASE}
|
||||
)
|
||||
target_include_directories(test_entities_smoke PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
@@ -10,6 +12,8 @@
|
||||
#include "game/entities/attack.hpp"
|
||||
|
||||
class Entity;
|
||||
class EntityManager;
|
||||
class Player;
|
||||
|
||||
// Atac de látigo: spawnejat pel jugador en entrar a `ATTACKING`. Viu la
|
||||
// durada de la seua animació (`swing`) i s'autoelimina al final.
|
||||
@@ -51,10 +55,20 @@ class WhipAttack : public Attack {
|
||||
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
|
||||
// primer `update`. Selecciona la surface i la mida del pixel_rect_
|
||||
// segons direcció, i posa la cantonada superior-esquerra de la
|
||||
// hitbox a (spawn_x, spawn_y).
|
||||
// hitbox a (spawn_x, spawn_y). També calcula la `damage_rect_`
|
||||
// retallada contra murs i sarcs (mai sortiria del playfield ni
|
||||
// travessaria un sarc).
|
||||
void configure(Direction dir, float spawn_x, float spawn_y, float range_px);
|
||||
|
||||
// Avança animació; quan completa, marca dead.
|
||||
// Injecta el context que necessita la fase de dany: punter a
|
||||
// l'`EntityManager` (per iterar Hazards) i a la llista de jugadors
|
||||
// (per al friendly fire). Si no es crida, els frames actius no
|
||||
// apliquen dany — el whip queda merament visual. Cridar abans del
|
||||
// primer `update` (típicament just després de `configure`).
|
||||
void setWorldContext(EntityManager* manager, const std::vector<Player*>* players);
|
||||
|
||||
// Avança animació; durant frames actius, aplica dany; quan
|
||||
// l'animació es completa, marca dead.
|
||||
void update(float delta_time) override;
|
||||
|
||||
[[nodiscard]] auto getDirection() const -> Direction { return direction_; }
|
||||
@@ -71,6 +85,11 @@ class WhipAttack : public Attack {
|
||||
// Llargada de la hitbox abans de retallar contra murs/sarcs (en píxels).
|
||||
[[nodiscard]] auto getRangePx() const -> float { return range_px_; }
|
||||
|
||||
// Hitbox efectiva de dany (ja retallada pel primer bloquejador en la
|
||||
// línia d'atac). Exposada per als smoke tests; en producció només la
|
||||
// consumeix la pròpia `applyDamage`.
|
||||
[[nodiscard]] auto getDamageRect() const -> SDL_FRect { return damage_rect_; }
|
||||
|
||||
// Clau al `Resource::Cache` per a la surface de la direcció actual
|
||||
// (la que ha triat `configure()`). El caller (típicament `GameScene`)
|
||||
// resol la surface i la passa via `setSurface()`. Aquesta separació
|
||||
@@ -81,11 +100,31 @@ class WhipAttack : public Attack {
|
||||
private:
|
||||
[[nodiscard]] auto spriteFor(Direction dir) const -> const DirSprite&;
|
||||
|
||||
// Calcula la `damage_rect_` final: retalla la longitud de l'atac
|
||||
// contra el primer bloquejador (mur extern del playfield o sarc) en
|
||||
// la línia d'atac. Implementació iterativa píxel a píxel — el rang
|
||||
// típic és ~48 px, així que el cost és menyspreable.
|
||||
void computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px);
|
||||
|
||||
// Aplica dany aquest frame: itera Hazards (Mummy whippable) i
|
||||
// jugadors (excepte `owner_`) que solapen amb `damage_rect_` i
|
||||
// estan en estat "atacable", crida `enterHurt` amb la direcció
|
||||
// adequada i els marca a `already_hit_` per garantir un sol
|
||||
// impacte per swing per objectiu.
|
||||
void applyDamage();
|
||||
|
||||
Direction direction_{Direction::EAST};
|
||||
AnimationComponent anim_swing_;
|
||||
std::vector<int> active_frames_;
|
||||
std::set<Entity*> already_hit_;
|
||||
// Set d'objectius ja colpejats aquest swing: usem `void*` perquè
|
||||
// hi guardem tant `Entity*` (Mummy) com `Player*` (que NO hereta
|
||||
// d'Entity) sense `reinterpret_cast` semànticament dubtós.
|
||||
std::unordered_set<void*> already_hit_;
|
||||
float range_px_{0.0F};
|
||||
SDL_FRect damage_rect_{0.0F, 0.0F, 0.0F, 0.0F};
|
||||
|
||||
EntityManager* entity_manager_{nullptr};
|
||||
const std::vector<Player*>* players_{nullptr};
|
||||
|
||||
DirSprite sprite_north_;
|
||||
DirSprite sprite_south_;
|
||||
|
||||
@@ -441,12 +441,16 @@ class Player : public AnimatedSprite {
|
||||
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
||||
// El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix
|
||||
// la 4a sense reorganitzar el sistema d'animacions.
|
||||
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
|
||||
// un altre jugador (friendly fire).
|
||||
public:
|
||||
enum class Cardinal : std::uint8_t { NONE,
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
private:
|
||||
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
||||
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
||||
// el comportament és instantani: soltar input para el jugador en sec.
|
||||
@@ -554,8 +558,12 @@ class Player : public AnimatedSprite {
|
||||
// i tria el que dóna més recorregut lliure.
|
||||
// Decelera linealment fins a parar i, en eixe instant, torna a
|
||||
// GROUNDED. Sense col·lisió amb altres jugadors mentre dura.
|
||||
// Pública: la dispara tant la cinemàtica de "caure damunt" com el
|
||||
// `WhipAttack` quan colpeja un altre jugador (friendly fire).
|
||||
public:
|
||||
void enterHurt(Cardinal preferred = Cardinal::NONE);
|
||||
|
||||
private:
|
||||
// Tria la direcció definitiva del HURT. Si `preferred` cabe el
|
||||
// recorregut natural complet, la retorna; d'altra manera tria la
|
||||
// direcció amb més recorregut lliure entre els 4 cardinals.
|
||||
|
||||
@@ -908,6 +908,9 @@ void GameScene::spawnWhip(const WhipSpawnPayload& payload) {
|
||||
payload.spawn_y,
|
||||
payload.range_px);
|
||||
whip->setSurface(Resource::Cache::get().getSurface(whip->getSurfaceKey()));
|
||||
// Donem accés a Hazards i jugadors per a la fase de dany. Sense això
|
||||
// el whip queda merament visual.
|
||||
whip->setWorldContext(&entity_manager_, &players_view_);
|
||||
entity_manager_.add(std::move(whip));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user