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
+23
View File
@@ -0,0 +1,23 @@
name: pentagon
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
shape:
path: enemy_pentagon.shp
physics:
mass: 5.0
speed: 35.0 # px/s (esquivador lent)
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
collision_radius: 20.0
behavior:
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
zigzag_prob_per_second: 0.8
colors:
normal: [0, 255, 255] # Cyan pur "esquivador"
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
score: 100
+23
View File
@@ -0,0 +1,23 @@
name: pinwheel
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
shape:
path: enemy_pinwheel.shp
physics:
mass: 4.0 # Més lleuger — àgil
speed: 50.0 # px/s (el més ràpid)
rotation_delta_min: 3.0 # rad/s — rotació base elevada
rotation_delta_max: 6.0
collision_radius: 20.0
behavior:
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
proximity_distance: 100.0 # Llindar de distància (px)
colors:
normal: [255, 0, 255] # Magenta pur "agressiu"
wounded: [255, 220, 60]
score: 200
+23
View File
@@ -0,0 +1,23 @@
name: square
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
shape:
path: enemy_square.shp
physics:
mass: 8.0 # Més pesat — "tanc"
speed: 40.0 # px/s (velocitat mitjana)
rotation_delta_min: 0.3 # rad/s — rotació lenta
rotation_delta_max: 1.5
collision_radius: 20.0
behavior:
# Square: tracking discret cap a la nau cada N segons.
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
tracking_interval: 1.0 # segons entre updates d'angle
colors:
normal: [255, 0, 0] # Roig pur "tanc"
wounded: [255, 220, 60]
score: 150
+4 -42
View File
@@ -13,41 +13,10 @@ namespace Defaults::Enemies {
constexpr float ANGULAR_DAMPING = 0.0F; constexpr float ANGULAR_DAMPING = 0.0F;
} // namespace Body } // namespace Body
// Pentagon (esquivador - zigzag evasion) // NOTA: els paràmetres per tipus (Pentagon/Square/Pinwheel) i el Scoring
namespace Pentagon { // viuen ara a data/entities/{pentagon,square,pinwheel}/*.yaml i s'accedeixen
constexpr float SPEED = 35.0F; // px/s (slightly slower) // via EnemyRegistry::get(EnemyType). Aquí només queden els paràmetres
constexpr float MASS = 5.0F; // Masa estándar // compartits entre tots els tipus (animació, wounded, spawn).
constexpr float ANGLE_CHANGE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
constexpr float ANGLE_CHANGE_MAX = 1.0F; // Max random angle change (rad)
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
constexpr float ROTATION_DELTA_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
constexpr float ROTATION_DELTA_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon
// Square (perseguidor - tracks player)
namespace Square {
constexpr float SPEED = 40.0F; // px/s (medium speed)
constexpr float MASS = 8.0F; // Más pesado, "tanque"
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
constexpr float ROTATION_DELTA_MIN = 0.3F; // Slow rotation [+50%]
constexpr float ROTATION_DELTA_MAX = 1.5F; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Square
// Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Pinwheel {
constexpr float SPEED = 50.0F; // px/s (fastest)
constexpr float MASS = 4.0F; // Más liviano, ágil
constexpr float ANGLE_CHANGE_PROB = 0.05F; // 5% per wall hit (rare direction change)
constexpr float ANGLE_CHANGE_MAX = 0.3F; // Small angle adjustments
constexpr float ROTATION_DELTA_MIN = 3.0F; // Base rotation (rad/s) [+50%]
constexpr float ROTATION_DELTA_MAX = 6.0F; // [+50%]
constexpr float ROTATION_DELTA_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
} // namespace Pinwheel
// Animation parameters (shared) // Animation parameters (shared)
namespace Animation { namespace Animation {
@@ -92,11 +61,4 @@ namespace Defaults::Enemies {
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
} // namespace Spawn } // namespace Spawn
// Scoring system (puntuación per type de enemy)
namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
constexpr int SQUARE_SCORE = 150; // Square (perseguidor, 40 px/s)
constexpr int PINWHEEL_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring
} // namespace Defaults::Enemies } // namespace Defaults::Enemies
+1 -1
View File
@@ -9,7 +9,7 @@ namespace Defaults::Entities {
constexpr int MAX_BULLETS = 50; constexpr int MAX_BULLETS = 50;
// SHIP_RADIUS migrat a data/entities/player/player.yaml (physics.collision_radius). // SHIP_RADIUS migrat a data/entities/player/player.yaml (physics.collision_radius).
constexpr float ENEMY_RADIUS = 20.0F; // ENEMY_RADIUS migrat a data/entities/<type>/<type>.yaml (physics.collision_radius).
constexpr float BULLET_RADIUS = 3.0F; constexpr float BULLET_RADIUS = 3.0F;
} // namespace Defaults::Entities } // namespace Defaults::Entities
+4 -5
View File
@@ -14,11 +14,10 @@ namespace Defaults::Palette {
// brillantor perceptual sota el bloom (sense alterar la identitat de color). // brillantor perceptual sota el bloom (sense alterar la identitat de color).
// El canal dominant es manté a 255 a cada color per maximitzar la saturació // El canal dominant es manté a 255 a cada color per maximitzar la saturació
// visible quan el halo s'expandeix. // visible quan el halo s'expandeix.
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador" // SHIP s'ha migrat a data/entities/player/player.yaml (colors.normal).
constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank" // PENTAGON, SQUARE, PINWHEEL i WOUNDED han migrat a cada enemy YAML
constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu" // (colors.normal i colors.wounded).
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido // BULLET es queda compartit fins a la migració del bullet a YAML.
} // namespace Defaults::Palette } // namespace Defaults::Palette
+35 -116
View File
@@ -14,6 +14,8 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/enemy_config.hpp"
#include "game/entities/enemy_registry.hpp"
namespace { namespace {
@@ -27,25 +29,20 @@ namespace {
} }
// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag). // Recupera el "ángulo equivalente" de un body en movimiento (para zigzag).
// Si está parado, devuelve 0.
auto velocityToAngle(const Vec2& velocity) -> float { auto velocityToAngle(const Vec2& velocity) -> float {
if (velocity.lengthSquared() < 0.0001F) { if (velocity.lengthSquared() < 0.0001F) {
return 0.0F; return 0.0F;
} }
// El movimiento (vx, vy) corresponde a angle - PI/2; invertimos.
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
} }
} // namespace } // namespace
Enemy::Enemy(Rendering::Renderer* renderer) Enemy::Enemy(Rendering::Renderer* renderer)
: Entity(renderer), : Entity(renderer) {
tracking_strength_(Defaults::Enemies::Square::TRACKING_STRENGTH) {
brightness_ = Defaults::Brightness::ENEMIC; brightness_ = Defaults::Brightness::ENEMIC;
// Configuración del cuerpo físico — defaults para enemy genérico. // Cuerpo físico — defaults comuns; init() ajusta mass/radius segons el tipus.
// init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo).
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS); body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS);
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo) body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
body_.restitution = Defaults::Enemies::Body::RESTITUTION; body_.restitution = Defaults::Enemies::Body::RESTITUTION;
@@ -55,56 +52,23 @@ Enemy::Enemy(Rendering::Renderer* renderer)
void Enemy::init(EnemyType type, const Vec2* ship_pos) { void Enemy::init(EnemyType type, const Vec2* ship_pos) {
type_ = type; type_ = type;
config_ = &EnemyRegistry::get(type);
const EnemyConfig& cfg = *config_;
const char* shape_file = nullptr; collision_radius_ = cfg.physics.collision_radius;
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;
switch (type_) { // Cas Square: resetejar tracking timer al spawn.
case EnemyType::PENTAGON: if (type_ == EnemyType::SQUARE) {
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; tracking_timer_ = 0.0F;
base_speed = Defaults::Enemies::Pentagon::SPEED; tracking_strength_ = cfg.behavior.tracking_strength;
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;
} }
body_.setMass(type_mass); body_.setMass(cfg.physics.mass);
body_.radius = Defaults::Entities::ENEMY_RADIUS; body_.radius = collision_radius_;
// Cargar shape shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
shape_ = Graphics::ShapeLoader::load(shape_file);
if (!shape_ || !shape_->isValid()) { 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 // 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 max_x;
float min_y; float min_y;
float max_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) { if (ship_pos != nullptr) {
bool found_safe_position = false; bool found_safe_position = false;
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
float candidate_x; float candidate_x;
float candidate_y; 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_.x = candidate_x;
center_.y = candidate_y; center_.y = candidate_y;
found_safe_position = true; 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 // Dirección inicial aleatoria, velocidad escalar según tipo
const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F; 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_.position = center_;
body_.angle = 0.0F; body_.angle = 0.0F;
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.clearAccumulators(); body_.clearAccumulators();
// Rotación visual aleatoria (independiente del body) // Rotación visual aleatoria dins del rang del tipus
const float ROTATION_DELTA_RANGE = rotation_delta_max - rotation_delta_min; const float ROTATION_RANGE = cfg.physics.rotation_delta_max - cfg.physics.rotation_delta_min;
rotation_delta_ = rotation_delta_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_DELTA_RANGE); rotation_delta_ = cfg.physics.rotation_delta_min +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_RANGE);
rotation_ = 0.0F; rotation_ = 0.0F;
// Estado de animación
animation_ = EnemyAnimation(); animation_ = EnemyAnimation();
animation_.rotation_delta_base = rotation_delta_; animation_.rotation_delta_base = rotation_delta_;
animation_.rotation_delta_target = rotation_delta_; animation_.rotation_delta_target = rotation_delta_;
animation_.rotation_delta_t = 1.0F; animation_.rotation_delta_t = 1.0F;
// Invulnerabilidad post-spawn
invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
// Timer para próximo cambio de dirección (Pentagon)
direction_change_timer_ = 0.0F; direction_change_timer_ = 0.0F;
is_active_ = true; is_active_ = true;
@@ -177,8 +138,6 @@ void Enemy::update(float delta_time) {
return; 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; wound_expired_this_frame_ = false;
if (wounded_timer_ > 0.0F) { if (wounded_timer_ > 0.0F) {
wounded_timer_ -= delta_time; 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) { if (invulnerability_timer_ > 0.0F) {
invulnerability_timer_ -= delta_time; invulnerability_timer_ -= delta_time;
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F); invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
@@ -201,9 +159,6 @@ void Enemy::update(float delta_time) {
brightness_ = START + ((END - START) * SMOOTH_T); 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()) { if (!isWounded()) {
switch (type_) { switch (type_) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
@@ -218,15 +173,12 @@ void Enemy::update(float delta_time) {
} }
} }
// Animaciones (palpitación + rotación acelerada)
updateAnimation(delta_time); updateAnimation(delta_time);
// Rotación visual (decoración, no afecta movimiento)
rotation_ += rotation_delta_ * delta_time; rotation_ += rotation_delta_ * delta_time;
} }
void Enemy::postUpdate(float /*delta_time*/) { void Enemy::postUpdate(float /*delta_time*/) {
// Sincronizar mirror tras la integración del world.
if (is_active_) { if (is_active_) {
center_ = body_.position; center_ = body_.position;
} }
@@ -237,26 +189,14 @@ void Enemy::draw() const {
return; return;
} }
const float SCALE = computeCurrentScale(); const float SCALE = computeCurrentScale();
SDL_Color color{}; SDL_Color color = config_->colors.normal;
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;
}
// Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado // Parpadeo dorado mientras está herido.
// a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo).
if (wounded_timer_ > 0.0F) { if (wounded_timer_ > 0.0F) {
const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ; const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ;
const float T = std::fmod(wounded_timer_, CYCLE); const float T = std::fmod(wounded_timer_, CYCLE);
if (T < (CYCLE / 2.0F)) { if (T < (CYCLE / 2.0F)) {
color = Defaults::Palette::WOUNDED; color = config_->colors.wounded;
} }
} }
@@ -267,7 +207,7 @@ void Enemy::destroy() {
is_active_ = false; is_active_ = false;
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.radius = 0.0F; // No colisiona mientras está inactivo body_.radius = 0.0F;
wounded_timer_ = 0.0F; wounded_timer_ = 0.0F;
wound_expired_this_frame_ = false; wound_expired_this_frame_ = false;
last_hit_by_ = 0xFF; last_hit_by_ = 0xFF;
@@ -276,8 +216,6 @@ void Enemy::destroy() {
void Enemy::hurt(uint8_t shooter_id) { void Enemy::hurt(uint8_t shooter_id) {
wounded_timer_ = Defaults::Enemies::Wounded::DURATION; wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
last_hit_by_ = shooter_id; 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) { void Enemy::applyImpulse(const Vec2& impulse) {
@@ -285,12 +223,10 @@ void Enemy::applyImpulse(const Vec2& impulse) {
} }
void Enemy::setVelocity(float speed) { void Enemy::setVelocity(float speed) {
// Mantener la dirección actual del body, cambiar solo la magnitud.
const float CURRENT_SPEED = body_.velocity.length(); const float CURRENT_SPEED = body_.velocity.length();
if (CURRENT_SPEED > 0.0F) { if (CURRENT_SPEED > 0.0F) {
body_.velocity = body_.velocity * (speed / CURRENT_SPEED); body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
} else { } else {
// Sin dirección actual: usar ángulo aleatorio
const float A = (std::rand() % 360) * Constants::PI / 180.0F; const float A = (std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(A, speed); setVelocityFromAngle(A, speed);
} }
@@ -306,13 +242,11 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
void Enemy::behaviorPentagon(float delta_time) { void Enemy::behaviorPentagon(float delta_time) {
direction_change_timer_ += 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); 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 CURRENT_ANGLE = velocityToAngle(body_.velocity);
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * 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 NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
const float SPEED = body_.velocity.length(); const float SPEED = body_.velocity.length();
setVelocityFromAngle(NEW_ANGLE, SPEED); 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_. // hacia el ship mezclando con tracking_strength_.
void Enemy::behaviorSquare(float delta_time) { void Enemy::behaviorSquare(float delta_time) {
tracking_timer_ += 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; tracking_timer_ = 0.0F;
const Vec2 TO_SHIP = *ship_position_ - center_; const Vec2 TO_SHIP = *ship_position_ - center_;
@@ -335,11 +269,9 @@ void Enemy::behaviorSquare(float delta_time) {
const float SPEED = body_.velocity.length(); const float SPEED = body_.velocity.length();
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED; 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_)) + body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
(DESIRED_VEL * tracking_strength_); (DESIRED_VEL * tracking_strength_);
// Renormalizar a la velocidad escalar original
const float NEW_SPEED = body_.velocity.length(); const float NEW_SPEED = body_.velocity.length();
if (NEW_SPEED > 0.0F) { if (NEW_SPEED > 0.0F) {
body_.velocity = body_.velocity * (SPEED / NEW_SPEED); 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. // 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*/) { void Enemy::behaviorPinwheel(float /*delta_time*/) {
// Boost de rotación visual por proximidad al ship
if (ship_position_ != nullptr) { if (ship_position_ != nullptr) {
const Vec2 TO_SHIP = *ship_position_ - center_; const Vec2 TO_SHIP = *ship_position_ - center_;
const float DIST = TO_SHIP.length(); const float DIST = TO_SHIP.length();
if (DIST < Defaults::Enemies::Pinwheel::PROXIMITY_DISTANCE) { if (DIST < config_->behavior.proximity_distance) {
rotation_delta_ = animation_.rotation_delta_base * Defaults::Enemies::Pinwheel::ROTATION_DELTA_PROXIMITY_MULTIPLIER; rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier;
} else { } else {
rotation_delta_ = animation_.rotation_delta_base; rotation_delta_ = animation_.rotation_delta_base;
} }
} }
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
} }
void Enemy::updateAnimation(float delta_time) { void Enemy::updateAnimation(float delta_time) {
@@ -452,16 +380,7 @@ auto Enemy::computeCurrentScale() const -> float {
} }
auto Enemy::getBaseVelocity() const -> float { auto Enemy::getBaseVelocity() const -> float {
switch (type_) { return EnemyRegistry::get(type_).physics.speed;
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;
}
} }
auto Enemy::getBaseRotation() const -> float { 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 min_x;
float max_x; float max_x;
float min_y; float min_y;
float max_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_X = static_cast<int>(max_x - min_x);
const int RANGE_Y = static_cast<int>(max_y - min_y); const int RANGE_Y = static_cast<int>(max_y - min_y);
+26 -17
View File
@@ -17,6 +17,9 @@ enum class EnemyType : uint8_t {
PINWHEEL = 2 // Molinillo agresivo (rápido, girando) 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) // Estado de animación (palpitación + rotación acelerada)
struct EnemyAnimation { struct EnemyAnimation {
// Palpitación (efecto respiración) // Palpitación (efecto respiración)
@@ -48,10 +51,8 @@ class Enemy : public Entities::Entity {
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return is_active_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión // Override: Interfaz de colisión. El radi ve del config carregat per tipus.
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
return Defaults::Entities::ENEMY_RADIUS;
}
// Mentre fa spawn (invulnerable) segueix col·lisionant: les bales el // 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 // poden abatre i el cos físic rebota amb la nau. El damage a la nau
// segueix filtrat per `isInvulnerable()` al detectShipEnemy. // segueix filtrat per `isInvulnerable()` al detectShipEnemy.
@@ -65,6 +66,9 @@ class Enemy : public Entities::Entity {
// Getters // Getters
[[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; } [[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; }
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } [[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 // 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; }
@@ -101,29 +105,34 @@ class Enemy : public Entities::Entity {
void applyImpulse(const Vec2& impulse); void applyImpulse(const Vec2& impulse);
private: private:
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Configuració carregada per tipus (apunta a una entrada de EnemyRegistry).
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo // nullptr abans del primer init(); per això getConfig() només és vàlid post-init.
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo. const EnemyConfig* config_{nullptr};
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) // 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}; bool is_active_{false};
EnemyType type_{EnemyType::PENTAGON}; EnemyType type_{EnemyType::PENTAGON};
EnemyAnimation animation_; EnemyAnimation animation_;
// Comportamiento type-specific // Comportamiento type-specific
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección float tracking_timer_{0.0F};
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking) const Vec2* ship_position_{nullptr};
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5 float tracking_strength_{0.0F};
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección float direction_change_timer_{0.0F};
// Invulnerabilidad post-spawn // Invulnerabilidad post-spawn
float invulnerability_timer_{0.0F}; 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}; float wounded_timer_{0.0F};
bool wound_expired_this_frame_{false}; 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 // Métodos privados
void updateAnimation(float delta_time); void updateAnimation(float delta_time);
@@ -133,8 +142,8 @@ class Enemy : public Entities::Entity {
void behaviorSquare(float delta_time); void behaviorSquare(float delta_time);
void behaviorPinwheel(float delta_time); void behaviorPinwheel(float delta_time);
[[nodiscard]] auto computeCurrentScale() const -> float; [[nodiscard]] auto computeCurrentScale() const -> float;
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy. // Static: passa collision_radius com a param per no acoblar a *this.
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool; 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. // Helper: setear body_.velocity desde un ángulo y magnitud.
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL). // angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
+124
View File
@@ -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;
}
}
+62
View File
@@ -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>;
};
+62
View File
@@ -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);
}
+30
View File
@@ -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;
};
+9 -1
View File
@@ -15,6 +15,7 @@
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/system/scene_context.hpp" #include "core/system/scene_context.hpp"
#include "core/system/service_menu.hpp" #include "core/system/service_menu.hpp"
#include "game/entities/enemy_registry.hpp"
#include "game/entities/player_config.hpp" #include "game/entities/player_config.hpp"
#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"
@@ -64,6 +65,13 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
std::exit(EXIT_FAILURE); std::exit(EXIT_FAILURE);
} }
// Carregar les configuracions dels 3 enemics. Sense fallback: si falla,
// abortem (els enemics no es poden construir sense els seus paràmetres).
if (!EnemyRegistry::loadAll()) {
std::cerr << "[GameScene] FATAL: no s'han pogut carregar els enemics YAML\n";
std::exit(EXIT_FAILURE);
}
// Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual. // Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual.
ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard
ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales
@@ -773,7 +781,7 @@ void GameScene::tocado(uint8_t player_id) {
0.0F, // sense herència angular 0.0F, // sense herència angular
0.0F, // sin herencia visual 0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION2, Defaults::Sound::EXPLOSION2,
Defaults::Palette::SHIP, ships_[player_id].getConfig().colors.normal,
Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION, Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
+5 -30
View File
@@ -8,38 +8,13 @@
#include "core/physics/collision.hpp" #include "core/physics/collision.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/enemy_config.hpp"
namespace Systems::Collision { namespace Systems::Collision {
namespace { namespace {
constexpr uint8_t NO_SHOOTER = 0xFF; constexpr uint8_t NO_SHOOTER = 0xFF;
// Lookup tabla puntos / color por tipo de enemy (mantiene la lógica
// anterior pero centralizada para reutilizar entre paths).
auto scoreForType(EnemyType type) -> int {
switch (type) {
case EnemyType::PENTAGON:
return Defaults::Enemies::Scoring::PENTAGON_SCORE;
case EnemyType::SQUARE:
return Defaults::Enemies::Scoring::SQUARE_SCORE;
case EnemyType::PINWHEEL:
return Defaults::Enemies::Scoring::PINWHEEL_SCORE;
}
return 0;
}
auto colorForType(EnemyType type) -> SDL_Color {
switch (type) {
case EnemyType::PENTAGON:
return Defaults::Palette::PENTAGON;
case EnemyType::SQUARE:
return Defaults::Palette::SQUARE;
case EnemyType::PINWHEEL:
return Defaults::Palette::PINWHEEL;
}
return SDL_Color{};
}
// Mata al enemy con explosión: floating score, debris con velocity heredada, // Mata al enemy con explosión: floating score, debris con velocity heredada,
// sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador. // sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador.
// CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity). // CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity).
@@ -48,10 +23,10 @@ namespace Systems::Collision {
const Vec2 ENEMY_VEL = enemy.getVelocityVector(); const Vec2 ENEMY_VEL = enemy.getVelocityVector();
const float BRIGHTNESS = enemy.getBrightness(); const float BRIGHTNESS = enemy.getBrightness();
const auto SHAPE = enemy.getShape(); const auto SHAPE = enemy.getShape();
const EnemyType TYPE = enemy.getType();
const int POINTS = scoreForType(TYPE); const int POINTS = enemy.getConfig().score;
const SDL_Color COLOR = colorForType(TYPE); const SDL_Color COLOR = enemy.getConfig().colors.normal;
const SDL_Color WOUNDED_COLOR = enemy.getConfig().colors.wounded;
if (shooter_id != NO_SHOOTER) { if (shooter_id != NO_SHOOTER) {
ctx.score_per_player[shooter_id] += POINTS; ctx.score_per_player[shooter_id] += POINTS;
@@ -86,7 +61,7 @@ namespace Systems::Collision {
Defaults::FX::Firework::N_POINTS, Defaults::FX::Firework::N_POINTS,
Defaults::FX::Firework::INITIAL_BRIGHTNESS, Defaults::FX::Firework::INITIAL_BRIGHTNESS,
/*glow=*/true, /*glow=*/true,
Defaults::Palette::WOUNDED); WOUNDED_COLOR);
} }
// Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva. // Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva.