diff --git a/source/core/defaults/physics.hpp b/source/core/defaults/physics.hpp index d448694..5b0977d 100644 --- a/source/core/defaults/physics.hpp +++ b/source/core/defaults/physics.hpp @@ -43,6 +43,13 @@ namespace Defaults::Physics::Debris { // 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua. constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F; + // Velocitat de la bala traspassada a cada fragment de debris al moment + // de l'impacte. Separat de la inèrcia del cos (velocitat_objecte): permet + // que els trossos volin "amb la força de la bala" encara que el cos pesi + // molt i amb prou feines es mogui. 0.4 a 700 px/s = ~280 px/s extra per + // fragment, molt visible sense ser excessiu. + constexpr float BULLET_IMPULSE_FACTOR = 0.4F; + // Tuneig específic de l'explosió d'enemic (overrides als defaults // que es passen com a paràmetres opcionals a explode()). constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més diff --git a/source/game/effects/debris_manager.cpp b/source/game/effects/debris_manager.cpp index 2a5b900..946762e 100644 --- a/source/game/effects/debris_manager.cpp +++ b/source/game/effects/debris_manager.cpp @@ -57,7 +57,8 @@ namespace Effects { SDL_Color color, float lifetime, float friction, - int segment_multiplier) { + int segment_multiplier, + const Vec2& bullet_impulse_velocity) { if (!shape || !shape->isValid()) { return; } @@ -84,7 +85,7 @@ namespace Effects { Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale); // Si el pool es ple, no té sentit continuar amb la resta de segments - if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction)) { + if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction, bullet_impulse_velocity)) { return; } } @@ -110,7 +111,7 @@ namespace Effects { return segments; } - auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool { + auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity) -> bool { Debris* debris = findFreeSlot(); if (debris == nullptr) { std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n"; @@ -131,13 +132,16 @@ namespace Effects { // Direcció radial (desde el centro hacia el segment) Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro); - // Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte) + // Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte + + // velocitat de la bala escalada per BULLET_IMPULSE_FACTOR). float speed = velocitat_base + (((std::rand() / static_cast(RAND_MAX)) * 2.0F - 1.0F) * Defaults::Physics::Debris::VARIACIO_SPEED); - debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x; - debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y; + debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x + + (bullet_impulse_velocity.x * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR); + debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y + + (bullet_impulse_velocity.y * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR); debris->acceleration = friction; // Rotación de trayectoria (con conversió a tangencial si excedeix cap) diff --git a/source/game/effects/debris_manager.hpp b/source/game/effects/debris_manager.hpp index f5b9c97..c58bc2f 100644 --- a/source/game/effects/debris_manager.hpp +++ b/source/game/effects/debris_manager.hpp @@ -47,6 +47,11 @@ namespace Effects { // - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s) // - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60) // - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar) + // - bullet_impulse_velocity: velocitat de la bala que ha causat l'impacte (px/s, + // per defecte 0). S'aplica a cada fragment escalada per + // Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR, independent de + // velocitat_objecte. Permet que els trossos "salten amb la força de la bala" + // encara que el cos sigui pesat i amb prou feines es mogui. void explode(const std::shared_ptr& shape, const Vec2& centro, float angle, @@ -60,7 +65,8 @@ namespace Effects { SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global float lifetime = Defaults::Physics::Debris::TEMPS_VIDA, float friction = Defaults::Physics::Debris::ACCELERACIO, - int segment_multiplier = 1); + int segment_multiplier = 1, + const Vec2& bullet_impulse_velocity = {.x = 0.0F, .y = 0.0F}); // Actualitzar todos los fragments active void update(float delta_time); @@ -97,7 +103,7 @@ namespace Effects { -> std::vector>; // Inicialitza un debris en un slot lliure i el deixa actiu. Retorna // false si el pool está ple (la cridadora ha d'aturar el bucle). - auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool; + auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity) -> bool; static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular); static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual); }; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 7aab904..d3bcfd5 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -580,7 +580,7 @@ auto GameScene::buildCollisionContext() -> Systems::Collision::Context { .firework_manager = firework_manager_, .floating_score_manager = floating_score_manager_, .match_config = match_config_, - .on_player_hit = [this](uint8_t pid) { tocado(pid); }, + .on_player_hit = [this](uint8_t pid, const Vec2& bv) { tocado(pid, bv); }, }; } @@ -764,7 +764,7 @@ void GameScene::drawLevelCompletedState() { drawScoreboard(); } -void GameScene::tocado(uint8_t player_id) { +void GameScene::tocado(uint8_t player_id, const Vec2& bullet_velocity) { // Death sequence: 3 phases // Phase 1: First call (hit_timer_per_player_[player_id] == 0) - trigger explosion // Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation @@ -788,6 +788,9 @@ void GameScene::tocado(uint8_t player_id) { // Mateixa dispersió i efecte que els debris d'enemic (lifetime, // friction, segment_multiplier alineats); només canvien sound i color. + // bullet_velocity arriba a explode() com a impuls extra independent + // de la inèrcia del cos del ship — els trossos volen amb la força + // de la bala encara que el ship estiga quiet. debris_manager_.explode( ships_[player_id].getShape(), SHIP_POS, @@ -802,7 +805,8 @@ void GameScene::tocado(uint8_t player_id) { ships_[player_id].getConfig().colors.normal, Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_FRICTION, - Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); + Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER, + bullet_velocity); // Start death timer (non-zero to avoid re-triggering) hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH; diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 44b8c85..4c9d413 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -101,7 +101,10 @@ class GameScene final : public Scene { bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo // Funciones privades - void tocado(uint8_t player_id); + // bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no + // ve d'una bala). Es passa al debris perquè els fragments volin en direcció + // de la bala (independent de la inèrcia del cos del ship). + void tocado(uint8_t player_id, const Vec2& bullet_velocity = {.x = 0.0F, .y = 0.0F}); void drawScoreboard(); // Dibuixar marcador de puntuación void fireBullet(uint8_t player_id); // Shoot bullet from player [[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index ea3d4d3..e7a32e5 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -150,7 +150,9 @@ namespace Systems::Collision { 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); + // 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). @@ -197,13 +199,12 @@ namespace Systems::Collision { } // *** 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); + // 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); @@ -237,15 +238,20 @@ namespace Systems::Collision { } // *** BALA D'ENEMIC → SHIP *** - // Apliquem l'impuls de la bala abans del hurt/death perquè la - // velocitat de la nau quedi capturada per als debris. - const Vec2 IMPULSE = bullet.getBody().velocity * - (bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor); - ctx.ships[player_id].getBody().applyImpulse(IMPULSE); + // 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); + 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); diff --git a/source/game/systems/collision_system.hpp b/source/game/systems/collision_system.hpp index e2cd644..9e28de6 100644 --- a/source/game/systems/collision_system.hpp +++ b/source/game/systems/collision_system.hpp @@ -40,8 +40,11 @@ namespace Systems::Collision { Effects::FireworkManager& firework_manager; Effects::FloatingScoreManager& floating_score_manager; const GameConfig::MatchConfig& match_config; - // Trigger de muerte del jugador (GameScene::tocado). - std::function on_player_hit; + // Trigger de muerte del jugador (GameScene::tocado). bullet_velocity es + // la velocitat de la bala que ha causat la mort (Vec2{} si la mort no + // ve d'una bala — col·lisió ship-enemy, etc.). Es passa al debris perquè + // els trossos volin en direcció de la bala. + std::function on_player_hit; }; // Detecta colisiones bullet → enemy. Si hit: diff --git a/source/game/systems/enemy_event_dispatcher.cpp b/source/game/systems/enemy_event_dispatcher.cpp index 21f37ef..e7c0f81 100644 --- a/source/game/systems/enemy_event_dispatcher.cpp +++ b/source/game/systems/enemy_event_dispatcher.cpp @@ -24,10 +24,11 @@ namespace Systems::EnemyEvents { ctx.floating_score_manager.crear(POINTS, enemy.getCenter()); } - void doCreateDebris(Systems::Collision::Context& ctx, const Enemy& enemy) { + void doCreateDebris(Systems::Collision::Context& ctx, const Enemy& enemy, const Bullet* bullet) { constexpr float SPEED_EXPLOSIO = 80.0F; const Vec2 INHERITED_VEL = enemy.getVelocityVector() * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE; + const Vec2 BULLET_VEL = (bullet != nullptr) ? bullet->getBody().velocity : Vec2{}; ctx.debris_manager.explode( enemy.getShape(), enemy.getCenter(), @@ -42,7 +43,8 @@ namespace Systems::EnemyEvents { enemy.getConfig().colors.normal, Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_FRICTION, - Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); + Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER, + BULLET_VEL); } void doCreateFireworks(Systems::Collision::Context& ctx, const Enemy& enemy) { @@ -67,6 +69,24 @@ namespace Systems::EnemyEvents { void dispatchEvent(Systems::Collision::Context& ctx, Enemy& enemy, EnemyEventType event, uint8_t shooter_id, const Bullet* bullet) { const auto& actions = enemy.getConfig().events.getActions(event); + + // Pre-scan: aquest event matarà l'enemic? Si sí, l'impuls de la bala + // va directament als debris (via doCreateDebris) i NO s'aplica al cos + // — així evitem el "double-count" on els trossos hereten la velocitat + // del cos (boostat per la bala) I a més el seu propi impuls de bala. + // Regla: el bullet impacta al cos O als trossos, mai a tots dos. + bool will_die = false; + for (const auto& action : actions) { + if (action.type == EnemyActionType::DESTROY) { + will_die = true; + break; + } + if (action.type == EnemyActionType::SET_HURT && enemy.isWounded()) { + will_die = true; + break; + } + } + for (const auto& action : actions) { switch (action.type) { case EnemyActionType::SET_HURT: @@ -86,13 +106,15 @@ namespace Systems::EnemyEvents { doAddScore(ctx, enemy, shooter_id); break; case EnemyActionType::CREATE_DEBRIS: - doCreateDebris(ctx, enemy); + doCreateDebris(ctx, enemy, bullet); break; case EnemyActionType::CREATE_FIREWORKS: doCreateFireworks(ctx, enemy); break; case EnemyActionType::APPLY_IMPULSE: - doApplyImpulse(enemy, bullet); + if (!will_die) { + doApplyImpulse(enemy, bullet); + } break; } }