// 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; constexpr uint8_t NO_SHOOTER = 0xFF; constexpr uint8_t ENEMY_BASE = Defaults::Entities::ENEMY_OWNER_BASE; for (auto& bullet : ctx.bullets) { if (!bullet.isActive()) { continue; } const uint8_t BULLET_OWNER = bullet.getOwnerId(); const bool IS_ENEMY_BULLET = BULLET_OWNER >= ENEMY_BASE; for (std::size_t i = 0; i < ctx.enemies.size(); ++i) { Enemy& enemy = ctx.enemies[i]; // Self-shot: una bala d'enemic mai no impacta el seu propi creador. if (IS_ENEMY_BULLET && BULLET_OWNER == static_cast(ENEMY_BASE + i)) { continue; } 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:`. Si la bala és d'enemic, // no atribuïm el kill a cap player (NO_SHOOTER). const uint8_t SHOOTER = IS_ENEMY_BULLET ? NO_SHOOTER : BULLET_OWNER; Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, SHOOTER, &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 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); // Sense bala: cap impuls de bala per als debris (mort per // col·lisió cos-cos). Els debris hereten la inèrcia del ship. ctx.on_player_hit(i, Vec2{}); } 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. Friendly fire sempre // mata: el bullet va als debris (via tocado) i NO al cos del ship // — el cos està a punt de desactivar-se, qualsevol impuls seria // double-count amb la velocitat que ja reben els trossos. const Vec2 BULLET_VEL = bullet.getBody().velocity; ctx.on_player_hit(player_id, BULLET_VEL); 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 detectEnemyBulletShip(Context& ctx) { constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; constexpr uint8_t ENEMY_BASE = Defaults::Entities::ENEMY_OWNER_BASE; for (auto& bullet : ctx.bullets) { if (!bullet.isActive() || bullet.getOwnerId() < ENEMY_BASE) { continue; } 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.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; } // *** BALA D'ENEMIC → SHIP *** // Regla "cos XOR trossos": l'impuls de la bala s'aplica al cos // només si el ship sobreviu (fereix). Si el ship mor, el bullet // va directament als trossos (via tocado) i el cos no rep impuls // — els trossos ja porten la força de la bala, qualsevol impuls // afegit al cos seria double-count. const Vec2 BULLET_VEL = bullet.getBody().velocity; if (ctx.ships[player_id].isHurt()) { // Segon impacte durant HURT → mort. ctx.on_player_hit(player_id, BULLET_VEL); } else { // Fereix: el cos sobreviu, rep l'impuls. No hi ha debris encara. const Vec2 IMPULSE = BULLET_VEL * (bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor); ctx.ships[player_id].getBody().applyImpulse(IMPULSE); ctx.ships[player_id].hurt(); } breakBullet(ctx.debris_manager, bullet); break; // una bala impacta una vegada per frame } } } void detectAll(Context& ctx) { processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame detectBulletEnemy(ctx); // Wounded chain desactivat: era massa fàcil que un enemic ferit topés // amb el big_pentagon (10 HP) i el matés instantàniament. La regla // "ferit-toca-sa → ferit" queda permanentment fora. detectShipEnemy(ctx); detectBulletPlayer(ctx); detectEnemyBulletShip(ctx); } void desactivateOutOfBoundsBullets( std::array(Defaults::Entities::MAX_BULLETS_TOTAL)>& 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