From 5cb547db0ae49be751bf75e3bbb75bdbf42071e0 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 21 May 2026 10:26:13 +0200 Subject: [PATCH] feat(collision): primer impacte fereix, segon mata; mort diferida via timer (Fase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE (placeholder 1.0). - Enemy::herir(shooter_id) emmagatzema last_hit_by_ per a atribució posterior. - collision_system: helper anònim explodeNow(ctx, enemy, shooter_id) que llegeix velocity/dades ABANS de destruir() (corregeix bug latent: el codi anterior llegia getVelocityVector() després de destruir, que zera velocity → l'explosió mai heretava inèrcia). - detectBulletEnemy: primer impacte aplica impulse + herir(); segon impacte sobre enemy ferit dispara explodeNow immediata. - processWoundedDeaths: explota enemics amb wound timer expirat aquest frame. - detectAll: processWoundedDeaths abans de detectBulletEnemy (les expiracions maten primer; les bales del mateix frame ja no toquen el cos destruït). Puntos s'atribueixen a la mort real, no a l'impacte inicial. Build neta i smoke test xvfb OK. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/defaults/physics.hpp | 4 + source/game/entities/enemy.cpp | 4 +- source/game/entities/enemy.hpp | 5 +- source/game/systems/collision_system.cpp | 139 +++++++++++++++-------- source/game/systems/collision_system.hpp | 38 ++++--- 5 files changed, 120 insertions(+), 70 deletions(-) diff --git a/source/core/defaults/physics.hpp b/source/core/defaults/physics.hpp index 218cdba..0f4ec3b 100644 --- a/source/core/defaults/physics.hpp +++ b/source/core/defaults/physics.hpp @@ -34,6 +34,10 @@ namespace Defaults::Physics { // Velocity heredada de la nau a l'explosió (80% del feel original). constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F; + // Velocity heredada de l'enemic a l'explosió (palanca per a tuneo). + // 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua. + constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F; + // Angular velocity sin for trajectory inheritance // Excess above this threshold is converted to tangential linear velocity // Prevents "vortex trap" problem with high-rotation enemies diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index a8d9f28..7766297 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -255,10 +255,12 @@ void Enemy::destruir() { body_.radius = 0.0F; // No colisiona mientras está inactivo wounded_timer_ = 0.0F; wound_expired_this_frame_ = false; + last_hit_by_ = 0xFF; } -void Enemy::herir() { +void Enemy::herir(uint8_t shooter_id) { wounded_timer_ = Defaults::Enemies::Wounded::DURATION; + last_hit_by_ = shooter_id; } void Enemy::applyImpulse(const Vec2& impulse) { diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index 4d24d00..dcfa89b 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -86,11 +86,13 @@ class Enemy : public Entities::Entity { [[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; } // Estado "herido": entre primer impacto de bala y explosión diferida. - void herir(); + // shooter_id: id del jugador que herí; 0xFF = sin atribución (cadena, etc.). + void herir(uint8_t shooter_id = 0xFF); [[nodiscard]] auto isWounded() const -> bool { return wounded_timer_ > 0.0F; } [[nodiscard]] auto getWoundedTimer() const -> float { return wounded_timer_; } [[nodiscard]] auto woundExpiredThisFrame() const -> bool { return wound_expired_this_frame_; } void consumeWoundExpired() { wound_expired_this_frame_ = false; } + [[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; } // Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico. void applyImpulse(const Vec2& impulse); @@ -118,6 +120,7 @@ class Enemy : public Entities::Entity { // Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración. float wounded_timer_{0.0F}; bool wound_expired_this_frame_{false}; + uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución // Métodos privados void updateAnimation(float delta_time); diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index f610d4c..3041df8 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -10,9 +10,75 @@ namespace Systems::Collision { + namespace { + constexpr uint8_t NO_SHOOTER = 0xFF; + + // Lookup tabla puntos / color por tipo de enemy (mantiene la lógica + // anterior pero centralizada para reutilizar entre paths). + auto scoreForType(EnemyType type) -> int { + switch (type) { + case EnemyType::PENTAGON: + return Defaults::Enemies::Scoring::PENTAGON_SCORE; + case EnemyType::QUADRAT: + return Defaults::Enemies::Scoring::QUADRAT_SCORE; + case EnemyType::MOLINILLO: + return Defaults::Enemies::Scoring::MOLINILLO_SCORE; + } + return 0; + } + + auto colorForType(EnemyType type) -> SDL_Color { + switch (type) { + case EnemyType::PENTAGON: + return Defaults::Palette::PENTAGON; + case EnemyType::QUADRAT: + return Defaults::Palette::QUADRAT; + case EnemyType::MOLINILLO: + return Defaults::Palette::MOLINILLO; + } + return SDL_Color{}; + } + + // Mata al enemy con explosión: floating score, debris con velocity heredada, + // sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador. + // CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity). + void explodeNow(Context& ctx, Enemy& enemy, uint8_t shooter_id) { + const Vec2 ENEMY_POS = enemy.getCenter(); + const Vec2 ENEMY_VEL = enemy.getVelocityVector(); + const float DROTACIO = enemy.getRotationDelta(); + const float BRIGHTNESS = enemy.getBrightness(); + const auto SHAPE = enemy.getShape(); + const EnemyType TYPE = enemy.getType(); + + const int POINTS = scoreForType(TYPE); + const SDL_Color COLOR = colorForType(TYPE); + + if (shooter_id != NO_SHOOTER) { + ctx.score_per_player[shooter_id] += POINTS; + } + ctx.floating_score_manager.crear(POINTS, ENEMY_POS); + + enemy.destruir(); + + constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suave) + const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE; + ctx.debris_manager.explode( + SHAPE, + ENEMY_POS, + 0.0F, // angle (rotación interna del enemy) + 1.0F, // escala + VELOCITAT_EXPLOSIO, + BRIGHTNESS, + INHERITED_VEL, + DROTACIO, + 0.0F, // sin herencia visual + Defaults::Sound::EXPLOSION, + COLOR); + } + } // anonymous namespace + void detectBulletEnemy(Context& ctx) { constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; - constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suau) for (auto& bullet : ctx.bullets) { for (auto& enemy : ctx.enemies) { @@ -21,11 +87,9 @@ namespace Systems::Collision { } // *** COLISIÓN bullet → enemy *** - const Vec2& enemy_pos = enemy.getCenter(); - // Empuje físico: impulse en la dirección bullet→enemy (fallback a la // dirección de la bala si están exactamente solapados). - Vec2 normal = enemy_pos - bullet.getCenter(); + Vec2 normal = enemy.getCenter() - bullet.getCenter(); if (normal.lengthSquared() > 0.000001F) { normal = normal.normalized(); } else { @@ -34,59 +98,33 @@ namespace Systems::Collision { } enemy.applyImpulse(normal * Defaults::Physics::Bullet::IMPACT_IMPULSE); - // 1. Puntos según tipo - int points = 0; - switch (enemy.getType()) { - case EnemyType::PENTAGON: - points = Defaults::Enemies::Scoring::PENTAGON_SCORE; - break; - case EnemyType::QUADRAT: - points = Defaults::Enemies::Scoring::QUADRAT_SCORE; - break; - case EnemyType::MOLINILLO: - points = Defaults::Enemies::Scoring::MOLINILLO_SCORE; - break; + const uint8_t SHOOTER = bullet.getOwnerId(); + + if (enemy.isWounded()) { + // Segundo impacto sobre enemy ya herido → muerte instantánea, + // puntos al nuevo shooter. + explodeNow(ctx, enemy, SHOOTER); + } else { + // Primer impacto → entra en estado herido (explosión diferida). + enemy.herir(SHOOTER); } - uint8_t owner_id = bullet.getOwnerId(); - ctx.score_per_player[owner_id] += points; - ctx.floating_score_manager.crear(points, enemy_pos); - - // 2. Destruir enemy + crear explosión (debris hereda color del enemy) - SDL_Color enemy_color{}; - switch (enemy.getType()) { - case EnemyType::PENTAGON: - enemy_color = Defaults::Palette::PENTAGON; - break; - case EnemyType::QUADRAT: - enemy_color = Defaults::Palette::QUADRAT; - break; - case EnemyType::MOLINILLO: - enemy_color = Defaults::Palette::MOLINILLO; - break; - } - enemy.destruir(); - Vec2 vel_enemic = enemy.getVelocityVector(); - ctx.debris_manager.explode( - enemy.getShape(), - enemy_pos, - 0.0F, // angle (la rotación es interna del enemy) - 1.0F, // escala - VELOCITAT_EXPLOSIO, - enemy.getBrightness(), - vel_enemic, - enemy.getRotationDelta(), - 0.0F, // sin herencia visual - Defaults::Sound::EXPLOSION, - enemy_color); - - // 3. Desactivar bullet (solo destruye 1 enemy) bullet.desactivar(); - break; + break; // Una bala impacta a un enemy y muere } } } + void processWoundedDeaths(Context& ctx) { + for (auto& enemy : ctx.enemies) { + if (!enemy.woundExpiredThisFrame()) { + continue; + } + enemy.consumeWoundExpired(); + explodeNow(ctx, enemy, enemy.getLastHitBy()); + } + } + void detectShipEnemy(Context& ctx) { constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; @@ -158,6 +196,7 @@ namespace Systems::Collision { } void detectAll(Context& ctx) { + processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame detectBulletEnemy(ctx); detectShipEnemy(ctx); detectBulletPlayer(ctx); diff --git a/source/game/systems/collision_system.hpp b/source/game/systems/collision_system.hpp index a7ef980..8e67400 100644 --- a/source/game/systems/collision_system.hpp +++ b/source/game/systems/collision_system.hpp @@ -26,9 +26,9 @@ namespace Systems::Collision { -// Todo lo que las detecciones necesitan leer/modificar. Vive en GameScene; -// se le pasa por referencia (no copia, no ownership). -struct Context { + // Todo lo que las detecciones necesitan leer/modificar. Vive en GameScene; + // se le pasa por referencia (no copia, no ownership). + struct Context { std::array& ships; std::array& enemies; std::array(Defaults::Entities::MAX_BALES) * 2>& bullets; @@ -40,24 +40,26 @@ struct Context { const GameConfig::MatchConfig& match_config; // Trigger de muerte del jugador (GameScene::tocado). std::function on_player_hit; -}; + }; -// Detecta colisiones bullet → enemy. Si hit: -// - destruye el enemy (radius=0 en physics body) -// - crea debris + floating score -// - desactiva la bullet -// - suma puntos al shooter -void detectBulletEnemy(Context& ctx); + // Detecta colisiones bullet → enemy. Si hit: + // - Primer impacto: aplica impulse, marca al enemy como "herido", desactiva bullet. + // - Segundo impacto (enemy ya herido): explosión inmediata + puntos al shooter. + void detectBulletEnemy(Context& ctx); -// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id). -void detectShipEnemy(Context& ctx); + // Procesa enemigos cuyo wound timer ha expirado este frame: explosión + puntos + // al `last_hit_by_` del enemy (si está set). + void processWoundedDeaths(Context& ctx); -// Detecta colisiones bullet → player (friendly fire / self-hit). -// Self-hit: el shooter pierde 1 vida. Teammate-hit: la víctima pierde 1, el -// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet. -void detectBulletPlayer(Context& ctx); + // Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id). + void detectShipEnemy(Context& ctx); -// Las tres en orden lógico del frame. -void detectAll(Context& ctx); + // Detecta colisiones bullet → player (friendly fire / self-hit). + // Self-hit: el shooter pierde 1 vida. Teammate-hit: la víctima pierde 1, el + // atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet. + void detectBulletPlayer(Context& ctx); + + // Las tres en orden lógico del frame. + void detectAll(Context& ctx); } // namespace Systems::Collision