feat(entities): migrar la configuració dels 3 enemics a data/entities/<type>/*.yaml
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user