From 4b6dc8a47a135db749091829dae2b09fe5a6dbb1 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 25 May 2026 11:54:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(entities):=20migrar=20par=C3=A0metres=20co?= =?UTF-8?q?mpartits=20dels=20enemics=20a=20cada=20YAML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/entities/pentagon/pentagon.yaml | 43 ++++++-- data/entities/pinwheel/pinwheel.yaml | 39 +++++++- data/entities/square/square.yaml | 43 ++++++-- source/core/defaults/enemies.hpp | 70 +++---------- source/game/entities/enemy.cpp | 138 +++++++++++--------------- source/game/entities/enemy.hpp | 5 +- source/game/entities/enemy_config.cpp | 57 +++++++++++ source/game/entities/enemy_config.hpp | 43 ++++++++ 8 files changed, 281 insertions(+), 157 deletions(-) diff --git a/data/entities/pentagon/pentagon.yaml b/data/entities/pentagon/pentagon.yaml index c1d88e8..0749834 100644 --- a/data/entities/pentagon/pentagon.yaml +++ b/data/entities/pentagon/pentagon.yaml @@ -8,17 +8,48 @@ shape: 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 + 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 + restitution: 1.0 # rebot elàstic perfecte contra parets + linear_damping: 0.0 # manté velocitat (sense fricció) + angular_damping: 0.0 behavior: # Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon). - angle_change_max: 1.0 # rad — magnitud del canvi de direcció + angle_change_max: 1.0 # rad — magnitud del canvi de direcció zigzag_prob_per_second: 0.8 +animation: + pulse: # respiració d'escala aleatòria + trigger_prob_per_second: 0.01 + duration_min: 1.0 + duration_max: 3.0 + amplitude_min: 0.08 + amplitude_max: 0.20 + frequency_min: 1.5 + frequency_max: 3.0 + rotation_accel: # acceleració/desacceleració de rotació visual + trigger_prob_per_second: 0.02 + duration_min: 3.0 + duration_max: 8.0 + multiplier_min: 0.3 + multiplier_max: 4.0 + +wounded: + duration: 1.0 # segons en estat ferit abans d'explotar + blink_hz: 10.0 # parpelleig color normal ↔ wounded + +spawn: + invulnerability_duration: 3.0 + invulnerability_brightness_start: 0.3 + invulnerability_brightness_end: 0.7 + invulnerability_scale_start: 0.0 + invulnerability_scale_end: 1.0 + safety_distance: 36.0 # px mínim respecte al player al spawn + colors: - normal: [0, 255, 255] # Cyan pur "esquivador" - wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte) + 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 index 2f7bb42..9b38b5f 100644 --- a/data/entities/pinwheel/pinwheel.yaml +++ b/data/entities/pinwheel/pinwheel.yaml @@ -7,18 +7,49 @@ shape: collision_factor: 1.0 # ajust opcional del hitbox (default 1.0) 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 + 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 + restitution: 1.0 + linear_damping: 0.0 + angular_damping: 0.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) +animation: + pulse: + trigger_prob_per_second: 0.01 + duration_min: 1.0 + duration_max: 3.0 + amplitude_min: 0.08 + amplitude_max: 0.20 + frequency_min: 1.5 + frequency_max: 3.0 + rotation_accel: + trigger_prob_per_second: 0.02 + duration_min: 3.0 + duration_max: 8.0 + multiplier_min: 0.3 + multiplier_max: 4.0 + +wounded: + duration: 1.0 + blink_hz: 10.0 + +spawn: + invulnerability_duration: 3.0 + invulnerability_brightness_start: 0.3 + invulnerability_brightness_end: 0.7 + invulnerability_scale_start: 0.0 + invulnerability_scale_end: 1.0 + safety_distance: 36.0 + colors: - normal: [255, 0, 255] # Magenta pur "agressiu" + 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 index 7541453..23613ef 100644 --- a/data/entities/square/square.yaml +++ b/data/entities/square/square.yaml @@ -7,18 +7,49 @@ shape: collision_factor: 1.0 # ajust opcional del hitbox (default 1.0) physics: - mass: 8.0 # Més pesat — "tanc" - speed: 40.0 # px/s (velocitat mitjana) - rotation_delta_min: 0.3 # rad/s — rotació lenta + 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 + restitution: 1.0 + linear_damping: 0.0 + angular_damping: 0.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 + tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1) + tracking_interval: 1.0 # segons entre updates d'angle + +animation: + pulse: + trigger_prob_per_second: 0.01 + duration_min: 1.0 + duration_max: 3.0 + amplitude_min: 0.08 + amplitude_max: 0.20 + frequency_min: 1.5 + frequency_max: 3.0 + rotation_accel: + trigger_prob_per_second: 0.02 + duration_min: 3.0 + duration_max: 8.0 + multiplier_min: 0.3 + multiplier_max: 4.0 + +wounded: + duration: 1.0 + blink_hz: 10.0 + +spawn: + invulnerability_duration: 3.0 + invulnerability_brightness_start: 0.3 + invulnerability_brightness_end: 0.7 + invulnerability_scale_start: 0.0 + invulnerability_scale_end: 1.0 + safety_distance: 36.0 colors: - normal: [255, 0, 0] # Roig pur "tanc" + 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 2d00cd8..c838f26 100644 --- a/source/core/defaults/enemies.hpp +++ b/source/core/defaults/enemies.hpp @@ -1,64 +1,18 @@ -// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Square/Molinillo), spawn i scoring +// enemies.hpp - Constants tècniques compartides per al sistema d'enemics. // © 2026 JailDesigner +// +// Tots els paràmetres jugables (physics, animation, wounded, spawn, +// behavior, colors, scoring) viuen a data/entities//.yaml i +// s'accedeixen via EnemyRegistry::get(EnemyType). Aquí només queda el +// que no és per personalitzar per tipus. #pragma once -namespace Defaults::Enemies { +namespace Defaults::Enemies::Spawn { - // Cuerpo físico común (valores por defecto del constructor) - namespace Body { - constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0) - constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes - constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad - constexpr float ANGULAR_DAMPING = 0.0F; - } // namespace Body + // Sostre de reintents al cercar una posició de spawn que respecti el + // safety_distance del tipus. No és un paràmetre jugable: és el llindar + // tècnic abans de caure a un fallback aleatori amb advertència. + constexpr int MAX_SPAWN_ATTEMPTS = 50; - // 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 { - // Palpitation - constexpr float PULSE_TRIGGER_PROB = 0.01F; // 1% chance per second - constexpr float PULSE_DURATION_MIN = 1.0F; // Min duration (seconds) - constexpr float PULSE_DURATION_MAX = 3.0F; // Max duration (seconds) - constexpr float PULSE_AMPLITUD_MIN = 0.08F; // Min scale variation - constexpr float PULSE_AMPLITUD_MAX = 0.20F; // Max scale variation - constexpr float PULSE_FREQ_MIN = 1.5F; // Min frequency (Hz) - constexpr float PULSE_FREQ_MAX = 3.0F; // Max frequency (Hz) - - // Rotation acceleration - constexpr float ROTATION_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent] - constexpr float ROTATION_ACCEL_DURATION_MIN = 3.0F; // Min transition time - constexpr float ROTATION_ACCEL_DURATION_MAX = 8.0F; // Max transition time - constexpr float ROTATION_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic] - constexpr float ROTATION_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic] - } // namespace Animation - - // Wounded state (entre primer impacto y explosión) - namespace Wounded { - constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar - constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado - } // namespace Wounded - - // Spawn safety and invulnerability system - namespace Spawn { - // Safe spawn distance from player. Antic: SHIP_RADIUS(12) * 3 = 36 px. - // SHIP_RADIUS ha migrat al YAML del player; aquesta constant es - // mantindrà fixa fins al PR de migració dels enemics a YAML, on - // passarà a derivar-se en runtime del player_config. - constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; - constexpr float SAFETY_DISTANCE = 36.0F; - constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position - - // Invulnerability system - constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds - constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim - constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC) - constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible - constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size - } // namespace Spawn - -} // namespace Defaults::Enemies +} // namespace Defaults::Enemies::Spawn diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index 9e576a3..af40730 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -36,18 +36,25 @@ namespace { return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); } + // Random float [0..1). + auto randFloat01() -> float { + return static_cast(std::rand()) / static_cast(RAND_MAX); + } + + // Random float [min..max). + auto randRange(float min, float max) -> float { + return min + (randFloat01() * (max - min)); + } + } // namespace Enemy::Enemy(Rendering::Renderer* renderer) : Entity(renderer) { brightness_ = Defaults::Brightness::ENEMIC; - // 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; - body_.linear_damping = Defaults::Enemies::Body::LINEAR_DAMPING; - body_.angular_damping = Defaults::Enemies::Body::ANGULAR_DAMPING; + // Body queda amb defaults inocus (radius=0 = no col·lisiona) fins + // que init() apliqui la configuració del tipus carregada via Registry. + body_.radius = 0.0F; } void Enemy::init(EnemyType type, const Vec2* ship_pos) { @@ -55,7 +62,6 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { config_ = &EnemyRegistry::get(type); const EnemyConfig& cfg = *config_; - // Cas Square: resetejar tracking timer al spawn. if (type_ == EnemyType::SQUARE) { tracking_timer_ = 0.0F; tracking_strength_ = cfg.behavior.tracking_strength; @@ -66,14 +72,17 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n'; } - // Radi de col·lisió derivat del cercle circumscrit de la shape * scale * collision_factor. + // Radi de col·lisió derivat del cercle circumscrit de la shape. const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F; collision_radius_ = BOUNDING * cfg.shape.scale * cfg.shape.collision_factor; body_.setMass(cfg.physics.mass); body_.radius = collision_radius_; + body_.restitution = cfg.physics.restitution; + body_.linear_damping = cfg.physics.linear_damping; + body_.angular_damping = cfg.physics.angular_damping; - // Posición aleatoria con comprobación de seguridad + // Posició aleatòria amb comprovació de safety_distance. float min_x; float max_x; float min_y; @@ -85,7 +94,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { float candidate_x; float candidate_y; - if (attemptSafeSpawn(*ship_pos, collision_radius_, candidate_x, candidate_y)) { + if (attemptSafeSpawn(*ship_pos, collision_radius_, cfg.spawn.safety_distance, candidate_x, candidate_y)) { center_.x = candidate_x; center_.y = candidate_y; found_safe_position = true; @@ -107,8 +116,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { center_.y = static_cast((std::rand() % RANGE_Y) + static_cast(min_y)); } - // Dirección inicial aleatoria, velocidad escalar según tipo - const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F; + const float ANGLE_INICIAL = static_cast(std::rand() % 360) * Constants::PI / 180.0F; setVelocityFromAngle(ANGLE_INICIAL, cfg.physics.speed); body_.position = center_; @@ -116,10 +124,8 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { body_.angular_velocity = 0.0F; body_.clearAccumulators(); - // 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); + // Rotació visual aleatòria dins del rang del tipus + rotation_delta_ = randRange(cfg.physics.rotation_delta_min, cfg.physics.rotation_delta_max); rotation_ = 0.0F; animation_ = EnemyAnimation(); @@ -127,8 +133,8 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) { animation_.rotation_delta_target = rotation_delta_; animation_.rotation_delta_t = 1.0F; - invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; - brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; + invulnerability_timer_ = cfg.spawn.invulnerability_duration; + brightness_ = cfg.spawn.invulnerability_brightness_start; direction_change_timer_ = 0.0F; @@ -153,11 +159,11 @@ void Enemy::update(float delta_time) { invulnerability_timer_ -= delta_time; invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F); - const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; + const float T_INV = invulnerability_timer_ / config_->spawn.invulnerability_duration; const float T = 1.0F - T_INV; const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); - constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; - constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END; + const float START = config_->spawn.invulnerability_brightness_start; + const float END = config_->spawn.invulnerability_brightness_end; brightness_ = START + ((END - START) * SMOOTH_T); } @@ -190,13 +196,11 @@ void Enemy::draw() const { if (!is_active_ || !shape_) { return; } - // El SCALE final = escala base del YAML * modulador dinàmic (spawn/pulse). const float SCALE = config_->shape.scale * computeCurrentScale(); SDL_Color color = config_->colors.normal; - // Parpadeo dorado mientras está herido. if (wounded_timer_ > 0.0F) { - const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ; + const float CYCLE = 1.0F / config_->wounded.blink_hz; const float T = std::fmod(wounded_timer_, CYCLE); if (T < (CYCLE / 2.0F)) { color = config_->colors.wounded; @@ -217,7 +221,7 @@ void Enemy::destroy() { } void Enemy::hurt(uint8_t shooter_id) { - wounded_timer_ = Defaults::Enemies::Wounded::DURATION; + wounded_timer_ = config_->wounded.duration; last_hit_by_ = shooter_id; } @@ -230,7 +234,7 @@ void Enemy::setVelocity(float speed) { if (CURRENT_SPEED > 0.0F) { body_.velocity = body_.velocity * (speed / CURRENT_SPEED); } else { - const float A = (std::rand() % 360) * Constants::PI / 180.0F; + const float A = static_cast(std::rand() % 360) * Constants::PI / 180.0F; setVelocityFromAngle(A, speed); } } @@ -239,17 +243,14 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) { body_.velocity = angleToDirection(angle_movement) * speed; } -// PENTAGON: zigzag esquivador. Cambios de dirección periódicos (probabilísticos) -// en lugar de detectar paredes; el rebote contra muros lo hace PhysicsWorld -// con restitution=1.0. +// PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics) +// en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld. void Enemy::behaviorPentagon(float delta_time) { direction_change_timer_ += delta_time; - const float RAND_VAL = static_cast(std::rand()) / static_cast(RAND_MAX); - if (RAND_VAL < config_->behavior.zigzag_prob_per_second * delta_time) { + if (randFloat01() < 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)) * - config_->behavior.angle_change_max; + const float DELTA = randFloat01() * 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); @@ -257,8 +258,7 @@ void Enemy::behaviorPentagon(float delta_time) { } } -// SQUARE: tracking discreto cada tracking_interval. Ajusta dirección -// hacia el ship mezclando con tracking_strength_. +// SQUARE: tracking discret cap a la nau cada N segons. void Enemy::behaviorSquare(float delta_time) { tracking_timer_ += delta_time; @@ -283,7 +283,7 @@ void Enemy::behaviorSquare(float delta_time) { } } -// PINWHEEL: movimiento recto + boost de rotación visual cerca del ship. +// PINWHEEL: movement rectilini + boost de rotació visual prop del ship. void Enemy::behaviorPinwheel(float /*delta_time*/) { if (ship_position_ != nullptr) { const Vec2 TO_SHIP = *ship_position_ - center_; @@ -302,38 +302,26 @@ void Enemy::updateAnimation(float delta_time) { } void Enemy::updatePulse(float delta_time) { + const auto& cfg = config_->animation.pulse; if (animation_.pulse_active) { animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time; animation_.pulse_time_remaining -= delta_time; if (animation_.pulse_time_remaining <= 0.0F) { animation_.pulse_active = false; } - } else { - const float RAND_VAL = static_cast(std::rand()) / static_cast(RAND_MAX); - const float TRIGGER_PROB = Defaults::Enemies::Animation::PULSE_TRIGGER_PROB * delta_time; - if (RAND_VAL < TRIGGER_PROB) { - animation_.pulse_active = true; - animation_.pulse_phase = 0.0F; - - const float FREQ_RANGE = Defaults::Enemies::Animation::PULSE_FREQ_MAX - - Defaults::Enemies::Animation::PULSE_FREQ_MIN; - animation_.pulse_frequency = Defaults::Enemies::Animation::PULSE_FREQ_MIN + - ((static_cast(std::rand()) / static_cast(RAND_MAX)) * FREQ_RANGE); - - const float AMP_RANGE = Defaults::Enemies::Animation::PULSE_AMPLITUD_MAX - - Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN; - animation_.pulse_amplitude = Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN + - ((static_cast(std::rand()) / static_cast(RAND_MAX)) * AMP_RANGE); - - const float DUR_RANGE = Defaults::Enemies::Animation::PULSE_DURATION_MAX - - Defaults::Enemies::Animation::PULSE_DURATION_MIN; - animation_.pulse_time_remaining = Defaults::Enemies::Animation::PULSE_DURATION_MIN + - ((static_cast(std::rand()) / static_cast(RAND_MAX)) * DUR_RANGE); - } + return; + } + if (randFloat01() < cfg.trigger_prob_per_second * delta_time) { + animation_.pulse_active = true; + animation_.pulse_phase = 0.0F; + animation_.pulse_frequency = randRange(cfg.frequency_min, cfg.frequency_max); + animation_.pulse_amplitude = randRange(cfg.amplitude_min, cfg.amplitude_max); + animation_.pulse_time_remaining = randRange(cfg.duration_min, cfg.duration_max); } } void Enemy::updateRotationAcceleration(float delta_time) { + const auto& cfg = config_->animation.rotation_accel; if (animation_.rotation_delta_t < 1.0F) { animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration; if (animation_.rotation_delta_t >= 1.0F) { @@ -347,34 +335,24 @@ void Enemy::updateRotationAcceleration(float delta_time) { const float TARGET = animation_.rotation_delta_target; rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T); } - } else { - const float RAND_VAL = static_cast(std::rand()) / static_cast(RAND_MAX); - const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTATION_ACCEL_TRIGGER_PROB * delta_time; - if (RAND_VAL < TRIGGER_PROB) { - animation_.rotation_delta_t = 0.0F; - - const float MULT_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MAX - - Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN; - const float MULTIPLIER = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN + - ((static_cast(std::rand()) / static_cast(RAND_MAX)) * MULT_RANGE); - animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER; - - const float DUR_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MAX - - Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN; - animation_.rotation_delta_duration = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN + - ((static_cast(std::rand()) / static_cast(RAND_MAX)) * DUR_RANGE); - } + return; + } + if (randFloat01() < cfg.trigger_prob_per_second * delta_time) { + animation_.rotation_delta_t = 0.0F; + const float MULTIPLIER = randRange(cfg.multiplier_min, cfg.multiplier_max); + animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER; + animation_.rotation_delta_duration = randRange(cfg.duration_min, cfg.duration_max); } } auto Enemy::computeCurrentScale() const -> float { float scale = 1.0F; if (invulnerability_timer_ > 0.0F) { - const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; + const float T_INV = invulnerability_timer_ / config_->spawn.invulnerability_duration; const float T = 1.0F - T_INV; const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); - constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START; - constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END; + const float START = config_->spawn.invulnerability_scale_start; + const float END = config_->spawn.invulnerability_scale_end; scale = START + ((END - START) * SMOOTH_T); } else if (animation_.pulse_active) { scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase); @@ -396,7 +374,7 @@ void Enemy::setTrackingStrength(float strength) { } } -auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float& out_x, float& out_y) -> bool { +auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool { float min_x; float max_x; float min_y; @@ -411,5 +389,5 @@ auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float const float DX = out_x - ship_pos.x; const float DY = out_y - ship_pos.y; const float DISTANCE = std::sqrt((DX * DX) + (DY * DY)); - return DISTANCE >= Defaults::Enemies::Spawn::SAFETY_DISTANCE; + return DISTANCE >= safety_distance; } diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index 0847354..a803911 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -6,7 +6,6 @@ #include -#include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/types.hpp" @@ -142,8 +141,8 @@ class Enemy : public Entities::Entity { void behaviorSquare(float delta_time); void behaviorPinwheel(float delta_time); [[nodiscard]] auto computeCurrentScale() const -> float; - // 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; + // Static: passa els paràmetres com a args per no acoblar a *this. + static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, 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 index c699391..c0b28b7 100644 --- a/source/game/entities/enemy_config.cpp +++ b/source/game/entities/enemy_config.cpp @@ -80,6 +80,60 @@ namespace { out.speed = p["speed"].get_value(); out.rotation_delta_min = p["rotation_delta_min"].get_value(); out.rotation_delta_max = p["rotation_delta_max"].get_value(); + out.restitution = p["restitution"].get_value(); + out.linear_damping = p["linear_damping"].get_value(); + out.angular_damping = p["angular_damping"].get_value(); + return true; + } + + auto parseAnimation(const fkyaml::node& node, const std::string& name, EnemyConfig::AnimationCfg& out) -> bool { + if (!node.contains("animation") || + !node["animation"].contains("pulse") || + !node["animation"].contains("rotation_accel")) { + std::cerr << "[EnemyConfig] Error: falta 'animation.pulse' o 'animation.rotation_accel' a " << name << '\n'; + return false; + } + const auto& p = node["animation"]["pulse"]; + out.pulse.trigger_prob_per_second = p["trigger_prob_per_second"].get_value(); + out.pulse.duration_min = p["duration_min"].get_value(); + out.pulse.duration_max = p["duration_max"].get_value(); + out.pulse.amplitude_min = p["amplitude_min"].get_value(); + out.pulse.amplitude_max = p["amplitude_max"].get_value(); + out.pulse.frequency_min = p["frequency_min"].get_value(); + out.pulse.frequency_max = p["frequency_max"].get_value(); + + const auto& r = node["animation"]["rotation_accel"]; + out.rotation_accel.trigger_prob_per_second = r["trigger_prob_per_second"].get_value(); + out.rotation_accel.duration_min = r["duration_min"].get_value(); + out.rotation_accel.duration_max = r["duration_max"].get_value(); + out.rotation_accel.multiplier_min = r["multiplier_min"].get_value(); + out.rotation_accel.multiplier_max = r["multiplier_max"].get_value(); + return true; + } + + auto parseWounded(const fkyaml::node& node, const std::string& name, EnemyConfig::WoundedCfg& out) -> bool { + if (!node.contains("wounded")) { + std::cerr << "[EnemyConfig] Error: falta 'wounded' a " << name << '\n'; + return false; + } + const auto& w = node["wounded"]; + out.duration = w["duration"].get_value(); + out.blink_hz = w["blink_hz"].get_value(); + return true; + } + + auto parseSpawn(const fkyaml::node& node, const std::string& name, EnemyConfig::SpawnCfg& out) -> bool { + if (!node.contains("spawn")) { + std::cerr << "[EnemyConfig] Error: falta 'spawn' a " << name << '\n'; + return false; + } + const auto& s = node["spawn"]; + out.invulnerability_duration = s["invulnerability_duration"].get_value(); + out.invulnerability_brightness_start = s["invulnerability_brightness_start"].get_value(); + out.invulnerability_brightness_end = s["invulnerability_brightness_end"].get_value(); + out.invulnerability_scale_start = s["invulnerability_scale_start"].get_value(); + out.invulnerability_scale_end = s["invulnerability_scale_end"].get_value(); + out.safety_distance = s["safety_distance"].get_value(); return true; } @@ -134,6 +188,9 @@ auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type) if (!parseShape(node, cfg.name, cfg.shape)) { return std::nullopt; } if (!parsePhysics(node, cfg.name, cfg.physics)) { return std::nullopt; } parseBehavior(node, cfg.behavior); + if (!parseAnimation(node, cfg.name, cfg.animation)) { return std::nullopt; } + if (!parseWounded(node, cfg.name, cfg.wounded)) { return std::nullopt; } + if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; } if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; } if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; } diff --git a/source/game/entities/enemy_config.hpp b/source/game/entities/enemy_config.hpp index 29b748d..8b6f152 100644 --- a/source/game/entities/enemy_config.hpp +++ b/source/game/entities/enemy_config.hpp @@ -27,6 +27,9 @@ struct EnemyConfig { float speed; float rotation_delta_min; float rotation_delta_max; + float restitution; // rebot contra parets (1.0 = perfectament elàstic) + float linear_damping; // fricció lineal (s^-1) + float angular_damping; }; // Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F @@ -43,6 +46,43 @@ struct EnemyConfig { float proximity_distance{0.0F}; }; + // Animacions decoratives. Compartides estructuralment entre tots els tipus + // però amb valors propis per personalitzar la "personalitat" visual de cada un. + struct AnimationCfg { + struct PulseCfg { + float trigger_prob_per_second; // probabilitat per segon d'iniciar un pulse + float duration_min; + float duration_max; + float amplitude_min; // amplitud d'escala (±) + float amplitude_max; + float frequency_min; // Hz + float frequency_max; + }; + struct RotationAccelCfg { + float trigger_prob_per_second; + float duration_min; // segons de transició al nou speed + float duration_max; + float multiplier_min; // multiplicador sobre rotation_delta_base + float multiplier_max; + }; + PulseCfg pulse; + RotationAccelCfg rotation_accel; + }; + + struct WoundedCfg { + float duration; // segons en estat ferit abans d'explotar + float blink_hz; // freqüència del parpelleig color normal ↔ wounded + }; + + struct SpawnCfg { + float invulnerability_duration; // segons d'invulnerabilitat post-spawn + float invulnerability_brightness_start; // brightness inicial (corba LERP) + float invulnerability_brightness_end; // brightness final + float invulnerability_scale_start; // escala inicial (corba LERP, 0 = invisible) + float invulnerability_scale_end; // escala final (1 = mida nativa) + float safety_distance; // px mínim respecte al player al spawn + }; + struct ColorsCfg { SDL_Color normal; SDL_Color wounded; @@ -53,6 +93,9 @@ struct EnemyConfig { ShapeCfg shape; PhysicsCfg physics; BehaviorCfg behavior; + AnimationCfg animation; + WoundedCfg wounded; + SpawnCfg spawn; ColorsCfg colors; int score;