diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index 88da249..89dea0c 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -15,9 +15,12 @@ namespace Defaults::Game { constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over // Valores centinela del temporitzador de mort per-jugador. - constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu - constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla) - constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous) + constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu + constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla) + // Ha de ser ≥ 1.0F: PhysicsWorld separa els cossos al contacte exacte (dist == suma de radis), + // així que un amplificador < 1 fa que el check de gameplay no es dispari mai. Marge petit + // (1.05F) per tolerar floating-point i petites separacions post-impuls. + constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 1.05F; constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous) // Friendly fire system diff --git a/source/core/defaults/ship.hpp b/source/core/defaults/ship.hpp index 75e07b9..5ad8e89 100644 --- a/source/core/defaults/ship.hpp +++ b/source/core/defaults/ship.hpp @@ -24,4 +24,10 @@ namespace Defaults::Ship { constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR) + // Estat "ferit": entre primera col·lisió amb enemic i recuperació o segona col·lisió mortal. + namespace Hurt { + constexpr float DURATION = 15.0F; // Segons en estat ferit (provisional) + constexpr float BLINK_HZ = 10.0F; // Freqüència parpelleig color normal ↔ ferit + } // namespace Hurt + } // namespace Defaults::Ship diff --git a/source/game/entities/ship.cpp b/source/game/entities/ship.cpp index f021eb4..1cbde9a 100644 --- a/source/game/entities/ship.cpp +++ b/source/game/entities/ship.cpp @@ -10,6 +10,7 @@ #include #include +#include "core/audio/audio.hpp" #include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/graphics/shape_loader.hpp" @@ -62,6 +63,8 @@ void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) { // Activar invulnerabilidad solo si es respawn invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F; is_hit_ = false; + hurt_timer_ = 0.0F; + touching_enemy_prev_frame_ = false; } void Ship::processInput(float delta_time, uint8_t player_id) { @@ -115,6 +118,12 @@ void Ship::update(float delta_time) { invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F); } + // Decrementar timer d'estat HURT (a 0 → torna a normal sense efecte extern) + if (hurt_timer_ > 0.0F) { + hurt_timer_ -= delta_time; + hurt_timer_ = std::max(hurt_timer_, 0.0F); + } + // El movimiento real lo hace PhysicsWorld::update(). // Aquí solo lógica de estado. @@ -157,5 +166,21 @@ void Ship::draw() const { const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR; const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR); - Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, Defaults::Palette::SHIP); + // Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt + // a Hurt::BLINK_HZ (mateixa estètica que el wounded dels enemics). + SDL_Color color = color_normal_; + if (hurt_timer_ > 0.0F) { + const float CYCLE = 1.0F / Defaults::Ship::Hurt::BLINK_HZ; + const float T = std::fmod(hurt_timer_, CYCLE); + if (T < (CYCLE / 2.0F)) { + color = color_hurt_; + } + } + + Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, color); +} + +void Ship::herir() { + hurt_timer_ = Defaults::Ship::Hurt::DURATION; + Audio::get()->playSound(Defaults::Sound::HIT, Audio::Group::GAME); } diff --git a/source/game/entities/ship.hpp b/source/game/entities/ship.hpp index 1a957a6..2ff1696 100644 --- a/source/game/entities/ship.hpp +++ b/source/game/entities/ship.hpp @@ -53,10 +53,31 @@ class Ship : public Entities::Entity { body_.velocity = Vec2{}; // Detener al morir } + // Estat "ferit": primera col·lisió amb enemic dispara HURT; segona durant HURT mata. + void herir(); + [[nodiscard]] auto isHurt() const -> bool { return hurt_timer_ > 0.0F; } + [[nodiscard]] auto getHurtTimer() const -> float { return hurt_timer_; } + + // Edge-trigger del contacte amb enemics: un impacte només compta a la transició + // no-tocant → tocant. Sense açò, el contacte continu durant el rebot frame-a-frame + // dispararia HURT i mort en frames consecutius. + [[nodiscard]] auto wasTouchingEnemyPrevFrame() const -> bool { return touching_enemy_prev_frame_; } + void setTouchingEnemyPrevFrame(bool touching) { touching_enemy_prev_frame_ = touching; } + private: // Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad", // que es el estado coherente al que llevan tanto init() como el ctor con renderer. bool is_hit_{false}; float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable + + // Colors de la nau (propietats, prep per migració a YAML). + SDL_Color color_normal_{Defaults::Palette::SHIP}; + SDL_Color color_hurt_{Defaults::Palette::WOUNDED}; + + // >0 → estat HURT (parpelleig color_normal_ ↔ color_hurt_). + float hurt_timer_{0.0F}; + + // Edge-trigger: true si el frame anterior la nau ja estava en contacte amb un enemic. + bool touching_enemy_prev_frame_{false}; }; diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index 926c344..3c66485 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -161,22 +161,41 @@ namespace Systems::Collision { constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; for (uint8_t i = 0; i < 2; i++) { - // Skip si ya tocado / muerto / invulnerable + // Skip si ya tocado / muerto / invulnerable. NO actualitzem el flag de contacte: + // mentre estem inactius no hi ha "frame anterior" rellevant, i el respawn ja el resetea. if (ctx.hit_timer_per_player[i] > 0.0F || !ctx.ships[i].isActive() || ctx.ships[i].isInvulnerable()) { continue; } + // Comprovem si la nau toca QUALSEVOL enemic vulnerable aquest frame. + bool touching_now = false; for (const auto& enemy : ctx.enemies) { if (enemy.isInvulnerable()) { continue; } if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) { - ctx.on_player_hit(i); - break; // Solo una colisión por player por frame + touching_now = true; + break; } } + + // Edge-trigger: només compta com a impacte la transició no-tocant → tocant. + // Així el contacte continu durant el rebot frame-a-frame no dispara HURT i mort + // en frames consecutius. + const bool RISING_EDGE = touching_now && !ctx.ships[i].wasTouchingEnemyPrevFrame(); + if (RISING_EDGE) { + if (ctx.ships[i].isHurt()) { + // Segon impacte durant HURT → mort definitiva (mateix flux que abans). + ctx.on_player_hit(i); + } else { + // Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld; + // l'enemic no rep dany per decisió de disseny). + ctx.ships[i].herir(); + } + } + ctx.ships[i].setTouchingEnemyPrevFrame(touching_now); } }