feat(enemy): migrar el moviment dels enemics a un sistema d'IA declaratiu

This commit is contained in:
2026-05-25 17:45:30 +02:00
parent 410955de3c
commit 61e40e88f4
8 changed files with 379 additions and 95 deletions
+6 -84
View File
@@ -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). // Random float [0..1).
auto randFloat01() -> float { auto randFloat01() -> float {
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX); return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
@@ -62,10 +54,8 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
config_ = &EnemyRegistry::get(type); config_ = &EnemyRegistry::get(type);
const EnemyConfig& cfg = *config_; const EnemyConfig& cfg = *config_;
if (type_ == EnemyType::SQUARE) { ai_state_ = EnemyAiState{};
tracking_timer_ = 0.0F; ai_state_.tracking_strength = cfg.ai.movement.tracking_strength;
tracking_strength_ = cfg.behavior.tracking_strength;
}
shape_ = Graphics::ShapeLoader::load(cfg.shape.path); shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
@@ -136,8 +126,6 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
invulnerability_timer_ = cfg.spawn.invulnerability_duration; invulnerability_timer_ = cfg.spawn.invulnerability_duration;
brightness_ = cfg.spawn.invulnerability_brightness_start; brightness_ = cfg.spawn.invulnerability_brightness_start;
direction_change_timer_ = 0.0F;
is_active_ = true; is_active_ = true;
} }
@@ -167,22 +155,9 @@ void Enemy::update(float delta_time) {
brightness_ = START + ((END - START) * SMOOTH_T); brightness_ = START + ((END - START) * SMOOTH_T);
} }
if (!isWounded()) { // El moviment es delega a Systems::EnemyAi::tick, invocat des de l'scene
switch (type_) { // ABANS d'aquest update (manté l'ordre: AI escriu velocity/rotation_delta,
case EnemyType::PENTAGON: // després animation pot modular rotation_delta via rotation_accel).
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;
}
}
updateAnimation(delta_time); updateAnimation(delta_time);
@@ -246,59 +221,6 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
body_.velocity = angleToDirection(angle_movement) * 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) { void Enemy::updateAnimation(float delta_time) {
updatePulse(delta_time); updatePulse(delta_time);
updateRotationAcceleration(delta_time); updateRotationAcceleration(delta_time);
@@ -373,7 +295,7 @@ auto Enemy::getBaseRotation() const -> float {
void Enemy::setTrackingStrength(float strength) { void Enemy::setTrackingStrength(float strength) {
if (type_ == EnemyType::SQUARE) { if (type_ == EnemyType::SQUARE) {
tracking_strength_ = strength; ai_state_.tracking_strength = strength;
} }
} }
+13 -11
View File
@@ -8,6 +8,7 @@
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/entities/enemy_ai.hpp"
// Tipo de enemy // Tipo de enemy
enum class EnemyType : uint8_t { enum class EnemyType : uint8_t {
@@ -72,6 +73,14 @@ class Enemy : public Entities::Entity {
// Set ship position reference for tracking behavior // Set ship position reference for tracking behavior
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } 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) // Stage system API (base stats)
[[nodiscard]] auto getBaseVelocity() const -> float; [[nodiscard]] auto getBaseVelocity() const -> float;
@@ -120,11 +129,11 @@ class Enemy : public Entities::Entity {
EnemyType type_{EnemyType::PENTAGON}; EnemyType type_{EnemyType::PENTAGON};
EnemyAnimation animation_; EnemyAnimation animation_;
// Comportamiento type-specific // Estat per-instància que la primitiva de moviment manté entre frames.
float tracking_timer_{0.0F}; EnemyAiState ai_state_;
// Referència a la posició del ship per a AI de tracking/proximity.
const Vec2* ship_position_{nullptr}; const Vec2* ship_position_{nullptr};
float tracking_strength_{0.0F};
float direction_change_timer_{0.0F};
// Invulnerabilidad post-spawn // Invulnerabilidad post-spawn
float invulnerability_timer_{0.0F}; float invulnerability_timer_{0.0F};
@@ -138,14 +147,7 @@ class Enemy : public Entities::Entity {
void updateAnimation(float delta_time); void updateAnimation(float delta_time);
void updatePulse(float delta_time); void updatePulse(float delta_time);
void updateRotationAcceleration(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; [[nodiscard]] auto computeCurrentScale() const -> float;
// Static: passa els paràmetres com a args per no acoblar a *this. // 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; 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);
}; };
+77
View File
@@ -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 <cstdint>
#include <string>
#include <vector>
// 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<AiTickAction> 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)
};
+133
View File
@@ -222,6 +222,138 @@ namespace {
}; };
} }
auto movementTypeFromString(const std::string& s) -> std::optional<MovementType> {
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<AiActionType> {
if (s == "shoot") { return AiActionType::SHOOT; }
return std::nullopt;
}
auto aimModeFromString(const std::string& s) -> std::optional<AimMode> {
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<std::string>();
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<float>();
}
};
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<AiTickAction>& 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<std::string>();
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<float>();
}
if (item.contains("aim_mode")) {
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
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<float>();
}
if (item.contains("bullet")) {
action.bullet_config_name = item["bullet"].get_value<std::string>();
}
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 { auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
if (!node.contains("events")) { if (!node.contains("events")) {
fillLegacyDefaults(out); 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 (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; } if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
if (!parseEvents(node, cfg.name, cfg.events)) { 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; return cfg;
} catch (const std::exception& e) { } catch (const std::exception& e) {
+2
View File
@@ -14,6 +14,7 @@
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/entities/enemy.hpp" // EnemyType #include "game/entities/enemy.hpp" // EnemyType
#include "game/entities/enemy_ai.hpp"
#include "game/entities/enemy_event.hpp" #include "game/entities/enemy_event.hpp"
struct EnemyConfig { struct EnemyConfig {
@@ -100,6 +101,7 @@ struct EnemyConfig {
ColorsCfg colors; ColorsCfg colors;
int score; int score;
EnemyEventConfig events; EnemyEventConfig events;
EnemyAiConfig ai;
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del // 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). // YAML coincideix amb el tipus que el caller espera (segons el directori).
+5
View File
@@ -21,6 +21,7 @@
#include "game/stage_system/stage_loader.hpp" #include "game/stage_system/stage_loader.hpp"
#include "game/systems/collision_system.hpp" #include "game/systems/collision_system.hpp"
#include "game/systems/continue_system.hpp" #include "game/systems/continue_system.hpp"
#include "game/systems/enemy_ai_system.hpp"
#include "game/systems/init_hud_animator.hpp" #include "game/systems/init_hud_animator.hpp"
// Using declarations per simplificar el codi // 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. // Enemies, bullets y efectos siguen moviéndose en background.
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
Systems::EnemyAi::tick(enemy, delta_time);
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
@@ -379,6 +381,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool {
// Enemies, bullets y efectos siguen moviéndose como fondo. // Enemies, bullets y efectos siguen moviéndose como fondo.
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
Systems::EnemyAi::tick(enemy, delta_time);
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
@@ -428,6 +431,7 @@ void GameScene::stepDeathSequence(float delta_time) {
// aunque otros jugadores aún jueguen. // aunque otros jugadores aún jueguen.
if (algun_mort) { if (algun_mort) {
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
Systems::EnemyAi::tick(enemy, delta_time);
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
@@ -528,6 +532,7 @@ void GameScene::runStagePlaying(float delta_time) {
} }
} }
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
Systems::EnemyAi::tick(enemy, delta_time);
enemy.update(delta_time); enemy.update(delta_time);
} }
+120
View File
@@ -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 <cmath>
#include <cstdlib>
#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<float>(std::rand()) / static_cast<float>(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
+23
View File
@@ -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