// enemy_event_dispatcher.cpp - Implementació del dispatcher d'events d'enemic // © 2026 JailDesigner #include "game/systems/enemy_event_dispatcher.hpp" #include #include #include #include "core/defaults.hpp" #include "core/types.hpp" #include "game/constants.hpp" #include "game/entities/bullet.hpp" #include "game/entities/bullet_config.hpp" #include "game/entities/bullet_registry.hpp" #include "game/entities/enemy_ai.hpp" #include "game/entities/enemy_config.hpp" #include "game/entities/ship.hpp" namespace Systems::EnemyEvents { namespace { constexpr uint8_t NO_SHOOTER = 0xFF; void doAddScore(Systems::Collision::Context& ctx, const Enemy& enemy, uint8_t shooter) { const int POINTS = enemy.getConfig().score; if (shooter != NO_SHOOTER) { ctx.score_per_player[shooter] += POINTS; } ctx.floating_score_manager.crear(POINTS, enemy.getCenter()); } // Helper compartit per CREATE_DEBRIS i CREATE_DEBRIS_PARTIAL: única // crida a explode(), paràmetres alineats; només canvien piece_scale // (1.0 explosió, 0.3 xip) i el color (cos vs hit-feedback). void spawnDebrisForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, const Bullet* bullet, float piece_scale, SDL_Color color) { 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(), 0.0F, 1.0F, SPEED_EXPLOSIO, enemy.getBrightness(), INHERITED_VEL, 0.0F, 0.0F, Defaults::Sound::ENEMY_EXPLOSION, color, Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_FRICTION, Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER, BULLET_VEL, piece_scale); } // Helper compartit per CREATE_FIREWORKS i CREATE_FIREWORKS_SMALL: // mateixa crida a spawn(); els callers decideixen line_color, glow_color, // n_points i initial_speed segons el "tamany" del burst (mort vs hit). void spawnFireworksForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, int n_points, float initial_speed, SDL_Color line_color, SDL_Color glow_color) { ctx.firework_manager.spawn(enemy.getCenter(), line_color, initial_speed, n_points, Defaults::FX::Firework::INITIAL_BRIGHTNESS, /*glow=*/true, glow_color); } void doApplyImpulse(Enemy& enemy, const Bullet* bullet) { if (bullet == nullptr) { return; } const Vec2 IMPULSE = bullet->getBody().velocity * (bullet->getBody().mass * bullet->getConfig().physics.impact_momentum_factor); enemy.applyImpulse(IMPULSE); } auto randFloat01() -> float { return static_cast(std::rand()) / static_cast(RAND_MAX); } // Còpia local de la mateixa primitiva que viu a enemy_ai_system.cpp. // No s'ha extret a un header compartit perquè és l'únic punt de // duplicació; si apareix un tercer consumidor, refactoritzar. auto findNearestShipPosition(const Enemy& enemy) -> const Vec2* { const Vec2& self = enemy.getCenter(); const Vec2* best = nullptr; float best_dist_sq = 0.0F; for (const Ship* ship : enemy.getShips()) { if (ship == nullptr || !ship->isActive()) { continue; } const Vec2& pos = ship->getCenter(); const Vec2 DELTA = pos - self; const float DIST_SQ = DELTA.lengthSquared(); if (best == nullptr || DIST_SQ < best_dist_sq) { best = &pos; best_dist_sq = DIST_SQ; } } return best; } // FIRE_BULLET: paral·lel a doShoot() d'enemy_ai_system.cpp, però disparat // per esdeveniment (típicament on_hit per a contra-atacs) en lloc de // periòdicament. owner_id es deriva de l'índex dins ctx.enemies via // aritmètica de punters (l'array és contigu). void doFireBullet(Systems::Collision::Context& ctx, const Enemy& enemy, const EnemyAction& action) { if (action.bullet_config_name.empty()) { return; } const BulletConfig* cfg = BulletRegistry::get(action.bullet_config_name); if (cfg == nullptr) { return; } Bullet* slot = nullptr; constexpr std::size_t START = Defaults::Entities::ENEMY_BULLET_START_IDX; constexpr std::size_t END = START + Defaults::Entities::MAX_ENEMY_BULLETS; for (std::size_t i = START; i < END; ++i) { if (!ctx.bullets[i].isActive()) { slot = &ctx.bullets[i]; break; } } if (slot == nullptr) { return; // pool d'enemic ple } float angle = 0.0F; if (action.aim_mode == AimMode::AIMED) { const Vec2* target = findNearestShipPosition(enemy); if (target == nullptr) { angle = randFloat01() * 2.0F * Constants::PI; } else { const Vec2 TO = *target - enemy.getCenter(); angle = std::atan2(TO.y, TO.x) + (Constants::PI / 2.0F); } } else { angle = randFloat01() * 2.0F * Constants::PI; } if (action.jitter_rad > 0.0F) { angle += (randFloat01() - 0.5F) * 2.0F * action.jitter_rad; } // Localitzem l'índex de l'enemic per construir l'owner_id. Evitem // aritmètica de punters sobre Enemy (tipus polimòrfic — UB si la // jerarquia canvia); cerca lineal a l'array (mida petita, no és hot path). std::size_t enemy_index = 0; for (std::size_t i = 0; i < ctx.enemies.size(); ++i) { if (&ctx.enemies[i] == &enemy) { enemy_index = i; break; } } const auto OWNER = static_cast(Defaults::Entities::ENEMY_OWNER_BASE + enemy_index); slot->fire(enemy.getCenter(), angle, OWNER, action.bullet_speed, cfg); } } // namespace 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) { // Si una acció prèvia d'aquest chain ha destruït l'enemic // (típicament DECREASE_HEALTH→ON_NO_HEALTH→SET_HURT-wounded→DESTROY), // saltem la resta — no té sentit aplicar APPLY_IMPULSE o FLASH a un // cos ja inactiu. if (!enemy.isActive()) { break; } switch (action.type) { case EnemyActionType::SET_HURT: if (enemy.isWounded()) { // Segon hit sobre wounded → mort immediata (regla 2-hits). dispatchEvent(ctx, enemy, EnemyEventType::ON_DESTROY, shooter_id, bullet); enemy.destroy(); } else { enemy.hurt(shooter_id); } break; case EnemyActionType::DESTROY: dispatchEvent(ctx, enemy, EnemyEventType::ON_DESTROY, shooter_id, bullet); enemy.destroy(); break; case EnemyActionType::ADD_SCORE: doAddScore(ctx, enemy, shooter_id); break; case EnemyActionType::CREATE_DEBRIS: // Explosió de mort: trossos en color cos (correcte físicament). spawnDebrisForEnemy(ctx, enemy, bullet, 1.0F, enemy.getConfig().colors.normal); break; case EnemyActionType::CREATE_DEBRIS_PARTIAL: // Xip d'impacte: trossos en color wounded (daurat) per // diferenciar-los visualment del cos i marcar "damage". spawnDebrisForEnemy(ctx, enemy, bullet, Defaults::Enemies::Debris::PARTIAL_PIECE_SCALE, enemy.getConfig().colors.wounded); break; case EnemyActionType::CREATE_FIREWORKS: // Burst de mort: línia blanca + glow wounded (daurat) per // marcar la mort com a esdeveniment "calent" i lluminós. spawnFireworksForEnemy(ctx, enemy, Defaults::FX::Firework::N_POINTS, Defaults::FX::Firework::SPEED, Defaults::FX::Firework::DEFAULT_COLOR, enemy.getConfig().colors.wounded); break; case EnemyActionType::CREATE_FIREWORKS_SMALL: // Espurna d'impacte: línia + glow tots dos en wounded // (daurat) per contrastar amb el cos i unificar la "tema // de damage" amb el debris parcial. spawnFireworksForEnemy(ctx, enemy, Defaults::Enemies::Fireworks::SMALL_N_POINTS, Defaults::Enemies::Fireworks::SMALL_SPEED, enemy.getConfig().colors.wounded, enemy.getConfig().colors.wounded); break; case EnemyActionType::APPLY_IMPULSE: if (!will_die) { doApplyImpulse(enemy, bullet); } break; case EnemyActionType::DECREASE_HEALTH: enemy.decrementHealth(shooter_id); if (enemy.getHealth() <= 0) { dispatchEvent(ctx, enemy, EnemyEventType::ON_NO_HEALTH, shooter_id, bullet); } break; case EnemyActionType::FLASH: enemy.triggerFlash(); break; case EnemyActionType::FIRE_BULLET: doFireBullet(ctx, enemy, action); break; } } } } // namespace Systems::EnemyEvents