diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index 89dea0c..a98a675 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -26,7 +26,6 @@ namespace Defaults::Game { // Friendly fire system constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) - constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s) constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS // Transición LEVEL_START (mensajes aleatorios PRE-level) diff --git a/source/core/physics/collision.hpp b/source/core/physics/collision.hpp index 9065dbe..9f5adea 100644 --- a/source/core/physics/collision.hpp +++ b/source/core/physics/collision.hpp @@ -3,30 +3,62 @@ #pragma once +#include + #include "core/entities/entity.hpp" #include "core/types.hpp" namespace Physics { -// Comprobación genèrica de colisión entre dues entidades -inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool { - // Comprovar si ambdós són col·lisionables - if (!a.isCollidable() || !b.isCollidable()) { - return false; + // Comprobación genèrica de colisión entre dues entidades + inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool { + // Comprovar si ambdós són col·lisionables + if (!a.isCollidable() || !b.isCollidable()) { + return false; + } + + // Calcular radi combinat (con amplificador per hitbox generós) + float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier; + float suma_radis_sq = suma_radis * suma_radis; + + // Comprobación distancia al cuadrado (sin sqrt) + const Vec2& pos_a = a.getCenter(); + const Vec2& pos_b = b.getCenter(); + float dx = pos_a.x - pos_b.x; + float dy = pos_a.y - pos_b.y; + float dist_sq = (dx * dx) + (dy * dy); + + return dist_sq <= suma_radis_sq; } - // Calcular radi combinat (con amplificador per hitbox generós) - float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier; - float suma_radis_sq = suma_radis * suma_radis; - - // Comprobación distancia al cuadrado (sin sqrt) - const Vec2& pos_a = a.getCenter(); - const Vec2& pos_b = b.getCenter(); - float dx = pos_a.x - pos_b.x; - float dy = pos_a.y - pos_b.y; - float dist_sq = (dx * dx) + (dy * dy); - - return dist_sq <= suma_radis_sq; -} + // Swept collision: una entitat mòbil (radi r_a) s'ha desplaçat de p0 a p1 aquest + // frame. Comprova si el segment expandit pel radi conjunt (r_a + radi de b, amb + // amplificador) toca el cercle de l'entity b. Equival al check discrete quan + // p0 == p1 (sense moviment). Evita tunneling a velocitats altes. + inline auto checkCollisionSwept(const Vec2& p0, const Vec2& p1, float r_a, const Entities::Entity& b, float amplifier = 1.0F) -> bool { + if (!b.isCollidable()) { + return false; + } + const float SUM_R = (r_a + b.getCollisionRadius()) * amplifier; + const float SUM_R_SQ = SUM_R * SUM_R; + const Vec2& center_b = b.getCenter(); + const float DX_SEG = p1.x - p0.x; + const float DY_SEG = p1.y - p0.y; + const float LEN_SQ = (DX_SEG * DX_SEG) + (DY_SEG * DY_SEG); + // Degenerat: punt-cercle (frame de spawn, o entitat parada). + if (LEN_SQ <= 0.0F) { + const float DX = p0.x - center_b.x; + const float DY = p0.y - center_b.y; + return ((DX * DX) + (DY * DY)) <= SUM_R_SQ; + } + // Projecció del centre sobre la recta del segment, clamp a [0,1] per acotar al segment. + const float T_RAW = (((center_b.x - p0.x) * DX_SEG) + ((center_b.y - p0.y) * DY_SEG)) / LEN_SQ; + const float T_CLAMPED = std::clamp(T_RAW, 0.0F, 1.0F); + const float CLOSEST_X = p0.x + (DX_SEG * T_CLAMPED); + const float CLOSEST_Y = p0.y + (DY_SEG * T_CLAMPED); + const float DX = CLOSEST_X - center_b.x; + const float DY = CLOSEST_Y - center_b.y; + return ((DX * DX) + (DY * DY)) <= SUM_R_SQ; + } } // namespace Physics diff --git a/source/game/entities/bullet.cpp b/source/game/entities/bullet.cpp index 32186b4..dc77574 100644 --- a/source/game/entities/bullet.cpp +++ b/source/game/entities/bullet.cpp @@ -3,7 +3,6 @@ #include "game/entities/bullet.hpp" -#include #include #include #include @@ -43,8 +42,8 @@ void Bullet::init() { // Inicialment inactiva is_active_ = false; center_ = {.x = 0.0F, .y = 0.0F}; + prev_position_ = {.x = 0.0F, .y = 0.0F}; angle_ = 0.0F; - grace_timer_ = 0.0F; // Reset del cuerpo físico body_.position = Vec2{}; @@ -61,11 +60,9 @@ void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) { // Almacenar propietario (0=P1, 1=P2) owner_id_ = owner_id; - // Activar grace period (prevents instant self-collision) - grace_timer_ = Defaults::Game::BULLET_GRACE_PERIOD; - // Posición y orientación iniciales = ship center_ = position; + prev_position_ = position; // Al spawn no hi ha moviment encara: swept degenera a punt-cercle angle_ = angle; // Sincronizar el body físico: posición + velocidad cartesiana @@ -82,37 +79,18 @@ void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) { Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME); } -void Bullet::update(float delta_time) { - if (!is_active_) { - return; - } - - // Decrementar grace timer - if (grace_timer_ > 0.0F) { - grace_timer_ -= delta_time; - grace_timer_ = std::max(grace_timer_, 0.0F); - } - - // El movimiento real lo hace PhysicsWorld::update() (integración). - // Aquí solo lógica de estado: detectar salida del PLAYAREA y desactivar. - float min_x; - float max_x; - float min_y; - float max_y; - Constants::getSafePlayAreaBounds(Defaults::Entities::BULLET_RADIUS, - min_x, - max_x, - min_y, - max_y); - - if (body_.position.x < min_x || body_.position.x > max_x || - body_.position.y < min_y || body_.position.y > max_y) { - desactivar(); - } +void Bullet::update(float /*delta_time*/) { + // No-op: la desactivació per fora-de-zona viu a + // Systems::Collision::desactivateOutOfBoundsBullets() perquè així té accés + // al DebrisManager i pot generar el "trencament" visual de la bala alhora. + // El moviment l'integra PhysicsWorld; postUpdate sincronitza center_ i prev_position_. } void Bullet::postUpdate(float /*delta_time*/) { - // Sincronizar mirror desde body_ tras la integración del world. + // Captura la posició al final del frame anterior abans de sobreescriure center_; + // així el sistema de col·lisions pot fer swept (segment-vs-cercle) entre prev_position_ + // i la nova center_, evitant tunneling a velocitats altes. + prev_position_ = center_; center_ = body_.position; // angle_ no cambia (las balas no rotan visualmente). } diff --git a/source/game/entities/bullet.hpp b/source/game/entities/bullet.hpp index 54bd204..7d9c8be 100644 --- a/source/game/entities/bullet.hpp +++ b/source/game/entities/bullet.hpp @@ -11,38 +11,39 @@ #include "core/types.hpp" class Bullet : public Entities::Entity { - public: - Bullet() - : Entity(nullptr) {} - explicit Bullet(Rendering::Renderer* renderer); + public: + Bullet() + : Entity(nullptr) {} + explicit Bullet(Rendering::Renderer* renderer); - void init() override; - void disparar(const Vec2& position, float angle, uint8_t owner_id); - void update(float delta_time) override; - void postUpdate(float delta_time) override; - void draw() const override; + void init() override; + void disparar(const Vec2& position, float angle, uint8_t owner_id); + void update(float delta_time) override; + void postUpdate(float delta_time) override; + void draw() const override; - // Override: Interfaz de Entity - [[nodiscard]] auto isActive() const -> bool override { return is_active_; } + // Override: Interfaz de Entity + [[nodiscard]] auto isActive() const -> bool override { return is_active_; } - // Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check) - [[nodiscard]] auto getCollisionRadius() const -> float override { - return Defaults::Entities::BULLET_RADIUS; - } - [[nodiscard]] auto isCollidable() const -> bool override { - return is_active_ && grace_timer_ <= 0.0F; - } + // Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check) + [[nodiscard]] auto getCollisionRadius() const -> float override { + return Defaults::Entities::BULLET_RADIUS; + } + [[nodiscard]] auto isCollidable() const -> bool override { + return is_active_; + } - // Getters (API pública sin cambios) - [[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; } - [[nodiscard]] auto getGraceTimer() const -> float { return grace_timer_; } - void desactivar(); + // Getters (API pública sin cambios) + [[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; } + // Posició al final del frame anterior, per a CCD segment-vs-cercle. + [[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; } + void desactivar(); - private: - // Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_). - // Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer - // dejen el objeto en estado coherente (proyectil inactivo, sin owner, sin grace timer). - bool is_active_{false}; - uint8_t owner_id_{0}; // 0=P1, 1=P2 - float grace_timer_{0.0F}; // Grace period timer (0.0 = vulnerable) + private: + // Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_). + // Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer + // dejen el objeto en estado coherente (proyectil inactivo, sin owner). + bool is_active_{false}; + uint8_t owner_id_{0}; // 0=P1, 1=P2 + Vec2 prev_position_{}; // Posició al final del frame anterior (per a swept collision) }; diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index 197406a..b739635 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -8,7 +8,6 @@ #include #include -#include "core/audio/audio.hpp" #include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/graphics/shape_loader.hpp" @@ -277,7 +276,8 @@ void Enemy::destruir() { void Enemy::herir(uint8_t shooter_id) { wounded_timer_ = Defaults::Enemies::Wounded::DURATION; last_hit_by_ = shooter_id; - Audio::get()->playSound(Defaults::Sound::HIT, Audio::Group::GAME); + // El so HIT ara el reprodueix la bala quan es trenca en debris + // (Systems::Collision::breakBullet), no l'enemic en entrar a HURT. } void Enemy::applyImpulse(const Vec2& impulse) { diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index f76f6df..b7b35ca 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -296,6 +296,7 @@ auto GameScene::stepContinueScreen(float delta_time) -> bool { for (auto& bullet : bullets_) { bullet.update(delta_time); } + Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_); debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); @@ -321,6 +322,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool { for (auto& bullet : bullets_) { bullet.update(delta_time); } + Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_); debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); @@ -438,6 +440,7 @@ void GameScene::runStageLevelStart(float delta_time) { for (auto& bullet : bullets_) { bullet.update(delta_time); } + Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_); debris_manager_.update(delta_time); firework_manager_.update(delta_time); } @@ -465,11 +468,15 @@ void GameScene::runStagePlaying(float delta_time) { for (auto& enemy : enemies_) { enemy.update(delta_time); } + + // Col·lisions primer, després desactivació per fora-de-zona: així una bala que + // el mateix frame xoca amb un enemic i alhora surt del PLAYAREA es compta com a + // impacte abans no se la trenqui per sortir. + runCollisionDetections(); for (auto& bullet : bullets_) { bullet.update(delta_time); } - - runCollisionDetections(); + Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_); debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); @@ -487,6 +494,7 @@ void GameScene::runStageLevelCompleted(float delta_time) { for (auto& bullet : bullets_) { bullet.update(delta_time); } + Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_); debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index 3c66485..bb02518 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -7,6 +7,7 @@ #include "core/audio/audio.hpp" #include "core/physics/collision.hpp" #include "core/types.hpp" +#include "game/constants.hpp" namespace Systems::Collision { @@ -81,14 +82,40 @@ namespace Systems::Collision { // No heretem color: el burst usa el blanc per defecte per a un feel més lluminós. ctx.firework_manager.spawn(ENEMY_POS); } + + // Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva. + // S'invoca des de qualsevol desactivació de bala (impacte amb enemic, amb jugador, + // o sortida del PLAYAREA) per a un feedback visual i sonor consistent. + void breakBullet(Effects::DebrisManager& debris_manager, Bullet& bullet) { + constexpr float DEBRIS_VELOCITY = 60.0F; + debris_manager.explode( + bullet.getShape(), + bullet.getCenter(), + bullet.getAngle(), + 1.0F, // scale + DEBRIS_VELOCITY, + bullet.getBrightness(), + Vec2{}, // sense herència de velocitat (fragments radials) + 0.0F, // sense velocity angular heretada + 0.0F, // sense rotació visual heretada + Defaults::Sound::HIT, + Defaults::Palette::BULLET, + Defaults::Physics::Debris::TEMPS_VIDA, + Defaults::Physics::Debris::ACCELERACIO, + 1); // sense duplicat de segments + bullet.desactivar(); + } } // anonymous namespace void detectBulletEnemy(Context& ctx) { constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; for (auto& bullet : ctx.bullets) { + if (!bullet.isActive()) { + continue; + } for (auto& enemy : ctx.enemies) { - if (!Physics::checkCollision(bullet, enemy, AMPLIFIER)) { + if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, enemy, AMPLIFIER)) { continue; } @@ -111,7 +138,7 @@ namespace Systems::Collision { enemy.herir(SHOOTER); } - bullet.desactivar(); + breakBullet(ctx.debris_manager, bullet); break; // Una bala impacta a un enemy y muere } } @@ -207,12 +234,17 @@ namespace Systems::Collision { constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; for (auto& bullet : ctx.bullets) { - if (!bullet.isActive() || bullet.getGraceTimer() > 0.0F) { + if (!bullet.isActive()) { continue; } const uint8_t BULLET_OWNER = bullet.getOwnerId(); for (uint8_t player_id = 0; player_id < 2; player_id++) { + // Una bala mai no impacta al seu propi shooter: les bales d'aquest joc no + // reboten ni el shooter pot atrapar-les, així que la prevenció és per disseny. + if (BULLET_OWNER == player_id) { + continue; + } if (ctx.hit_timer_per_player[player_id] > 0.0F || !ctx.ships[player_id].isActive() || ctx.ships[player_id].isInvulnerable()) { @@ -225,22 +257,16 @@ namespace Systems::Collision { continue; } - if (!Physics::checkCollision(bullet, ctx.ships[player_id], AMPLIFIER)) { + if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, ctx.ships[player_id], AMPLIFIER)) { continue; } - // *** FRIENDLY FIRE HIT *** - if (BULLET_OWNER == player_id) { - // Self-hit: víctima pierde 1 vida. - ctx.on_player_hit(player_id); - } else { - // Teammate hit: víctima pierde 1, atacante gana 1. - ctx.on_player_hit(player_id); - ctx.lives_per_player[BULLET_OWNER]++; - } - + // *** TEAMMATE HIT (friendly fire) *** + // Víctima perd 1 vida, atacant en guanya 1. + ctx.on_player_hit(player_id); + ctx.lives_per_player[BULLET_OWNER]++; Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); - bullet.desactivar(); + breakBullet(ctx.debris_manager, bullet); break; // Una bullet solo impacta una vez por frame } } @@ -254,4 +280,26 @@ namespace Systems::Collision { detectBulletPlayer(ctx); } + void desactivateOutOfBoundsBullets( + std::array(Defaults::Entities::MAX_BALES) * 2>& bullets, + Effects::DebrisManager& debris_manager) { + float min_x; + float max_x; + float min_y; + float max_y; + Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y); + constexpr float R = Defaults::Entities::BULLET_RADIUS; + + for (auto& bullet : bullets) { + if (!bullet.isActive()) { + continue; + } + const Vec2& pos = bullet.getCenter(); + if (pos.x < min_x + R || pos.x > max_x - R || + pos.y < min_y + R || pos.y > max_y - R) { + breakBullet(debris_manager, bullet); + } + } + } + } // namespace Systems::Collision diff --git a/source/game/systems/collision_system.hpp b/source/game/systems/collision_system.hpp index 5836a52..49f8647 100644 --- a/source/game/systems/collision_system.hpp +++ b/source/game/systems/collision_system.hpp @@ -70,4 +70,11 @@ namespace Systems::Collision { // Las tres en orden lógico del frame. void detectAll(Context& ctx); + // Desactiva les bales que han sortit del PLAYAREA, generant debris visual + // (8 fragments de l'octàgon) i el so HIT. Cal cridar-la després de detectAll() + // perquè una bala que el mateix frame xoca i alhora surt es comptabilitzi com a impacte. + void desactivateOutOfBoundsBullets( + std::array(Defaults::Entities::MAX_BALES) * 2>& bullets, + Effects::DebrisManager& debris_manager); + } // namespace Systems::Collision