Merge branch 'feat/ship-hurt-state': estat HURT a la nau

This commit is contained in:
2026-05-22 17:32:04 +02:00
5 changed files with 81 additions and 7 deletions
+6 -3
View File
@@ -15,9 +15,12 @@ namespace Defaults::Game {
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
// Valores centinela del temporitzador de mort per-jugador. // Valores centinela del temporitzador de mort per-jugador.
constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu 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 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) // 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) constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
// Friendly fire system // Friendly fire system
+6
View File
@@ -24,4 +24,10 @@ namespace Defaults::Ship {
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR) 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 } // namespace Defaults::Ship
+26 -1
View File
@@ -10,6 +10,7 @@
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
#include "core/audio/audio.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/graphics/shape_loader.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 // Activar invulnerabilidad solo si es respawn
invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F; invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F;
is_hit_ = false; is_hit_ = false;
hurt_timer_ = 0.0F;
touching_enemy_prev_frame_ = false;
} }
void Ship::processInput(float delta_time, uint8_t player_id) { 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); 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(). // El movimiento real lo hace PhysicsWorld::update().
// Aquí solo lógica de estado. // 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 VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR;
const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_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);
} }
+21
View File
@@ -53,10 +53,31 @@ class Ship : public Entities::Entity {
body_.velocity = Vec2{}; // Detener al morir 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: private:
// Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // 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", // 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. // que es el estado coherente al que llevan tanto init() como el ctor con renderer.
bool is_hit_{false}; bool is_hit_{false};
float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable 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};
}; };
+22 -3
View File
@@ -161,22 +161,41 @@ namespace Systems::Collision {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
for (uint8_t i = 0; i < 2; i++) { 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 || if (ctx.hit_timer_per_player[i] > 0.0F ||
!ctx.ships[i].isActive() || !ctx.ships[i].isActive() ||
ctx.ships[i].isInvulnerable()) { ctx.ships[i].isInvulnerable()) {
continue; continue;
} }
// Comprovem si la nau toca QUALSEVOL enemic vulnerable aquest frame.
bool touching_now = false;
for (const auto& enemy : ctx.enemies) { for (const auto& enemy : ctx.enemies) {
if (enemy.isInvulnerable()) { if (enemy.isInvulnerable()) {
continue; continue;
} }
if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) { if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) {
ctx.on_player_hit(i); touching_now = true;
break; // Solo una colisión por player por frame 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);
} }
} }