feat(enemy): migrar el moviment dels enemics a un sistema d'IA declaratiu
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
};
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user