Merge branch 'feat/ship-hurt-state': estat HURT a la nau
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user