diff --git a/data/entities/square/square.yaml b/data/entities/square/square.yaml index 1d62445..703bf3d 100644 --- a/data/entities/square/square.yaml +++ b/data/entities/square/square.yaml @@ -15,10 +15,11 @@ physics: linear_damping: 0.0 angular_damping: 0.0 -behavior: - # Square: tracking discret cap a la nau cada N segons. - tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1) - tracking_interval: 1.0 # segons entre updates d'angle +ai: + # Square: persecució contínua del ship més proper (steering suau, "tanc lent"). + movement: + type: chase + chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear) animation: pulse: diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index 3678c91..aa59066 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -4,12 +4,15 @@ #pragma once #include +#include #include #include "core/entities/entity.hpp" #include "core/types.hpp" #include "game/entities/enemy_ai.hpp" +class Ship; + // Tipo de enemy enum class EnemyType : uint8_t { PENTAGON = 0, // Pentágono esquivador (zigzag) @@ -71,9 +74,10 @@ class Enemy : public Entities::Entity { // ha estat inicialitzat almenys un cop; abans és nullptr. [[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; } - // 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_; } + // Referències als 2 ships per a AI de tracking/proximity/chase/flee. + // nullptr = ship inexistent al match. El sistema d'IA filtra per ship->isActive(). + void setShips(const Ship* p1, const Ship* p2) { ships_ = {p1, p2}; } + [[nodiscard]] auto getShips() const -> const std::array& { return ships_; } // Accessors per al sistema d'IA (Systems::EnemyAi). [[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; } @@ -132,8 +136,8 @@ class Enemy : public Entities::Entity { // 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}; + // Referències als 2 ships per a AI de tracking/proximity/chase/flee. + std::array ships_{nullptr, nullptr}; // Invulnerabilidad post-spawn float invulnerability_timer_{0.0F}; diff --git a/source/game/entities/enemy_ai.hpp b/source/game/entities/enemy_ai.hpp index 8b12422..d61439c 100644 --- a/source/game/entities/enemy_ai.hpp +++ b/source/game/entities/enemy_ai.hpp @@ -15,11 +15,12 @@ // 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) + ZIGZAG, // Canvi de direcció probabilístic agressiu (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 + WANDER, // Canvi de direcció probabilístic suau, sense target + CHASE, // Steering continu cap al ship més proper + FLEE, // Steering continu allunyant-se del ship més proper }; // Accions que s'executen periòdicament (un timer per acció). Futur (Fase C): @@ -39,7 +40,7 @@ enum class AimMode : uint8_t { struct MovementConfig { MovementType type{MovementType::ZIGZAG}; - // ZIGZAG + // ZIGZAG i WANDER (canvi de direcció probabilístic; comparteixen camps). float angle_change_max{0.0F}; float zigzag_prob_per_second{0.0F}; @@ -50,6 +51,11 @@ struct MovementConfig { // RECTILINEAR_PROXIMITY float rotation_proximity_multiplier{0.0F}; float proximity_distance{0.0F}; + + // CHASE / FLEE: força del steering per segon (LERP velocity ↔ direcció ideal). + // 1.0 = en ~1s la velocitat queda totalment realineada cap al target. + float chase_strength{0.0F}; + float flee_strength{0.0F}; }; // Acció periòdica. interval = segons entre disparades; el dispatcher manté un diff --git a/source/game/entities/enemy_config.cpp b/source/game/entities/enemy_config.cpp index 66c84ca..08d1e7e 100644 --- a/source/game/entities/enemy_config.cpp +++ b/source/game/entities/enemy_config.cpp @@ -226,6 +226,9 @@ namespace { if (s == "zigzag") { return MovementType::ZIGZAG; } if (s == "tracking") { return MovementType::TRACKING; } if (s == "rectilinear_proximity") { return MovementType::RECTILINEAR_PROXIMITY; } + if (s == "wander") { return MovementType::WANDER; } + if (s == "chase") { return MovementType::CHASE; } + if (s == "flee") { return MovementType::FLEE; } return std::nullopt; } @@ -264,6 +267,8 @@ namespace { READ_OPT("tracking_interval", out.tracking_interval); READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier); READ_OPT("proximity_distance", out.proximity_distance); + READ_OPT("chase_strength", out.chase_strength); + READ_OPT("flee_strength", out.flee_strength); return true; } diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index e73334b..6a7cf8d 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -186,7 +186,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) // Registramos el body al world incluso inactivo: con radius=0 no colisiona // ni se mueve, y al init() del stage system se activa sin re-registrar. for (auto& enemy : enemies_) { - enemy.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now) + enemy.setShips(ships_.data(), &ships_[1]); physics_world_.addBody(&enemy.getBody()); // DON'T call enemy.init() here - stage system handles spawning } diff --git a/source/game/systems/enemy_ai_system.cpp b/source/game/systems/enemy_ai_system.cpp index 90b7f14..2c0b147 100644 --- a/source/game/systems/enemy_ai_system.cpp +++ b/source/game/systems/enemy_ai_system.cpp @@ -11,6 +11,7 @@ #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 { @@ -27,6 +28,28 @@ namespace Systems::EnemyAi { 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) { @@ -53,7 +76,7 @@ namespace Systems::EnemyAi { EnemyAiState& state = enemy.getAiState(); state.tracking_timer += delta_time; - const Vec2* ship_pos = enemy.getShipPosition(); + const Vec2* ship_pos = findNearestShipPosition(enemy); if (state.tracking_timer < mv.tracking_interval || ship_pos == nullptr) { return; } @@ -79,12 +102,56 @@ namespace Systems::EnemyAi { 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 = enemy.getShipPosition(); + const Vec2* ship_pos = findNearestShipPosition(enemy); if (ship_pos == nullptr) { return; } @@ -106,6 +173,9 @@ namespace Systems::EnemyAi { } 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: @@ -114,6 +184,12 @@ namespace Systems::EnemyAi { 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; } }