// enemy_ai_system.cpp - Implementació del dispatcher de moviment d'enemics // © 2026 JailDesigner #include "game/systems/enemy_ai_system.hpp" #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.hpp" #include "game/entities/enemy_ai.hpp" #include "game/entities/enemy_config.hpp" #include "game/entities/ship.hpp" namespace Systems::EnemyAi { namespace { auto randFloat01() -> float { return static_cast(std::rand()) / static_cast(RAND_MAX); } auto velocityToAngle(const Vec2& velocity) -> float { if (velocity.lengthSquared() < 0.0001F) { return 0.0F; } return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); } // Retorna el centre del ship actiu més proper a l'enemic, o nullptr si // no n'hi ha cap viu. Els ships destruïts (is_hit_) i els slots nullptr // (player no participant al match) queden filtrats. 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; } // ZIGZAG: canvi de direcció probabilístic. Còpia literal del legacy // Enemy::behaviorPentagon. void moveZigzag(Enemy& enemy, float delta_time) { const auto& mv = enemy.getConfig().ai.movement; EnemyAiState& state = enemy.getAiState(); state.direction_change_timer += delta_time; if (randFloat01() < mv.zigzag_prob_per_second * delta_time) { const Vec2 VEL = enemy.getBody().velocity; const float CURRENT_ANGLE = velocityToAngle(VEL); const float DELTA = randFloat01() * mv.angle_change_max; const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA); const float SPEED = VEL.length(); enemy.setVelocityFromAngle(NEW_ANGLE, SPEED); state.direction_change_timer = 0.0F; } } // TRACKING: cada N segons, interpola la velocitat actual cap a la // direcció del ship mantenint la mateixa magnitud. Còpia literal del // legacy Enemy::behaviorSquare. void moveTracking(Enemy& enemy, float delta_time) { const auto& mv = enemy.getConfig().ai.movement; EnemyAiState& state = enemy.getAiState(); state.tracking_timer += delta_time; const Vec2* ship_pos = findNearestShipPosition(enemy); if (state.tracking_timer < mv.tracking_interval || ship_pos == nullptr) { return; } state.tracking_timer = 0.0F; const Vec2 TO_SHIP = *ship_pos - enemy.getCenter(); const float DIST = TO_SHIP.length(); if (DIST <= 0.0F) { return; } const Vec2 DESIRED_DIR = TO_SHIP / DIST; const float SPEED = enemy.getBody().velocity.length(); const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED; const float STRENGTH = state.tracking_strength; Vec2 new_vel = (enemy.getBody().velocity * (1.0F - STRENGTH)) + (DESIRED_VEL * STRENGTH); const float NEW_SPEED = new_vel.length(); if (NEW_SPEED > 0.0F) { new_vel = new_vel * (SPEED / NEW_SPEED); } enemy.getBody().velocity = new_vel; } // CHASE / FLEE comparteixen lògica: steering continu cap a (o lluny de) // la direcció ideal, preservant la magnitud de velocitat. La força és // strength * dt clampejada a 1 (LERP frame-independent simple). void steerTowards(Enemy& enemy, const Vec2& desired_dir, float strength, float delta_time) { const float SPEED = enemy.getBody().velocity.length(); if (SPEED <= 0.0F) { return; } const Vec2 DESIRED_VEL = desired_dir * SPEED; const float T = std::min(1.0F, strength * delta_time); Vec2 new_vel = (enemy.getBody().velocity * (1.0F - T)) + (DESIRED_VEL * T); const float NEW_SPEED = new_vel.length(); if (NEW_SPEED > 0.0F) { new_vel = new_vel * (SPEED / NEW_SPEED); } enemy.getBody().velocity = new_vel; } void moveChase(Enemy& enemy, float delta_time) { const Vec2* ship_pos = findNearestShipPosition(enemy); if (ship_pos == nullptr) { return; } const Vec2 TO_SHIP = *ship_pos - enemy.getCenter(); const float DIST = TO_SHIP.length(); if (DIST <= 0.0F) { return; } steerTowards(enemy, TO_SHIP / DIST, enemy.getConfig().ai.movement.chase_strength, delta_time); } void moveFlee(Enemy& enemy, float delta_time) { const Vec2* ship_pos = findNearestShipPosition(enemy); if (ship_pos == nullptr) { return; } const Vec2 AWAY = enemy.getCenter() - *ship_pos; const float DIST = AWAY.length(); if (DIST <= 0.0F) { return; } steerTowards(enemy, AWAY / DIST, enemy.getConfig().ai.movement.flee_strength, delta_time); } // RECTILINEAR_PROXIMITY: rectilini (cap modificació a velocity); boost // de rotació visual quan distància al ship < proximity_distance. Còpia // literal del legacy Enemy::behaviorPinwheel. void moveRectilinearProximity(Enemy& enemy, float /*delta_time*/) { const auto& mv = enemy.getConfig().ai.movement; const Vec2* ship_pos = findNearestShipPosition(enemy); if (ship_pos == nullptr) { return; } const Vec2 TO_SHIP = *ship_pos - enemy.getCenter(); const float DIST = TO_SHIP.length(); const float BASE = enemy.getRotationBase(); if (DIST < mv.proximity_distance) { enemy.setRotationDelta(BASE * mv.rotation_proximity_multiplier); } else { enemy.setRotationDelta(BASE); } } // SHOOT: cerca slot lliure a ctx.bullets i el dispara amb el bullet config // referenciat per nom (lazy-load via BulletRegistry). Angle segons aim_mode + // jitter. owner_id = ENEMY_OWNER_BASE + enemy_index per al filtre d'autoimpacte. void doShoot(Systems::Collision::Context& ctx, const Enemy& enemy, std::size_t enemy_index, const AiTickAction& action) { if (action.bullet_config_name.empty()) { return; } const BulletConfig* cfg = BulletRegistry::get(action.bullet_config_name); if (cfg == nullptr) { return; } // Cerca slot dins la zona reservada per a enemics: així no robem // slots als pools de player (que iteren [0, MAX_BULLETS) i [MAX_BULLETS, 2*MAX_BULLETS)). 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) { // Sense ship viu: degrada a random per no congelar el dispar. angle = randFloat01() * 2.0F * Constants::PI; } else { const Vec2 TO = *target - enemy.getCenter(); // angle=0 apunta amunt (eix Y negatiu SDL): atan2 + PI/2. 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; } const auto OWNER = static_cast(Defaults::Entities::ENEMY_OWNER_BASE + enemy_index); slot->fire(enemy.getCenter(), angle, OWNER, action.bullet_speed, cfg); } void runMovement(Enemy& enemy, float delta_time) { switch (enemy.getConfig().ai.movement.type) { case MovementType::ZIGZAG: case MovementType::WANDER: // WANDER reusa la mecànica de canvi de direcció probabilístic; // l'única diferència és semàntica i el tunning dels paràmetres. moveZigzag(enemy, delta_time); break; case MovementType::TRACKING: moveTracking(enemy, delta_time); break; case MovementType::RECTILINEAR_PROXIMITY: moveRectilinearProximity(enemy, delta_time); break; case MovementType::CHASE: moveChase(enemy, delta_time); break; case MovementType::FLEE: moveFlee(enemy, delta_time); break; } } } // namespace void move(Enemy& enemy, float delta_time) { if (!enemy.isActive() || enemy.isWounded()) { return; } runMovement(enemy, delta_time); } void tick(Systems::Collision::Context& ctx, Enemy& enemy, std::size_t enemy_index, float delta_time) { if (!enemy.isActive() || enemy.isWounded()) { return; } runMovement(enemy, delta_time); // Accions periòdiques: decrementa timer, dispara quan ≤0. auto& timers = enemy.getAiTickTimers(); const auto& actions = enemy.getConfig().ai.tick; for (std::size_t i = 0; i < actions.size() && i < timers.size(); ++i) { timers[i] -= delta_time; if (timers[i] > 0.0F) { continue; } timers[i] = actions[i].interval; switch (actions[i].type) { case AiActionType::SHOOT: doShoot(ctx, enemy, enemy_index, actions[i]); break; } } } } // namespace Systems::EnemyAi