feat(entities): migrar la configuració dels 3 enemics a data/entities/<type>/*.yaml

This commit is contained in:
2026-05-25 10:01:12 +02:00
parent ed4d3a3915
commit 39bda0775e
14 changed files with 431 additions and 212 deletions
+35 -116
View File
@@ -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);