// 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" namespace Systems::Collision { namespace { constexpr uint8_t NO_SHOOTER = 0xFF; // Lookup tabla puntos / color por tipo de enemy (mantiene la lógica // anterior pero centralizada para reutilizar entre paths). auto scoreForType(EnemyType type) -> int { switch (type) { case EnemyType::PENTAGON: return Defaults::Enemies::Scoring::PENTAGON_SCORE; case EnemyType::QUADRAT: return Defaults::Enemies::Scoring::QUADRAT_SCORE; case EnemyType::MOLINILLO: return Defaults::Enemies::Scoring::MOLINILLO_SCORE; } return 0; } auto colorForType(EnemyType type) -> SDL_Color { switch (type) { case EnemyType::PENTAGON: return Defaults::Palette::PENTAGON; case EnemyType::QUADRAT: return Defaults::Palette::QUADRAT; case EnemyType::MOLINILLO: return Defaults::Palette::MOLINILLO; } return SDL_Color{}; } // Mata al enemy con explosión: floating score, debris con velocity heredada, // sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador. // CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity). void explodeNow(Context& ctx, Enemy& enemy, uint8_t shooter_id) { const Vec2 ENEMY_POS = enemy.getCenter(); const Vec2 ENEMY_VEL = enemy.getVelocityVector(); const float BRIGHTNESS = enemy.getBrightness(); const auto SHAPE = enemy.getShape(); const EnemyType TYPE = enemy.getType(); const int POINTS = scoreForType(TYPE); const SDL_Color COLOR = colorForType(TYPE); if (shooter_id != NO_SHOOTER) { ctx.score_per_player[shooter_id] += POINTS; } ctx.floating_score_manager.crear(POINTS, ENEMY_POS); enemy.destruir(); constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suave) const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE; ctx.debris_manager.explode( SHAPE, ENEMY_POS, 0.0F, // angle (rotación interna del enemy) 1.0F, // escala VELOCITAT_EXPLOSIO, BRIGHTNESS, INHERITED_VEL, 0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc 0.0F, // sin herencia visual Defaults::Sound::EXPLOSION, COLOR, Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_FRICTION, Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); // Firework burst radial des del centro de l'enemic (efecte adicional al debris). // Línia blanca + halo daurat (WOUNDED) per a feel d'espurnes. ctx.firework_manager.spawn(ENEMY_POS, Defaults::FX::Firework::DEFAULT_COLOR, Defaults::FX::Firework::SPEED, Defaults::FX::Firework::N_POINTS, Defaults::FX::Firework::INITIAL_BRIGHTNESS, /*glow=*/true, Defaults::Palette::WOUNDED); } // 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::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, enemy, AMPLIFIER)) { continue; } // *** COLISIÓN bullet → enemy *** // Empuje físico cuasi-realista: el impulse és el moment de la bala // (m·v) multiplicat pel factor de transferència. Direcció = vector // velocity de la bala (cap a on viatjava). const Vec2 IMPULSE = bullet.getBody().velocity * (bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR); enemy.applyImpulse(IMPULSE); const uint8_t SHOOTER = bullet.getOwnerId(); if (enemy.isWounded()) { // Segundo impacto sobre enemy ya herido → muerte instantánea, // puntos al nuevo shooter. explodeNow(ctx, enemy, SHOOTER); } else { // Primer impacto → entra en estado herido (explosión diferida). enemy.herir(SHOOTER); } breakBullet(ctx.debris_manager, bullet); break; // Una bala impacta a un enemy y muere } } } void processWoundedDeaths(Context& ctx) { for (auto& enemy : ctx.enemies) { if (!enemy.woundExpiredThisFrame()) { continue; } enemy.consumeWoundExpired(); explodeNow(ctx, enemy, 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, 1.0F)) { continue; } // El sano queda herido, propagando el shooter original. if (A_WOUNDED) { b.herir(a.getLastHitBy()); } else { a.herir(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 Vec2 IMPULSE = SHIP_VEL * (Defaults::Ship::MASS * Defaults::Physics::Ship::DEATH_IMPACT_MOMENTUM_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].herir(); } } 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.jugador1_actiu : ctx.match_config.jugador2_actiu; if (!JUGADOR_ACTIU) { continue; } if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, 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 * Defaults::Physics::Bullet::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_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