// collision_system.cpp - Implementación del sistema de colisiones #include "game/systems/collision_system.hpp" #include #include "core/audio/audio.hpp" #include "core/physics/collision.hpp" #include "core/types.hpp" #include "game/constants.hpp" #include "game/entities/bullet_config.hpp" #include "game/systems/enemy_event_dispatcher.hpp" namespace Systems::Collision { namespace { // 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, bullet.getConfig().colors.normal, 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::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) { continue; } // *** COLISIÓN bullet → enemy *** // La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu // al YAML de l'enemic via la secció `events:`. Aquí només dispatchem. Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, bullet.getOwnerId(), &bullet); breakBullet(ctx.debris_manager, bullet); break; } } } void processWoundedDeaths(Context& ctx) { for (auto& enemy : ctx.enemies) { if (!enemy.woundExpiredThisFrame()) { continue; } enemy.consumeWoundExpired(); Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HURT_END, enemy.getLastHitBy()); } } void detectWoundedChain(Context& ctx) { const std::size_t N = ctx.enemies.size(); for (std::size_t i = 0; i < N; i++) { Enemy& a = ctx.enemies[i]; if (!a.isCollidable()) { continue; } for (std::size_t j = i + 1; j < N; j++) { Enemy& b = ctx.enemies[j]; if (!b.isCollidable()) { continue; } const bool A_WOUNDED = a.isWounded(); const bool B_WOUNDED = b.isWounded(); if (A_WOUNDED == B_WOUNDED) { continue; // ambos sanos o ambos heridos: nada que propagar } if (!Physics::checkCollision(a, b, Defaults::Game::COLLISION_WOUNDED_CHAIN_AMPLIFIER)) { continue; } // El sano queda herido, propagando el shooter original. if (A_WOUNDED) { b.hurt(a.getLastHitBy()); } else { a.hurt(b.getLastHitBy()); } } } } void detectShipEnemy(Context& ctx) { constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; for (uint8_t i = 0; i < 2; i++) { // 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. Enemy* touched_enemy = nullptr; for (auto& enemy : ctx.enemies) { if (enemy.isInvulnerable()) { continue; } if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) { touched_enemy = &enemy; break; } } const bool TOUCHING_NOW = touched_enemy != nullptr; // 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. Aplica un impuls afegit // perquè l'enemic surti disparat (feedback visible). const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector(); const float DEATH_FACTOR = ctx.ships[i].getConfig().physics.death_impact_factor; const Vec2 IMPULSE = SHIP_VEL * (ctx.ships[i].getBody().mass * DEATH_FACTOR); touched_enemy->applyImpulse(IMPULSE); 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].hurt(); } } ctx.ships[i].setTouchingEnemyPrevFrame(TOUCHING_NOW); } } void detectBulletPlayer(Context& ctx) { if (!Defaults::Game::FRIENDLY_FIRE_ENABLED) { return; } constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; for (auto& bullet : ctx.bullets) { 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()) { continue; } const bool JUGADOR_ACTIU = (player_id == 0) ? ctx.match_config.player1_active : ctx.match_config.player2_active; if (!JUGADOR_ACTIU) { continue; } if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), ctx.ships[player_id], AMPLIFIER)) { continue; } // *** TEAMMATE HIT (friendly fire) *** // Víctima perd 1 vida, atacant en guanya 1. Apliquem l'impuls // de la bala a la nau ABANS de on_player_hit perquè tocado() // captura la velocitat per als debris (si no, queden quiets). const Vec2 BULLET_IMPULSE = bullet.getBody().velocity * (bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor); ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE); ctx.on_player_hit(player_id); ctx.lives_per_player[BULLET_OWNER]++; Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); breakBullet(ctx.debris_manager, bullet); break; // Una bullet solo impacta una vez por frame } } } void detectAll(Context& ctx) { processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame detectBulletEnemy(ctx); detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo detectShipEnemy(ctx); detectBulletPlayer(ctx); } void desactivateOutOfBoundsBullets( std::array(Defaults::Entities::MAX_BULLETS) * 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); for (auto& bullet : bullets) { if (!bullet.isActive()) { continue; } const float R = bullet.getCollisionRadius(); 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