feat(entities): migrar la configuració dels 3 enemics a data/entities/<type>/*.yaml
This commit is contained in:
+35
-116
@@ -14,6 +14,8 @@
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/constants.hpp"
|
||||
#include "game/entities/enemy_config.hpp"
|
||||
#include "game/entities/enemy_registry.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -27,25 +29,20 @@ namespace {
|
||||
}
|
||||
|
||||
// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag).
|
||||
// Si está parado, devuelve 0.
|
||||
auto velocityToAngle(const Vec2& velocity) -> float {
|
||||
if (velocity.lengthSquared() < 0.0001F) {
|
||||
return 0.0F;
|
||||
}
|
||||
// El movimiento (vx, vy) corresponde a angle - PI/2; invertimos.
|
||||
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Enemy::Enemy(Rendering::Renderer* renderer)
|
||||
: Entity(renderer),
|
||||
|
||||
tracking_strength_(Defaults::Enemies::Square::TRACKING_STRENGTH) {
|
||||
: Entity(renderer) {
|
||||
brightness_ = Defaults::Brightness::ENEMIC;
|
||||
|
||||
// Configuración del cuerpo físico — defaults para enemy genérico.
|
||||
// init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo).
|
||||
// Cuerpo físico — defaults comuns; init() ajusta mass/radius segons el tipus.
|
||||
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS);
|
||||
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
|
||||
body_.restitution = Defaults::Enemies::Body::RESTITUTION;
|
||||
@@ -55,56 +52,23 @@ Enemy::Enemy(Rendering::Renderer* renderer)
|
||||
|
||||
void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
type_ = type;
|
||||
config_ = &EnemyRegistry::get(type);
|
||||
const EnemyConfig& cfg = *config_;
|
||||
|
||||
const char* shape_file = nullptr;
|
||||
float base_speed = 0.0F;
|
||||
float rotation_delta_min = 0.0F;
|
||||
float rotation_delta_max = 0.0F;
|
||||
float type_mass = Defaults::Enemies::Body::DEFAULT_MASS;
|
||||
collision_radius_ = cfg.physics.collision_radius;
|
||||
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pentagon::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Pentagon::MASS;
|
||||
break;
|
||||
|
||||
case EnemyType::SQUARE:
|
||||
shape_file = Defaults::Enemies::Square::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Square::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Square::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Square::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Square::MASS;
|
||||
tracking_timer_ = 0.0F;
|
||||
break;
|
||||
|
||||
case EnemyType::PINWHEEL:
|
||||
shape_file = Defaults::Enemies::Pinwheel::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pinwheel::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Pinwheel::MASS;
|
||||
break;
|
||||
|
||||
default:
|
||||
std::cerr << "[Enemy] Error: tipo desconocido ("
|
||||
<< static_cast<int>(type_) << "), usando PENTAGON\n";
|
||||
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pentagon::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
|
||||
break;
|
||||
// Cas Square: resetejar tracking timer al spawn.
|
||||
if (type_ == EnemyType::SQUARE) {
|
||||
tracking_timer_ = 0.0F;
|
||||
tracking_strength_ = cfg.behavior.tracking_strength;
|
||||
}
|
||||
|
||||
body_.setMass(type_mass);
|
||||
body_.radius = Defaults::Entities::ENEMY_RADIUS;
|
||||
body_.setMass(cfg.physics.mass);
|
||||
body_.radius = collision_radius_;
|
||||
|
||||
// Cargar shape
|
||||
shape_ = Graphics::ShapeLoader::load(shape_file);
|
||||
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
||||
if (!shape_ || !shape_->isValid()) {
|
||||
std::cerr << "[Enemy] Error: no se ha podido cargar " << shape_file << '\n';
|
||||
std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
|
||||
}
|
||||
|
||||
// Posición aleatoria con comprobación de seguridad
|
||||
@@ -112,14 +76,14 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
float max_x;
|
||||
float min_y;
|
||||
float max_y;
|
||||
Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y);
|
||||
Constants::getSafePlayAreaBounds(collision_radius_, min_x, max_x, min_y, max_y);
|
||||
|
||||
if (ship_pos != nullptr) {
|
||||
bool found_safe_position = false;
|
||||
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
|
||||
float candidate_x;
|
||||
float candidate_y;
|
||||
if (attemptSafeSpawn(*ship_pos, candidate_x, candidate_y)) {
|
||||
if (attemptSafeSpawn(*ship_pos, collision_radius_, candidate_x, candidate_y)) {
|
||||
center_.x = candidate_x;
|
||||
center_.y = candidate_y;
|
||||
found_safe_position = true;
|
||||
@@ -143,30 +107,27 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
|
||||
// Dirección inicial aleatoria, velocidad escalar según tipo
|
||||
const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F;
|
||||
setVelocityFromAngle(ANGLE_INICIAL, base_speed);
|
||||
setVelocityFromAngle(ANGLE_INICIAL, cfg.physics.speed);
|
||||
|
||||
// Sincronizar body_ con posición inicial
|
||||
body_.position = center_;
|
||||
body_.angle = 0.0F;
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.clearAccumulators();
|
||||
|
||||
// Rotación visual aleatoria (independiente del body)
|
||||
const float ROTATION_DELTA_RANGE = rotation_delta_max - rotation_delta_min;
|
||||
rotation_delta_ = rotation_delta_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_DELTA_RANGE);
|
||||
// Rotación visual aleatoria dins del rang del tipus
|
||||
const float ROTATION_RANGE = cfg.physics.rotation_delta_max - cfg.physics.rotation_delta_min;
|
||||
rotation_delta_ = cfg.physics.rotation_delta_min +
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_RANGE);
|
||||
rotation_ = 0.0F;
|
||||
|
||||
// Estado de animación
|
||||
animation_ = EnemyAnimation();
|
||||
animation_.rotation_delta_base = rotation_delta_;
|
||||
animation_.rotation_delta_target = rotation_delta_;
|
||||
animation_.rotation_delta_t = 1.0F;
|
||||
|
||||
// Invulnerabilidad post-spawn
|
||||
invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
||||
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
|
||||
|
||||
// Timer para próximo cambio de dirección (Pentagon)
|
||||
direction_change_timer_ = 0.0F;
|
||||
|
||||
is_active_ = true;
|
||||
@@ -177,8 +138,6 @@ void Enemy::update(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Decremento de timer "herido"; al cruzar 0 marca expiración para que el
|
||||
// system layer dispare la explosión diferida.
|
||||
wound_expired_this_frame_ = false;
|
||||
if (wounded_timer_ > 0.0F) {
|
||||
wounded_timer_ -= delta_time;
|
||||
@@ -188,7 +147,6 @@ void Enemy::update(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Decremento de invulnerabilidad + LERP de brightness
|
||||
if (invulnerability_timer_ > 0.0F) {
|
||||
invulnerability_timer_ -= delta_time;
|
||||
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
|
||||
@@ -201,9 +159,6 @@ void Enemy::update(float delta_time) {
|
||||
brightness_ = START + ((END - START) * SMOOTH_T);
|
||||
}
|
||||
|
||||
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición).
|
||||
// Skip cuando está herido: el enemy és un "cos mort" inert, només
|
||||
// respon a la inèrcia del impulse rebut i a les col·lisions físiques.
|
||||
if (!isWounded()) {
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
@@ -218,15 +173,12 @@ void Enemy::update(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Animaciones (palpitación + rotación acelerada)
|
||||
updateAnimation(delta_time);
|
||||
|
||||
// Rotación visual (decoración, no afecta movimiento)
|
||||
rotation_ += rotation_delta_ * delta_time;
|
||||
}
|
||||
|
||||
void Enemy::postUpdate(float /*delta_time*/) {
|
||||
// Sincronizar mirror tras la integración del world.
|
||||
if (is_active_) {
|
||||
center_ = body_.position;
|
||||
}
|
||||
@@ -237,26 +189,14 @@ void Enemy::draw() const {
|
||||
return;
|
||||
}
|
||||
const float SCALE = computeCurrentScale();
|
||||
SDL_Color color{};
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
color = Defaults::Palette::PENTAGON;
|
||||
break;
|
||||
case EnemyType::SQUARE:
|
||||
color = Defaults::Palette::SQUARE;
|
||||
break;
|
||||
case EnemyType::PINWHEEL:
|
||||
color = Defaults::Palette::PINWHEEL;
|
||||
break;
|
||||
}
|
||||
SDL_Color color = config_->colors.normal;
|
||||
|
||||
// Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado
|
||||
// a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo).
|
||||
// Parpadeo dorado mientras está herido.
|
||||
if (wounded_timer_ > 0.0F) {
|
||||
const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ;
|
||||
const float T = std::fmod(wounded_timer_, CYCLE);
|
||||
if (T < (CYCLE / 2.0F)) {
|
||||
color = Defaults::Palette::WOUNDED;
|
||||
color = config_->colors.wounded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +207,7 @@ void Enemy::destroy() {
|
||||
is_active_ = false;
|
||||
body_.velocity = Vec2{};
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.radius = 0.0F; // No colisiona mientras está inactivo
|
||||
body_.radius = 0.0F;
|
||||
wounded_timer_ = 0.0F;
|
||||
wound_expired_this_frame_ = false;
|
||||
last_hit_by_ = 0xFF;
|
||||
@@ -276,8 +216,6 @@ void Enemy::destroy() {
|
||||
void Enemy::hurt(uint8_t shooter_id) {
|
||||
wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
|
||||
last_hit_by_ = shooter_id;
|
||||
// El so HIT ara el reprodueix la bala quan es trenca en debris
|
||||
// (Systems::Collision::breakBullet), no l'enemic en entrar a HURT.
|
||||
}
|
||||
|
||||
void Enemy::applyImpulse(const Vec2& impulse) {
|
||||
@@ -285,12 +223,10 @@ void Enemy::applyImpulse(const Vec2& impulse) {
|
||||
}
|
||||
|
||||
void Enemy::setVelocity(float speed) {
|
||||
// Mantener la dirección actual del body, cambiar solo la magnitud.
|
||||
const float CURRENT_SPEED = body_.velocity.length();
|
||||
if (CURRENT_SPEED > 0.0F) {
|
||||
body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
|
||||
} else {
|
||||
// Sin dirección actual: usar ángulo aleatorio
|
||||
const float A = (std::rand() % 360) * Constants::PI / 180.0F;
|
||||
setVelocityFromAngle(A, speed);
|
||||
}
|
||||
@@ -306,13 +242,11 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
|
||||
void Enemy::behaviorPentagon(float delta_time) {
|
||||
direction_change_timer_ += delta_time;
|
||||
|
||||
// Probabilidad de zigzag por segundo (calibrada para sensación equivalente
|
||||
// a la versión vieja que disparaba en cada toque de pared).
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) {
|
||||
if (RAND_VAL < config_->behavior.zigzag_prob_per_second * delta_time) {
|
||||
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
|
||||
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) *
|
||||
Defaults::Enemies::Pentagon::ANGLE_CHANGE_MAX;
|
||||
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);
|
||||
@@ -320,12 +254,12 @@ void Enemy::behaviorPentagon(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// SQUARE: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección
|
||||
// SQUARE: tracking discreto cada tracking_interval. Ajusta dirección
|
||||
// hacia el ship mezclando con tracking_strength_.
|
||||
void Enemy::behaviorSquare(float delta_time) {
|
||||
tracking_timer_ += delta_time;
|
||||
|
||||
if (tracking_timer_ >= Defaults::Enemies::Square::TRACKING_INTERVAL && ship_position_ != nullptr) {
|
||||
if (tracking_timer_ >= config_->behavior.tracking_interval && ship_position_ != nullptr) {
|
||||
tracking_timer_ = 0.0F;
|
||||
|
||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
||||
@@ -335,11 +269,9 @@ void Enemy::behaviorSquare(float delta_time) {
|
||||
const float SPEED = body_.velocity.length();
|
||||
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
|
||||
|
||||
// Mezcla LERP: velocidad actual con la deseada según tracking_strength_.
|
||||
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
|
||||
(DESIRED_VEL * tracking_strength_);
|
||||
|
||||
// Renormalizar a la velocidad escalar original
|
||||
const float NEW_SPEED = body_.velocity.length();
|
||||
if (NEW_SPEED > 0.0F) {
|
||||
body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
|
||||
@@ -349,20 +281,16 @@ void Enemy::behaviorSquare(float delta_time) {
|
||||
}
|
||||
|
||||
// PINWHEEL: movimiento recto + boost de rotación visual cerca del ship.
|
||||
// Sin tracking — solo cambios de dirección raros (igual que Pentagon pero
|
||||
// con probabilidad mucho menor).
|
||||
void Enemy::behaviorPinwheel(float /*delta_time*/) {
|
||||
// Boost de rotación visual por proximidad al ship
|
||||
if (ship_position_ != nullptr) {
|
||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
||||
const float DIST = TO_SHIP.length();
|
||||
if (DIST < Defaults::Enemies::Pinwheel::PROXIMITY_DISTANCE) {
|
||||
rotation_delta_ = animation_.rotation_delta_base * Defaults::Enemies::Pinwheel::ROTATION_DELTA_PROXIMITY_MULTIPLIER;
|
||||
if (DIST < config_->behavior.proximity_distance) {
|
||||
rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier;
|
||||
} else {
|
||||
rotation_delta_ = animation_.rotation_delta_base;
|
||||
}
|
||||
}
|
||||
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
|
||||
}
|
||||
|
||||
void Enemy::updateAnimation(float delta_time) {
|
||||
@@ -452,16 +380,7 @@ auto Enemy::computeCurrentScale() const -> float {
|
||||
}
|
||||
|
||||
auto Enemy::getBaseVelocity() const -> float {
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Enemies::Pentagon::SPEED;
|
||||
case EnemyType::SQUARE:
|
||||
return Defaults::Enemies::Square::SPEED;
|
||||
case EnemyType::PINWHEEL:
|
||||
return Defaults::Enemies::Pinwheel::SPEED;
|
||||
default:
|
||||
return Defaults::Enemies::Pentagon::SPEED;
|
||||
}
|
||||
return EnemyRegistry::get(type_).physics.speed;
|
||||
}
|
||||
|
||||
auto Enemy::getBaseRotation() const -> float {
|
||||
@@ -474,12 +393,12 @@ void Enemy::setTrackingStrength(float strength) {
|
||||
}
|
||||
}
|
||||
|
||||
auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool {
|
||||
auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float& out_x, float& out_y) -> bool {
|
||||
float min_x;
|
||||
float max_x;
|
||||
float min_y;
|
||||
float max_y;
|
||||
Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y);
|
||||
Constants::getSafePlayAreaBounds(collision_radius, min_x, max_x, min_y, max_y);
|
||||
|
||||
const int RANGE_X = static_cast<int>(max_x - min_x);
|
||||
const int RANGE_Y = static_cast<int>(max_y - min_y);
|
||||
|
||||
@@ -17,6 +17,9 @@ enum class EnemyType : uint8_t {
|
||||
PINWHEEL = 2 // Molinillo agresivo (rápido, girando)
|
||||
};
|
||||
|
||||
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
|
||||
struct EnemyConfig;
|
||||
|
||||
// Estado de animación (palpitación + rotación acelerada)
|
||||
struct EnemyAnimation {
|
||||
// Palpitación (efecto respiración)
|
||||
@@ -48,10 +51,8 @@ class Enemy : public Entities::Entity {
|
||||
// Override: Interfaz de Entity
|
||||
[[nodiscard]] auto isActive() const -> bool override { return is_active_; }
|
||||
|
||||
// Override: Interfaz de colisión
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
||||
return Defaults::Entities::ENEMY_RADIUS;
|
||||
}
|
||||
// Override: Interfaz de colisión. El radi ve del config carregat per tipus.
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
|
||||
// Mentre fa spawn (invulnerable) segueix col·lisionant: les bales el
|
||||
// poden abatre i el cos físic rebota amb la nau. El damage a la nau
|
||||
// segueix filtrat per `isInvulnerable()` al detectShipEnemy.
|
||||
@@ -65,6 +66,9 @@ class Enemy : public Entities::Entity {
|
||||
// Getters
|
||||
[[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; }
|
||||
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
|
||||
// Configuració activa (carregada al darrer init()). Vàlida mentre l'enemic
|
||||
// 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; }
|
||||
@@ -101,29 +105,34 @@ class Enemy : public Entities::Entity {
|
||||
void applyImpulse(const Vec2& impulse);
|
||||
|
||||
private:
|
||||
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
||||
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo
|
||||
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo.
|
||||
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
|
||||
float rotation_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
|
||||
// Configuració carregada per tipus (apunta a una entrada de EnemyRegistry).
|
||||
// nullptr abans del primer init(); per això getConfig() només és vàlid post-init.
|
||||
const EnemyConfig* config_{nullptr};
|
||||
|
||||
// Cache local del radi (per evitar dereferenciar config_ a getCollisionRadius);
|
||||
// s'actualitza a init() segons el tipus.
|
||||
float collision_radius_{0.0F};
|
||||
|
||||
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s)
|
||||
float rotation_{0.0F}; // Rotación visual acumulada
|
||||
bool is_active_{false};
|
||||
|
||||
EnemyType type_{EnemyType::PENTAGON};
|
||||
EnemyAnimation animation_;
|
||||
|
||||
// Comportamiento type-specific
|
||||
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección
|
||||
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking)
|
||||
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5
|
||||
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección
|
||||
float tracking_timer_{0.0F};
|
||||
const Vec2* ship_position_{nullptr};
|
||||
float tracking_strength_{0.0F};
|
||||
float direction_change_timer_{0.0F};
|
||||
|
||||
// Invulnerabilidad post-spawn
|
||||
float invulnerability_timer_{0.0F};
|
||||
|
||||
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración.
|
||||
// Estado "herido"
|
||||
float wounded_timer_{0.0F};
|
||||
bool wound_expired_this_frame_{false};
|
||||
uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución
|
||||
uint8_t last_hit_by_{0xFF};
|
||||
|
||||
// Métodos privados
|
||||
void updateAnimation(float delta_time);
|
||||
@@ -133,8 +142,8 @@ class Enemy : public Entities::Entity {
|
||||
void behaviorSquare(float delta_time);
|
||||
void behaviorPinwheel(float delta_time);
|
||||
[[nodiscard]] auto computeCurrentScale() const -> float;
|
||||
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy.
|
||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
|
||||
// Static: passa collision_radius com a param per no acoblar a *this.
|
||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, 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).
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// enemy_config.cpp - Implementació del parser de EnemyConfig
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "game/entities/enemy_config.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
|
||||
if (!node.is_sequence() || node.size() != 3) {
|
||||
return false;
|
||||
}
|
||||
const auto R = node[0].get_value<uint32_t>();
|
||||
const auto G = node[1].get_value<uint32_t>();
|
||||
const auto B = node[2].get_value<uint32_t>();
|
||||
out = SDL_Color{
|
||||
.r = static_cast<uint8_t>(R),
|
||||
.g = static_cast<uint8_t>(G),
|
||||
.b = static_cast<uint8_t>(B),
|
||||
.a = 255};
|
||||
return true;
|
||||
}
|
||||
|
||||
auto aiTypeFromString(const std::string& s) -> std::optional<EnemyType> {
|
||||
if (s == "pentagon") { return EnemyType::PENTAGON; }
|
||||
if (s == "square") { return EnemyType::SQUARE; }
|
||||
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
||||
-> std::optional<EnemyConfig> {
|
||||
try {
|
||||
EnemyConfig cfg;
|
||||
|
||||
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "enemy";
|
||||
|
||||
// ai_type — validació estricta contra el tipus esperat
|
||||
if (!node.contains("ai_type")) {
|
||||
std::cerr << "[EnemyConfig] Error: falta 'ai_type' a " << cfg.name << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto AI_STR = node["ai_type"].get_value<std::string>();
|
||||
const auto PARSED = aiTypeFromString(AI_STR);
|
||||
if (!PARSED) {
|
||||
std::cerr << "[EnemyConfig] Error: ai_type desconegut '" << AI_STR << "' a " << cfg.name << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
if (*PARSED != expected_ai_type) {
|
||||
std::cerr << "[EnemyConfig] Error: ai_type '" << AI_STR
|
||||
<< "' no coincideix amb el tipus esperat (per directori) a " << cfg.name << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
cfg.ai_type = *PARSED;
|
||||
|
||||
// shape
|
||||
if (!node.contains("shape") || !node["shape"].contains("path")) {
|
||||
std::cerr << "[EnemyConfig] Error: falta 'shape.path' a " << cfg.name << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
cfg.shape.path = node["shape"]["path"].get_value<std::string>();
|
||||
|
||||
// physics
|
||||
if (!node.contains("physics")) {
|
||||
std::cerr << "[EnemyConfig] Error: falta 'physics' a " << cfg.name << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& physics = node["physics"];
|
||||
cfg.physics.mass = physics["mass"].get_value<float>();
|
||||
cfg.physics.speed = physics["speed"].get_value<float>();
|
||||
cfg.physics.rotation_delta_min = physics["rotation_delta_min"].get_value<float>();
|
||||
cfg.physics.rotation_delta_max = physics["rotation_delta_max"].get_value<float>();
|
||||
cfg.physics.collision_radius = physics["collision_radius"].get_value<float>();
|
||||
|
||||
// behavior — tots els camps són opcionals; només l'AI corresponent els consumeix.
|
||||
if (node.contains("behavior")) {
|
||||
const auto& b = node["behavior"];
|
||||
if (b.contains("zigzag_prob_per_second")) {
|
||||
cfg.behavior.zigzag_prob_per_second = b["zigzag_prob_per_second"].get_value<float>();
|
||||
}
|
||||
if (b.contains("angle_change_max")) {
|
||||
cfg.behavior.angle_change_max = b["angle_change_max"].get_value<float>();
|
||||
}
|
||||
if (b.contains("tracking_strength")) {
|
||||
cfg.behavior.tracking_strength = b["tracking_strength"].get_value<float>();
|
||||
}
|
||||
if (b.contains("tracking_interval")) {
|
||||
cfg.behavior.tracking_interval = b["tracking_interval"].get_value<float>();
|
||||
}
|
||||
if (b.contains("rotation_proximity_multiplier")) {
|
||||
cfg.behavior.rotation_proximity_multiplier = b["rotation_proximity_multiplier"].get_value<float>();
|
||||
}
|
||||
if (b.contains("proximity_distance")) {
|
||||
cfg.behavior.proximity_distance = b["proximity_distance"].get_value<float>();
|
||||
}
|
||||
}
|
||||
|
||||
// colors
|
||||
if (!node.contains("colors") ||
|
||||
!parseColor(node["colors"]["normal"], cfg.colors.normal) ||
|
||||
!parseColor(node["colors"]["wounded"], cfg.colors.wounded)) {
|
||||
std::cerr << "[EnemyConfig] Error: 'colors.normal' / 'colors.wounded' no són [r,g,b] a " << cfg.name << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// score
|
||||
if (!node.contains("score")) {
|
||||
std::cerr << "[EnemyConfig] Error: falta 'score' a " << cfg.name << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
cfg.score = node["score"].get_value<int>();
|
||||
|
||||
return cfg;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[EnemyConfig] Excepció parsejant: " << e.what() << '\n';
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// enemy_config.hpp - Configuració d'un tipus d'enemic carregada des de YAML
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Una instància per tipus (Pentagon/Square/Pinwheel), carregada un cop al
|
||||
// startup per EnemyRegistry i compartida entre totes les instàncies d'aquell
|
||||
// tipus. Estructura paral·lela a PlayerConfig.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/enemy.hpp" // EnemyType
|
||||
|
||||
struct EnemyConfig {
|
||||
struct ShapeCfg {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct PhysicsCfg {
|
||||
float mass;
|
||||
float speed;
|
||||
float rotation_delta_min;
|
||||
float rotation_delta_max;
|
||||
float collision_radius;
|
||||
};
|
||||
|
||||
// Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F
|
||||
// i no s'usen — el dispatch viu a Enemy::behaviorXxx.
|
||||
struct BehaviorCfg {
|
||||
// Pentagon
|
||||
float zigzag_prob_per_second{0.0F};
|
||||
float angle_change_max{0.0F};
|
||||
// Square
|
||||
float tracking_strength{0.0F};
|
||||
float tracking_interval{0.0F};
|
||||
// Pinwheel
|
||||
float rotation_proximity_multiplier{0.0F};
|
||||
float proximity_distance{0.0F};
|
||||
};
|
||||
|
||||
struct ColorsCfg {
|
||||
SDL_Color normal;
|
||||
SDL_Color wounded;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
EnemyType ai_type;
|
||||
ShapeCfg shape;
|
||||
PhysicsCfg physics;
|
||||
BehaviorCfg behavior;
|
||||
ColorsCfg colors;
|
||||
int score;
|
||||
|
||||
// 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).
|
||||
static auto fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
||||
-> std::optional<EnemyConfig>;
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
// enemy_registry.cpp - Implementació del registre estàtic d'enemics
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "game/entities/enemy_registry.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/entities/entity_loader.hpp"
|
||||
|
||||
EnemyConfig EnemyRegistry::pentagon_config;
|
||||
EnemyConfig EnemyRegistry::square_config;
|
||||
EnemyConfig EnemyRegistry::pinwheel_config;
|
||||
bool EnemyRegistry::loaded = false;
|
||||
|
||||
namespace {
|
||||
|
||||
auto loadOne(const std::string& name, EnemyType expected_type, EnemyConfig& out) -> bool {
|
||||
auto yaml = Entities::EntityLoader::load(name);
|
||||
if (!yaml) {
|
||||
std::cerr << "[EnemyRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
|
||||
return false;
|
||||
}
|
||||
auto cfg = EnemyConfig::fromYaml(*yaml, expected_type);
|
||||
if (!cfg) {
|
||||
std::cerr << "[EnemyRegistry] Error: format invàlid a " << name << ".yaml\n";
|
||||
return false;
|
||||
}
|
||||
out = *cfg;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto EnemyRegistry::loadAll() -> bool {
|
||||
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
|
||||
loadOne("square", EnemyType::SQUARE, square_config) &&
|
||||
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config);
|
||||
loaded = OK;
|
||||
if (OK) {
|
||||
std::cout << "[EnemyRegistry] 3 configuracions d'enemic carregades.\n";
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
|
||||
if (!loaded) {
|
||||
std::cerr << "[EnemyRegistry] FATAL: get() abans de loadAll()\n";
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
switch (type) {
|
||||
case EnemyType::PENTAGON:
|
||||
return pentagon_config;
|
||||
case EnemyType::SQUARE:
|
||||
return square_config;
|
||||
case EnemyType::PINWHEEL:
|
||||
return pinwheel_config;
|
||||
}
|
||||
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// enemy_registry.hpp - Registre estàtic de configuracions d'enemics per tipus
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Carrega els 3 fitxers YAML (pentagon, square, pinwheel) un cop al startup
|
||||
// i exposa el lookup per EnemyType. Pensat per a ser invocat des de
|
||||
// GameScene; si la càrrega falla, el caller decideix avortar.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "game/entities/enemy.hpp"
|
||||
#include "game/entities/enemy_config.hpp"
|
||||
|
||||
class EnemyRegistry {
|
||||
public:
|
||||
EnemyRegistry() = delete; // tot estàtic
|
||||
|
||||
// Carrega els 3 descriptors. Retorna true si tots tres s'han carregat
|
||||
// i parsejat correctament. Cridar abans del primer get().
|
||||
static auto loadAll() -> bool;
|
||||
|
||||
// Lookup. Cal haver cridat loadAll() abans. Si el tipus no s'ha carregat
|
||||
// (loadAll fallida o no cridada), avorta amb log fatal.
|
||||
static auto get(EnemyType type) -> const EnemyConfig&;
|
||||
|
||||
private:
|
||||
static EnemyConfig pentagon_config;
|
||||
static EnemyConfig square_config;
|
||||
static EnemyConfig pinwheel_config;
|
||||
static bool loaded;
|
||||
};
|
||||
Reference in New Issue
Block a user