diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index b1b206d..f83b3eb 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -28,14 +28,6 @@ namespace { }; } - // Recupera el "ángulo equivalente" de un body en movimiento (para zigzag). - 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); - } - // Random float [0..1). auto randFloat01() -> float { return static_cast(std::rand()) / static_cast(RAND_MAX); @@ -62,10 +54,8 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { config_ = &EnemyRegistry::get(type); const EnemyConfig& cfg = *config_; - if (type_ == EnemyType::SQUARE) { - tracking_timer_ = 0.0F; - tracking_strength_ = cfg.behavior.tracking_strength; - } + ai_state_ = EnemyAiState{}; + ai_state_.tracking_strength = cfg.ai.movement.tracking_strength; shape_ = Graphics::ShapeLoader::load(cfg.shape.path); if (!shape_ || !shape_->isValid()) { @@ -136,8 +126,6 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { invulnerability_timer_ = cfg.spawn.invulnerability_duration; brightness_ = cfg.spawn.invulnerability_brightness_start; - direction_change_timer_ = 0.0F; - is_active_ = true; } @@ -167,22 +155,9 @@ void Enemy::update(float delta_time) { brightness_ = START + ((END - START) * SMOOTH_T); } - if (!isWounded()) { - switch (type_) { - case EnemyType::PENTAGON: - case EnemyType::STAR: - // STAR reusa el zigzag esquivador de Pentagon. Si en el futur - // vol comportament propi, separa-li el cas. - behaviorPentagon(delta_time); - break; - case EnemyType::SQUARE: - behaviorSquare(delta_time); - break; - case EnemyType::PINWHEEL: - behaviorPinwheel(delta_time); - break; - } - } + // El moviment es delega a Systems::EnemyAi::tick, invocat des de l'scene + // ABANS d'aquest update (manté l'ordre: AI escriu velocity/rotation_delta, + // després animation pot modular rotation_delta via rotation_accel). updateAnimation(delta_time); @@ -246,59 +221,6 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) { body_.velocity = angleToDirection(angle_movement) * speed; } -// PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics) -// en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld. -void Enemy::behaviorPentagon(float delta_time) { - direction_change_timer_ += delta_time; - - if (randFloat01() < config_->behavior.zigzag_prob_per_second * delta_time) { - const float CURRENT_ANGLE = velocityToAngle(body_.velocity); - const float DELTA = randFloat01() * config_->behavior.angle_change_max; - const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA); - const float SPEED = body_.velocity.length(); - setVelocityFromAngle(NEW_ANGLE, SPEED); - direction_change_timer_ = 0.0F; - } -} - -// SQUARE: tracking discret cap a la nau cada N segons. -void Enemy::behaviorSquare(float delta_time) { - tracking_timer_ += delta_time; - - if (tracking_timer_ >= config_->behavior.tracking_interval && ship_position_ != nullptr) { - tracking_timer_ = 0.0F; - - const Vec2 TO_SHIP = *ship_position_ - center_; - const float DIST = TO_SHIP.length(); - if (DIST > 0.0F) { - const Vec2 DESIRED_DIR = TO_SHIP / DIST; - const float SPEED = body_.velocity.length(); - const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED; - - body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) + - (DESIRED_VEL * tracking_strength_); - - const float NEW_SPEED = body_.velocity.length(); - if (NEW_SPEED > 0.0F) { - body_.velocity = body_.velocity * (SPEED / NEW_SPEED); - } - } - } -} - -// PINWHEEL: movement rectilini + boost de rotació visual prop del ship. -void Enemy::behaviorPinwheel(float /*delta_time*/) { - if (ship_position_ != nullptr) { - const Vec2 TO_SHIP = *ship_position_ - center_; - const float DIST = TO_SHIP.length(); - if (DIST < config_->behavior.proximity_distance) { - rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier; - } else { - rotation_delta_ = animation_.rotation_delta_base; - } - } -} - void Enemy::updateAnimation(float delta_time) { updatePulse(delta_time); updateRotationAcceleration(delta_time); @@ -373,7 +295,7 @@ auto Enemy::getBaseRotation() const -> float { void Enemy::setTrackingStrength(float strength) { if (type_ == EnemyType::SQUARE) { - tracking_strength_ = strength; + ai_state_.tracking_strength = strength; } } diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index 041c68b..3678c91 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -8,6 +8,7 @@ #include "core/entities/entity.hpp" #include "core/types.hpp" +#include "game/entities/enemy_ai.hpp" // Tipo de enemy enum class EnemyType : uint8_t { @@ -72,6 +73,14 @@ class Enemy : public Entities::Entity { // Set ship position reference for tracking behavior void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } + [[nodiscard]] auto getShipPosition() const -> const Vec2* { return ship_position_; } + + // Accessors per al sistema d'IA (Systems::EnemyAi). + [[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; } + [[nodiscard]] auto getRotationBase() const -> float { return animation_.rotation_delta_base; } + void setRotationDelta(float rot) { rotation_delta_ = rot; } + // Public: el sistema d'IA reorienta la velocitat des d'un angle. + void setVelocityFromAngle(float angle_movement, float speed); // Stage system API (base stats) [[nodiscard]] auto getBaseVelocity() const -> float; @@ -120,11 +129,11 @@ class Enemy : public Entities::Entity { EnemyType type_{EnemyType::PENTAGON}; EnemyAnimation animation_; - // Comportamiento type-specific - float tracking_timer_{0.0F}; + // Estat per-instància que la primitiva de moviment manté entre frames. + EnemyAiState ai_state_; + + // Referència a la posició del ship per a AI de tracking/proximity. const Vec2* ship_position_{nullptr}; - float tracking_strength_{0.0F}; - float direction_change_timer_{0.0F}; // Invulnerabilidad post-spawn float invulnerability_timer_{0.0F}; @@ -138,14 +147,7 @@ class Enemy : public Entities::Entity { void updateAnimation(float delta_time); void updatePulse(float delta_time); void updateRotationAcceleration(float delta_time); - void behaviorPentagon(float delta_time); - void behaviorSquare(float delta_time); - void behaviorPinwheel(float delta_time); [[nodiscard]] auto computeCurrentScale() const -> float; // Static: passa els paràmetres com a args per no acoblar a *this. static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool; - - // Helper: setear body_.velocity desde un ángulo y magnitud. - // angle_movement=0 apunta hacia arriba (eje Y negativo SDL). - void setVelocityFromAngle(float angle_movement, float speed); }; diff --git a/source/game/entities/enemy_ai.hpp b/source/game/entities/enemy_ai.hpp new file mode 100644 index 0000000..8b12422 --- /dev/null +++ b/source/game/entities/enemy_ai.hpp @@ -0,0 +1,77 @@ +// enemy_ai.hpp - Sistema declaratiu d'IA per a enemics +// © 2026 JailDesigner +// +// Cada enemic declara al seu YAML quin movement primitiu fa servir i, opcional- +// ment, una llista d'accions periòdiques (tick). El motor només dispatcha; el +// comportament viu a les dades. Patró paral·lel al d'events declaratius +// (enemy_event.hpp). + +#pragma once + +#include +#include +#include + +// Primitiva de moviment activa per a un enemic. Substitueix el switch +// hardcoded sobre EnemyType. +enum class MovementType : uint8_t { + ZIGZAG, // Canvi de direcció probabilístic (Pentagon/Star) + TRACKING, // LERP discret cap al ship cada N segons (Square) + RECTILINEAR_PROXIMITY, // Rectilini + boost rotació visual prop del ship (Pinwheel) + // Futurs (Fase B): + // WANDER, CHASE, FLEE +}; + +// Accions que s'executen periòdicament (un timer per acció). Futur (Fase C): +// SHOOT amb aim_mode/jitter/bullet config. +enum class AiActionType : uint8_t { + SHOOT, +}; + +enum class AimMode : uint8_t { + RANDOM, // Angle uniformement aleatori + AIMED, // atan2(nearest_ship - center) + soroll gaussià (jitter_rad) +}; + +// Camps de tots els movements; només el subset rellevant per al type actiu +// s'usa. Els altres queden a 0.0F. Mateixa filosofia que la BehaviorCfg +// llegacy però amb el type explícit dins. +struct MovementConfig { + MovementType type{MovementType::ZIGZAG}; + + // ZIGZAG + float angle_change_max{0.0F}; + float zigzag_prob_per_second{0.0F}; + + // TRACKING + float tracking_strength{0.0F}; + float tracking_interval{0.0F}; + + // RECTILINEAR_PROXIMITY + float rotation_proximity_multiplier{0.0F}; + float proximity_distance{0.0F}; +}; + +// Acció periòdica. interval = segons entre disparades; el dispatcher manté un +// timer per acció (paral·lel a aquesta llista) i dispara quan arriba a 0. +struct AiTickAction { + AiActionType type{AiActionType::SHOOT}; + float interval{1.0F}; + AimMode aim_mode{AimMode::RANDOM}; + float jitter_rad{0.0F}; + std::string bullet_config_name; // referit per nom (Fase C); buit a Fase A/B +}; + +struct EnemyAiConfig { + MovementConfig movement; + std::vector tick; +}; + +// Estat per-instància que la primitiva de moviment manté entre frames (timers +// d'interval, contadors de canvi de direcció...). Es viu dins de Enemy i el +// sistema d'IA hi escriu via getAiState(). +struct EnemyAiState { + float direction_change_timer{0.0F}; // ZIGZAG + float tracking_timer{0.0F}; // TRACKING + float tracking_strength{0.0F}; // TRACKING (cau de cfg, mutable per dificultat) +}; diff --git a/source/game/entities/enemy_config.cpp b/source/game/entities/enemy_config.cpp index 4e56bcc..66c84ca 100644 --- a/source/game/entities/enemy_config.cpp +++ b/source/game/entities/enemy_config.cpp @@ -222,6 +222,138 @@ namespace { }; } + auto movementTypeFromString(const std::string& s) -> std::optional { + if (s == "zigzag") { return MovementType::ZIGZAG; } + if (s == "tracking") { return MovementType::TRACKING; } + if (s == "rectilinear_proximity") { return MovementType::RECTILINEAR_PROXIMITY; } + return std::nullopt; + } + + auto aiActionTypeFromString(const std::string& s) -> std::optional { + if (s == "shoot") { return AiActionType::SHOOT; } + return std::nullopt; + } + + auto aimModeFromString(const std::string& s) -> std::optional { + if (s == "random") { return AimMode::RANDOM; } + if (s == "aimed") { return AimMode::AIMED; } + return std::nullopt; + } + + auto parseMovement(const fkyaml::node& mv_node, const std::string& enemy_name, MovementConfig& out) -> bool { + if (!mv_node.contains("type")) { + std::cerr << "[EnemyConfig] Error: falta 'ai.movement.type' a " << enemy_name << '\n'; + return false; + } + const auto TYPE_STR = mv_node["type"].get_value(); + const auto PARSED = movementTypeFromString(TYPE_STR); + if (!PARSED) { + std::cerr << "[EnemyConfig] Error: movement type desconegut '" << TYPE_STR + << "' a " << enemy_name << '\n'; + return false; + } + out.type = *PARSED; + const auto READ_OPT = [&mv_node](const char* key, float& dst) { + if (mv_node.contains(key)) { + dst = mv_node[key].get_value(); + } + }; + READ_OPT("angle_change_max", out.angle_change_max); + READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second); + READ_OPT("tracking_strength", out.tracking_strength); + READ_OPT("tracking_interval", out.tracking_interval); + READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier); + READ_OPT("proximity_distance", out.proximity_distance); + return true; + } + + auto parseTickList(const fkyaml::node& list_node, const std::string& enemy_name, std::vector& out) -> bool { + if (!list_node.is_sequence()) { + std::cerr << "[EnemyConfig] Error: 'ai.tick' ha de ser una llista a " << enemy_name << '\n'; + return false; + } + for (const auto& item : list_node) { + if (!item.contains("action")) { + std::cerr << "[EnemyConfig] Error: entrada sense 'action' a ai.tick (" + << enemy_name << ")\n"; + return false; + } + const auto STR = item["action"].get_value(); + const auto PARSED = aiActionTypeFromString(STR); + if (!PARSED) { + std::cerr << "[EnemyConfig] Error: acció d'ai desconeguda '" << STR + << "' a " << enemy_name << '\n'; + return false; + } + AiTickAction action; + action.type = *PARSED; + if (item.contains("interval")) { + action.interval = item["interval"].get_value(); + } + if (item.contains("aim_mode")) { + const auto AIM_STR = item["aim_mode"].get_value(); + const auto AIM = aimModeFromString(AIM_STR); + if (!AIM) { + std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR + << "' a ai.tick (" << enemy_name << ")\n"; + return false; + } + action.aim_mode = *AIM; + } + if (item.contains("jitter_rad")) { + action.jitter_rad = item["jitter_rad"].get_value(); + } + if (item.contains("bullet")) { + action.bullet_config_name = item["bullet"].get_value(); + } + out.push_back(action); + } + return true; + } + + // Migració progressiva: si el YAML no porta secció `ai:`, derivem el + // movement a partir de l'ai_type i copiem els paràmetres de la BehaviorCfg + // ja parsejada. Comportament idèntic al hardcoded actual. + void fillLegacyAiDefaults(EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) { + switch (ai_type) { + case EnemyType::PENTAGON: + case EnemyType::STAR: + out.movement.type = MovementType::ZIGZAG; + out.movement.angle_change_max = legacy.angle_change_max; + out.movement.zigzag_prob_per_second = legacy.zigzag_prob_per_second; + break; + case EnemyType::SQUARE: + out.movement.type = MovementType::TRACKING; + out.movement.tracking_strength = legacy.tracking_strength; + out.movement.tracking_interval = legacy.tracking_interval; + break; + case EnemyType::PINWHEEL: + out.movement.type = MovementType::RECTILINEAR_PROXIMITY; + out.movement.rotation_proximity_multiplier = legacy.rotation_proximity_multiplier; + out.movement.proximity_distance = legacy.proximity_distance; + break; + } + } + + auto parseAi(const fkyaml::node& node, const std::string& name, EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) -> bool { + if (!node.contains("ai")) { + fillLegacyAiDefaults(ai_type, legacy, out); + return true; + } + const auto& ai = node["ai"]; + if (!ai.contains("movement")) { + std::cerr << "[EnemyConfig] Error: falta 'ai.movement' a " << name << '\n'; + return false; + } + if (!parseMovement(ai["movement"], name, out.movement)) { + return false; + } + if (ai.contains("tick") && !parseTickList(ai["tick"], name, out.tick)) { + return false; + } + return true; + } + auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool { if (!node.contains("events")) { fillLegacyDefaults(out); @@ -268,6 +400,7 @@ auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type) if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; } if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; } if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; } + if (!parseAi(node, cfg.name, cfg.ai_type, cfg.behavior, cfg.ai)) { return std::nullopt; } return cfg; } catch (const std::exception& e) { diff --git a/source/game/entities/enemy_config.hpp b/source/game/entities/enemy_config.hpp index a7ca422..72ca35d 100644 --- a/source/game/entities/enemy_config.hpp +++ b/source/game/entities/enemy_config.hpp @@ -14,6 +14,7 @@ #include "external/fkyaml_node.hpp" #include "game/entities/enemy.hpp" // EnemyType +#include "game/entities/enemy_ai.hpp" #include "game/entities/enemy_event.hpp" struct EnemyConfig { @@ -100,6 +101,7 @@ struct EnemyConfig { ColorsCfg colors; int score; EnemyEventConfig events; + EnemyAiConfig ai; // Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del // YAML coincideix amb el tipus que el caller espera (segons el directori). diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 31303f3..e73334b 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -21,6 +21,7 @@ #include "game/stage_system/stage_loader.hpp" #include "game/systems/collision_system.hpp" #include "game/systems/continue_system.hpp" +#include "game/systems/enemy_ai_system.hpp" #include "game/systems/init_hud_animator.hpp" // Using declarations per simplificar el codi @@ -353,6 +354,7 @@ auto GameScene::stepContinueScreen(float delta_time) -> bool { // Enemies, bullets y efectos siguen moviéndose en background. for (auto& enemy : enemies_) { + Systems::EnemyAi::tick(enemy, delta_time); enemy.update(delta_time); } for (auto& bullet : bullets_) { @@ -379,6 +381,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool { // Enemies, bullets y efectos siguen moviéndose como fondo. for (auto& enemy : enemies_) { + Systems::EnemyAi::tick(enemy, delta_time); enemy.update(delta_time); } for (auto& bullet : bullets_) { @@ -428,6 +431,7 @@ void GameScene::stepDeathSequence(float delta_time) { // aunque otros jugadores aún jueguen. if (algun_mort) { for (auto& enemy : enemies_) { + Systems::EnemyAi::tick(enemy, delta_time); enemy.update(delta_time); } for (auto& bullet : bullets_) { @@ -528,6 +532,7 @@ void GameScene::runStagePlaying(float delta_time) { } } for (auto& enemy : enemies_) { + Systems::EnemyAi::tick(enemy, delta_time); enemy.update(delta_time); } diff --git a/source/game/systems/enemy_ai_system.cpp b/source/game/systems/enemy_ai_system.cpp new file mode 100644 index 0000000..90b7f14 --- /dev/null +++ b/source/game/systems/enemy_ai_system.cpp @@ -0,0 +1,120 @@ +// enemy_ai_system.cpp - Implementació del dispatcher de moviment d'enemics +// © 2026 JailDesigner + +#include "game/systems/enemy_ai_system.hpp" + +#include +#include + +#include "core/types.hpp" +#include "game/constants.hpp" +#include "game/entities/enemy.hpp" +#include "game/entities/enemy_ai.hpp" +#include "game/entities/enemy_config.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); + } + + // 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 = enemy.getShipPosition(); + 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; + } + + // 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 = enemy.getShipPosition(); + 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); + } + } + + } // namespace + + void tick(Enemy& enemy, float delta_time) { + if (!enemy.isActive() || enemy.isWounded()) { + return; + } + switch (enemy.getConfig().ai.movement.type) { + case MovementType::ZIGZAG: + moveZigzag(enemy, delta_time); + break; + case MovementType::TRACKING: + moveTracking(enemy, delta_time); + break; + case MovementType::RECTILINEAR_PROXIMITY: + moveRectilinearProximity(enemy, delta_time); + break; + } + } + +} // namespace Systems::EnemyAi diff --git a/source/game/systems/enemy_ai_system.hpp b/source/game/systems/enemy_ai_system.hpp new file mode 100644 index 0000000..89b8915 --- /dev/null +++ b/source/game/systems/enemy_ai_system.hpp @@ -0,0 +1,23 @@ +// enemy_ai_system.hpp - Executa la primitiva de moviment d'un enemic +// © 2026 JailDesigner +// +// Llegeix `enemy.getConfig().ai.movement` i aplica la primitiva corresponent +// (ZIGZAG, TRACKING, RECTILINEAR_PROXIMITY...). El sistema reemplaça el switch +// per type que vivia dins Enemy::update. +// +// A Fase C s'ampliarà per dispatchar també `ai.tick` (accions periòdiques tipus +// SHOOT), moment en què caldrà passar el Systems::Collision::Context per +// accedir a les bales i als ships. + +#pragma once + +class Enemy; + +namespace Systems::EnemyAi { + + // Aplica la primitiva de moviment activa de l'enemic durant aquest frame. + // No fa res si l'enemic està actualment wounded (manté el comportament + // legacy del switch original). + void tick(Enemy& enemy, float delta_time); + +} // namespace Systems::EnemyAi