diff --git a/data/entities/pentagon/pentagon.yaml b/data/entities/pentagon/pentagon.yaml new file mode 100644 index 0000000..328f8ef --- /dev/null +++ b/data/entities/pentagon/pentagon.yaml @@ -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 diff --git a/data/entities/pinwheel/pinwheel.yaml b/data/entities/pinwheel/pinwheel.yaml new file mode 100644 index 0000000..fe79082 --- /dev/null +++ b/data/entities/pinwheel/pinwheel.yaml @@ -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 diff --git a/data/entities/square/square.yaml b/data/entities/square/square.yaml new file mode 100644 index 0000000..e781a76 --- /dev/null +++ b/data/entities/square/square.yaml @@ -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 diff --git a/source/core/defaults/enemies.hpp b/source/core/defaults/enemies.hpp index 5d34216..2d00cd8 100644 --- a/source/core/defaults/enemies.hpp +++ b/source/core/defaults/enemies.hpp @@ -13,41 +13,10 @@ namespace Defaults::Enemies { constexpr float ANGULAR_DAMPING = 0.0F; } // namespace Body - // Pentagon (esquivador - zigzag evasion) - namespace Pentagon { - constexpr float SPEED = 35.0F; // px/s (slightly slower) - constexpr float MASS = 5.0F; // Masa estándar - 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 + // NOTA: els paràmetres per tipus (Pentagon/Square/Pinwheel) i el Scoring + // viuen ara a data/entities/{pentagon,square,pinwheel}/*.yaml i s'accedeixen + // via EnemyRegistry::get(EnemyType). Aquí només queden els paràmetres + // compartits entre tots els tipus (animació, wounded, spawn). // Animation parameters (shared) namespace Animation { @@ -92,11 +61,4 @@ namespace Defaults::Enemies { constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size } // 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 diff --git a/source/core/defaults/entities.hpp b/source/core/defaults/entities.hpp index 274dedf..3de3424 100644 --- a/source/core/defaults/entities.hpp +++ b/source/core/defaults/entities.hpp @@ -9,7 +9,7 @@ namespace Defaults::Entities { constexpr int MAX_BULLETS = 50; // SHIP_RADIUS migrat a data/entities/player/player.yaml (physics.collision_radius). - constexpr float ENEMY_RADIUS = 20.0F; + // ENEMY_RADIUS migrat a data/entities//.yaml (physics.collision_radius). constexpr float BULLET_RADIUS = 3.0F; } // namespace Defaults::Entities diff --git a/source/core/defaults/palette.hpp b/source/core/defaults/palette.hpp index 310ea8c..42158a3 100644 --- a/source/core/defaults/palette.hpp +++ b/source/core/defaults/palette.hpp @@ -14,11 +14,10 @@ namespace Defaults::Palette { // 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ó // 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 PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador" - constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank" - constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu" - constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido + // SHIP s'ha migrat a data/entities/player/player.yaml (colors.normal). + // PENTAGON, SQUARE, PINWHEEL i WOUNDED han migrat a cada enemy YAML + // (colors.normal i colors.wounded). + // BULLET es queda compartit fins a la migració del bullet a YAML. } // namespace Defaults::Palette diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index 3cd8948..0e1a4cc 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -14,6 +14,8 @@ #include "core/rendering/shape_renderer.hpp" #include "core/types.hpp" #include "game/constants.hpp" +#include "game/entities/enemy_config.hpp" +#include "game/entities/enemy_registry.hpp" namespace { @@ -27,25 +29,20 @@ namespace { } // Recupera el "ángulo equivalente" de un body en movimiento (para zigzag). - // Si está parado, devuelve 0. auto velocityToAngle(const Vec2& velocity) -> float { if (velocity.lengthSquared() < 0.0001F) { return 0.0F; } - // El movimiento (vx, vy) corresponde a angle - PI/2; invertimos. return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); } } // namespace Enemy::Enemy(Rendering::Renderer* renderer) - : Entity(renderer), - - tracking_strength_(Defaults::Enemies::Square::TRACKING_STRENGTH) { + : Entity(renderer) { brightness_ = Defaults::Brightness::ENEMIC; - // Configuración del cuerpo físico — defaults para enemy genérico. - // init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo). + // Cuerpo físico — defaults comuns; init() ajusta mass/radius segons el tipus. body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS); body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo) body_.restitution = Defaults::Enemies::Body::RESTITUTION; @@ -55,56 +52,23 @@ Enemy::Enemy(Rendering::Renderer* renderer) void Enemy::init(EnemyType type, const Vec2* ship_pos) { type_ = type; + config_ = &EnemyRegistry::get(type); + const EnemyConfig& cfg = *config_; - const char* shape_file = nullptr; - float base_speed = 0.0F; - float rotation_delta_min = 0.0F; - float rotation_delta_max = 0.0F; - float type_mass = Defaults::Enemies::Body::DEFAULT_MASS; + collision_radius_ = cfg.physics.collision_radius; - switch (type_) { - case EnemyType::PENTAGON: - shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; - base_speed = Defaults::Enemies::Pentagon::SPEED; - rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN; - rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX; - type_mass = Defaults::Enemies::Pentagon::MASS; - break; - - case EnemyType::SQUARE: - shape_file = Defaults::Enemies::Square::SHAPE_FILE; - base_speed = Defaults::Enemies::Square::SPEED; - rotation_delta_min = Defaults::Enemies::Square::ROTATION_DELTA_MIN; - rotation_delta_max = Defaults::Enemies::Square::ROTATION_DELTA_MAX; - type_mass = Defaults::Enemies::Square::MASS; - tracking_timer_ = 0.0F; - break; - - case EnemyType::PINWHEEL: - shape_file = Defaults::Enemies::Pinwheel::SHAPE_FILE; - base_speed = Defaults::Enemies::Pinwheel::SPEED; - rotation_delta_min = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MIN; - rotation_delta_max = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MAX; - type_mass = Defaults::Enemies::Pinwheel::MASS; - break; - - default: - std::cerr << "[Enemy] Error: tipo desconocido (" - << static_cast(type_) << "), usando PENTAGON\n"; - shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; - base_speed = Defaults::Enemies::Pentagon::SPEED; - rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN; - rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX; - break; + // Cas Square: resetejar tracking timer al spawn. + if (type_ == EnemyType::SQUARE) { + tracking_timer_ = 0.0F; + tracking_strength_ = cfg.behavior.tracking_strength; } - body_.setMass(type_mass); - body_.radius = Defaults::Entities::ENEMY_RADIUS; + body_.setMass(cfg.physics.mass); + body_.radius = collision_radius_; - // Cargar shape - shape_ = Graphics::ShapeLoader::load(shape_file); + shape_ = Graphics::ShapeLoader::load(cfg.shape.path); if (!shape_ || !shape_->isValid()) { - std::cerr << "[Enemy] Error: no se ha podido cargar " << shape_file << '\n'; + std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n'; } // Posición aleatoria con comprobación de seguridad @@ -112,14 +76,14 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { float max_x; float min_y; float max_y; - Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); + Constants::getSafePlayAreaBounds(collision_radius_, min_x, max_x, min_y, max_y); if (ship_pos != nullptr) { bool found_safe_position = false; for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { float candidate_x; float candidate_y; - if (attemptSafeSpawn(*ship_pos, candidate_x, candidate_y)) { + if (attemptSafeSpawn(*ship_pos, collision_radius_, candidate_x, candidate_y)) { center_.x = candidate_x; center_.y = candidate_y; found_safe_position = true; @@ -143,30 +107,27 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { // Dirección inicial aleatoria, velocidad escalar según tipo const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F; - setVelocityFromAngle(ANGLE_INICIAL, base_speed); + setVelocityFromAngle(ANGLE_INICIAL, cfg.physics.speed); - // Sincronizar body_ con posición inicial body_.position = center_; body_.angle = 0.0F; body_.angular_velocity = 0.0F; body_.clearAccumulators(); - // Rotación visual aleatoria (independiente del body) - const float ROTATION_DELTA_RANGE = rotation_delta_max - rotation_delta_min; - rotation_delta_ = rotation_delta_min + ((static_cast(std::rand()) / static_cast(RAND_MAX)) * ROTATION_DELTA_RANGE); + // Rotación visual aleatoria dins del rang del tipus + const float ROTATION_RANGE = cfg.physics.rotation_delta_max - cfg.physics.rotation_delta_min; + rotation_delta_ = cfg.physics.rotation_delta_min + + ((static_cast(std::rand()) / static_cast(RAND_MAX)) * ROTATION_RANGE); rotation_ = 0.0F; - // Estado de animación animation_ = EnemyAnimation(); animation_.rotation_delta_base = rotation_delta_; animation_.rotation_delta_target = rotation_delta_; animation_.rotation_delta_t = 1.0F; - // Invulnerabilidad post-spawn invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; - // Timer para próximo cambio de dirección (Pentagon) direction_change_timer_ = 0.0F; is_active_ = true; @@ -177,8 +138,6 @@ void Enemy::update(float delta_time) { return; } - // Decremento de timer "herido"; al cruzar 0 marca expiración para que el - // system layer dispare la explosión diferida. wound_expired_this_frame_ = false; if (wounded_timer_ > 0.0F) { wounded_timer_ -= delta_time; @@ -188,7 +147,6 @@ void Enemy::update(float delta_time) { } } - // Decremento de invulnerabilidad + LERP de brightness if (invulnerability_timer_ > 0.0F) { invulnerability_timer_ -= delta_time; invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F); @@ -201,9 +159,6 @@ void Enemy::update(float delta_time) { brightness_ = START + ((END - START) * SMOOTH_T); } - // Comportamiento por tipo (ajusta body_.velocity, NO mueve posición). - // Skip cuando está herido: el enemy és un "cos mort" inert, només - // respon a la inèrcia del impulse rebut i a les col·lisions físiques. if (!isWounded()) { switch (type_) { case EnemyType::PENTAGON: @@ -218,15 +173,12 @@ void Enemy::update(float delta_time) { } } - // Animaciones (palpitación + rotación acelerada) updateAnimation(delta_time); - // Rotación visual (decoración, no afecta movimiento) rotation_ += rotation_delta_ * delta_time; } void Enemy::postUpdate(float /*delta_time*/) { - // Sincronizar mirror tras la integración del world. if (is_active_) { center_ = body_.position; } @@ -237,26 +189,14 @@ void Enemy::draw() const { return; } const float SCALE = computeCurrentScale(); - SDL_Color color{}; - switch (type_) { - case EnemyType::PENTAGON: - color = Defaults::Palette::PENTAGON; - break; - case EnemyType::SQUARE: - color = Defaults::Palette::SQUARE; - break; - case EnemyType::PINWHEEL: - color = Defaults::Palette::PINWHEEL; - break; - } + SDL_Color color = config_->colors.normal; - // Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado - // a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo). + // Parpadeo dorado mientras está herido. if (wounded_timer_ > 0.0F) { const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ; const float T = std::fmod(wounded_timer_, CYCLE); if (T < (CYCLE / 2.0F)) { - color = Defaults::Palette::WOUNDED; + color = config_->colors.wounded; } } @@ -267,7 +207,7 @@ void Enemy::destroy() { is_active_ = false; body_.velocity = Vec2{}; body_.angular_velocity = 0.0F; - body_.radius = 0.0F; // No colisiona mientras está inactivo + body_.radius = 0.0F; wounded_timer_ = 0.0F; wound_expired_this_frame_ = false; last_hit_by_ = 0xFF; @@ -276,8 +216,6 @@ void Enemy::destroy() { void Enemy::hurt(uint8_t shooter_id) { wounded_timer_ = Defaults::Enemies::Wounded::DURATION; last_hit_by_ = shooter_id; - // El so HIT ara el reprodueix la bala quan es trenca en debris - // (Systems::Collision::breakBullet), no l'enemic en entrar a HURT. } void Enemy::applyImpulse(const Vec2& impulse) { @@ -285,12 +223,10 @@ void Enemy::applyImpulse(const Vec2& impulse) { } void Enemy::setVelocity(float speed) { - // Mantener la dirección actual del body, cambiar solo la magnitud. const float CURRENT_SPEED = body_.velocity.length(); if (CURRENT_SPEED > 0.0F) { body_.velocity = body_.velocity * (speed / CURRENT_SPEED); } else { - // Sin dirección actual: usar ángulo aleatorio const float A = (std::rand() % 360) * Constants::PI / 180.0F; setVelocityFromAngle(A, speed); } @@ -306,13 +242,11 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) { void Enemy::behaviorPentagon(float delta_time) { direction_change_timer_ += delta_time; - // Probabilidad de zigzag por segundo (calibrada para sensación equivalente - // a la versión vieja que disparaba en cada toque de pared). const float RAND_VAL = static_cast(std::rand()) / static_cast(RAND_MAX); - if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) { + if (RAND_VAL < config_->behavior.zigzag_prob_per_second * delta_time) { const float CURRENT_ANGLE = velocityToAngle(body_.velocity); const float DELTA = (static_cast(std::rand()) / static_cast(RAND_MAX)) * - Defaults::Enemies::Pentagon::ANGLE_CHANGE_MAX; + config_->behavior.angle_change_max; const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA); const float SPEED = body_.velocity.length(); setVelocityFromAngle(NEW_ANGLE, SPEED); @@ -320,12 +254,12 @@ void Enemy::behaviorPentagon(float delta_time) { } } -// SQUARE: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección +// SQUARE: tracking discreto cada tracking_interval. Ajusta dirección // hacia el ship mezclando con tracking_strength_. void Enemy::behaviorSquare(float delta_time) { tracking_timer_ += delta_time; - if (tracking_timer_ >= Defaults::Enemies::Square::TRACKING_INTERVAL && ship_position_ != nullptr) { + if (tracking_timer_ >= config_->behavior.tracking_interval && ship_position_ != nullptr) { tracking_timer_ = 0.0F; const Vec2 TO_SHIP = *ship_position_ - center_; @@ -335,11 +269,9 @@ void Enemy::behaviorSquare(float delta_time) { const float SPEED = body_.velocity.length(); const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED; - // Mezcla LERP: velocidad actual con la deseada según tracking_strength_. body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) + (DESIRED_VEL * tracking_strength_); - // Renormalizar a la velocidad escalar original const float NEW_SPEED = body_.velocity.length(); if (NEW_SPEED > 0.0F) { body_.velocity = body_.velocity * (SPEED / NEW_SPEED); @@ -349,20 +281,16 @@ void Enemy::behaviorSquare(float delta_time) { } // PINWHEEL: movimiento recto + boost de rotación visual cerca del ship. -// Sin tracking — solo cambios de dirección raros (igual que Pentagon pero -// con probabilidad mucho menor). void Enemy::behaviorPinwheel(float /*delta_time*/) { - // Boost de rotación visual por proximidad al ship if (ship_position_ != nullptr) { const Vec2 TO_SHIP = *ship_position_ - center_; const float DIST = TO_SHIP.length(); - if (DIST < Defaults::Enemies::Pinwheel::PROXIMITY_DISTANCE) { - rotation_delta_ = animation_.rotation_delta_base * Defaults::Enemies::Pinwheel::ROTATION_DELTA_PROXIMITY_MULTIPLIER; + if (DIST < config_->behavior.proximity_distance) { + rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier; } else { rotation_delta_ = animation_.rotation_delta_base; } } - // Movimiento lineal puro: el world se encarga de integrar y rebotar. } void Enemy::updateAnimation(float delta_time) { @@ -452,16 +380,7 @@ auto Enemy::computeCurrentScale() const -> float { } auto Enemy::getBaseVelocity() const -> float { - switch (type_) { - case EnemyType::PENTAGON: - return Defaults::Enemies::Pentagon::SPEED; - case EnemyType::SQUARE: - return Defaults::Enemies::Square::SPEED; - case EnemyType::PINWHEEL: - return Defaults::Enemies::Pinwheel::SPEED; - default: - return Defaults::Enemies::Pentagon::SPEED; - } + return EnemyRegistry::get(type_).physics.speed; } auto Enemy::getBaseRotation() const -> float { @@ -474,12 +393,12 @@ void Enemy::setTrackingStrength(float strength) { } } -auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool { +auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float& out_x, float& out_y) -> bool { float min_x; float max_x; float min_y; float max_y; - Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); + Constants::getSafePlayAreaBounds(collision_radius, min_x, max_x, min_y, max_y); const int RANGE_X = static_cast(max_x - min_x); const int RANGE_Y = static_cast(max_y - min_y); diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index 76d6ae3..0847354 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -17,6 +17,9 @@ enum class EnemyType : uint8_t { PINWHEEL = 2 // Molinillo agresivo (rápido, girando) }; +// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp. +struct EnemyConfig; + // Estado de animación (palpitación + rotación acelerada) struct EnemyAnimation { // Palpitación (efecto respiración) @@ -48,10 +51,8 @@ class Enemy : public Entities::Entity { // Override: Interfaz de Entity [[nodiscard]] auto isActive() const -> bool override { return is_active_; } - // Override: Interfaz de colisión - [[nodiscard]] auto getCollisionRadius() const -> float override { - return Defaults::Entities::ENEMY_RADIUS; - } + // Override: Interfaz de colisión. El radi ve del config carregat per tipus. + [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; } // Mentre fa spawn (invulnerable) segueix col·lisionant: les bales el // poden abatre i el cos físic rebota amb la nau. El damage a la nau // segueix filtrat per `isInvulnerable()` al detectShipEnemy. @@ -65,6 +66,9 @@ class Enemy : public Entities::Entity { // Getters [[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; } [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } + // Configuració activa (carregada al darrer init()). Vàlida mentre l'enemic + // ha estat inicialitzat almenys un cop; abans és nullptr. + [[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; } // Set ship position reference for tracking behavior void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } @@ -101,29 +105,34 @@ class Enemy : public Entities::Entity { void applyImpulse(const Vec2& impulse); private: - // Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_). - // Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo - // como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo. - float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity - float rotation_{0.0F}; // Rotación visual acumulada (no afecta movimiento) + // Configuració carregada per tipus (apunta a una entrada de EnemyRegistry). + // nullptr abans del primer init(); per això getConfig() només és vàlid post-init. + const EnemyConfig* config_{nullptr}; + + // Cache local del radi (per evitar dereferenciar config_ a getCollisionRadius); + // s'actualitza a init() segons el tipus. + float collision_radius_{0.0F}; + + float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s) + float rotation_{0.0F}; // Rotación visual acumulada bool is_active_{false}; EnemyType type_{EnemyType::PENTAGON}; EnemyAnimation animation_; // Comportamiento type-specific - float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección - const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking) - float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5 - float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección + float tracking_timer_{0.0F}; + const Vec2* ship_position_{nullptr}; + float tracking_strength_{0.0F}; + float direction_change_timer_{0.0F}; // Invulnerabilidad post-spawn float invulnerability_timer_{0.0F}; - // Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración. + // Estado "herido" float wounded_timer_{0.0F}; bool wound_expired_this_frame_{false}; - uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución + uint8_t last_hit_by_{0xFF}; // Métodos privados void updateAnimation(float delta_time); @@ -133,8 +142,8 @@ class Enemy : public Entities::Entity { void behaviorSquare(float delta_time); void behaviorPinwheel(float delta_time); [[nodiscard]] auto computeCurrentScale() const -> float; - // Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy. - static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool; + // Static: passa collision_radius com a param per no acoblar a *this. + static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float& out_x, float& out_y) -> bool; // Helper: setear body_.velocity desde un ángulo y magnitud. // angle_movement=0 apunta hacia arriba (eje Y negativo SDL). diff --git a/source/game/entities/enemy_config.cpp b/source/game/entities/enemy_config.cpp new file mode 100644 index 0000000..509f5d9 --- /dev/null +++ b/source/game/entities/enemy_config.cpp @@ -0,0 +1,124 @@ +// enemy_config.cpp - Implementació del parser de EnemyConfig +// © 2026 JailDesigner + +#include "game/entities/enemy_config.hpp" + +#include +#include +#include +#include + +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(); + const auto G = node[1].get_value(); + const auto B = node[2].get_value(); + out = SDL_Color{ + .r = static_cast(R), + .g = static_cast(G), + .b = static_cast(B), + .a = 255}; + return true; + } + + auto aiTypeFromString(const std::string& s) -> std::optional { + 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 { + try { + EnemyConfig cfg; + + cfg.name = node.contains("name") ? node["name"].get_value() : "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(); + 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(); + + // 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(); + cfg.physics.speed = physics["speed"].get_value(); + cfg.physics.rotation_delta_min = physics["rotation_delta_min"].get_value(); + cfg.physics.rotation_delta_max = physics["rotation_delta_max"].get_value(); + cfg.physics.collision_radius = physics["collision_radius"].get_value(); + + // 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(); + } + if (b.contains("angle_change_max")) { + cfg.behavior.angle_change_max = b["angle_change_max"].get_value(); + } + if (b.contains("tracking_strength")) { + cfg.behavior.tracking_strength = b["tracking_strength"].get_value(); + } + if (b.contains("tracking_interval")) { + cfg.behavior.tracking_interval = b["tracking_interval"].get_value(); + } + if (b.contains("rotation_proximity_multiplier")) { + cfg.behavior.rotation_proximity_multiplier = b["rotation_proximity_multiplier"].get_value(); + } + if (b.contains("proximity_distance")) { + cfg.behavior.proximity_distance = b["proximity_distance"].get_value(); + } + } + + // 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(); + + return cfg; + } catch (const std::exception& e) { + std::cerr << "[EnemyConfig] Excepció parsejant: " << e.what() << '\n'; + return std::nullopt; + } +} diff --git a/source/game/entities/enemy_config.hpp b/source/game/entities/enemy_config.hpp new file mode 100644 index 0000000..3292981 --- /dev/null +++ b/source/game/entities/enemy_config.hpp @@ -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 + +#include +#include + +#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; +}; diff --git a/source/game/entities/enemy_registry.cpp b/source/game/entities/enemy_registry.cpp new file mode 100644 index 0000000..ceee87b --- /dev/null +++ b/source/game/entities/enemy_registry.cpp @@ -0,0 +1,62 @@ +// enemy_registry.cpp - Implementació del registre estàtic d'enemics +// © 2026 JailDesigner + +#include "game/entities/enemy_registry.hpp" + +#include +#include +#include + +#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); +} diff --git a/source/game/entities/enemy_registry.hpp b/source/game/entities/enemy_registry.hpp new file mode 100644 index 0000000..3f4e8a4 --- /dev/null +++ b/source/game/entities/enemy_registry.hpp @@ -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; +}; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 4333e87..92fc924 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -15,6 +15,7 @@ #include "core/locale/locale.hpp" #include "core/system/scene_context.hpp" #include "core/system/service_menu.hpp" +#include "game/entities/enemy_registry.hpp" #include "game/entities/player_config.hpp" #include "game/stage_system/stage_loader.hpp" #include "game/systems/collision_system.hpp" @@ -64,6 +65,13 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) 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. 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 @@ -773,7 +781,7 @@ void GameScene::tocado(uint8_t player_id) { 0.0F, // sense herència angular 0.0F, // sin herencia visual Defaults::Sound::EXPLOSION2, - Defaults::Palette::SHIP, + ships_[player_id].getConfig().colors.normal, Defaults::Physics::Debris::ENEMY_LIFETIME, Defaults::Physics::Debris::ENEMY_FRICTION, Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index 885a8a3..730220c 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -8,38 +8,13 @@ #include "core/physics/collision.hpp" #include "core/types.hpp" #include "game/constants.hpp" +#include "game/entities/enemy_config.hpp" namespace Systems::Collision { namespace { 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, // sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador. // 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 float BRIGHTNESS = enemy.getBrightness(); const auto SHAPE = enemy.getShape(); - const EnemyType TYPE = enemy.getType(); - const int POINTS = scoreForType(TYPE); - const SDL_Color COLOR = colorForType(TYPE); + const int POINTS = enemy.getConfig().score; + const SDL_Color COLOR = enemy.getConfig().colors.normal; + const SDL_Color WOUNDED_COLOR = enemy.getConfig().colors.wounded; if (shooter_id != NO_SHOOTER) { ctx.score_per_player[shooter_id] += POINTS; @@ -86,7 +61,7 @@ namespace Systems::Collision { Defaults::FX::Firework::N_POINTS, Defaults::FX::Firework::INITIAL_BRIGHTNESS, /*glow=*/true, - Defaults::Palette::WOUNDED); + WOUNDED_COLOR); } // Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva.