// 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" 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). // No heretem color: el burst usa el blanc per defecte per a un feel més lluminós. ctx.firework_manager.spawn(ENEMY_POS); } } // anonymous namespace void detectBulletEnemy(Context& ctx) { constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; for (auto& bullet : ctx.bullets) { for (auto& enemy : ctx.enemies) { if (!Physics::checkCollision(bullet, 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); } bullet.desactivar(); 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 if (ctx.hit_timer_per_player[i] > 0.0F || !ctx.ships[i].isActive() || ctx.ships[i].isInvulnerable()) { continue; } 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 } } } } 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() || bullet.getGraceTimer() > 0.0F) { continue; } const uint8_t BULLET_OWNER = bullet.getOwnerId(); for (uint8_t player_id = 0; player_id < 2; player_id++) { 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::checkCollision(bullet, 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]++; } Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); bullet.desactivar(); 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); } } // namespace Systems::Collision