diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index d0f5bc1..6e1e731 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -1,4 +1,4 @@ -// enemy.cpp - Implementació de enemigos (ORNIs) +// enemy.cpp - Implementación de enemigos (ORNIs) // © 1999 Visente i Sergi (versión Pascal) // © 2025 Port a C++20 con SDL3 @@ -16,488 +16,399 @@ #include "core/types.hpp" #include "game/constants.hpp" +namespace { + +// Velocidad inicial vectorial a partir de un ángulo (rad). +// angle=0 apunta hacia arriba (eje Y negativo SDL), como el resto del juego. +auto angleToDirection(float angle) -> Vec2 { + return Vec2{ + .x = std::cos(angle - (Constants::PI / 2.0F)), + .y = std::sin(angle - (Constants::PI / 2.0F)), + }; +} + +// 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(SDL_Renderer* renderer) : Entity(renderer), - velocity_(0.0F), drotacio_(0.0F), rotacio_(0.0F), esta_(false), type_(EnemyType::PENTAGON), tracking_timer_(0.0F), ship_position_(nullptr), - tracking_strength_(0.5F), // Default tracking strength - timer_invulnerabilitat_(0.0F) { // Start vulnerable - // [NUEVO] Brightness específic per enemigos + tracking_strength_(0.5F), + direction_change_timer_(0.0F), + timer_invulnerabilitat_(0.0F) { brightness_ = Defaults::Brightness::ENEMIC; - // [NUEVO] Forma es carrega a init() segons el type - // Constructor no carrega shape per permetre type diferents + // Configuración del cuerpo físico — defaults para enemy genérico. + // init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo). + body_.setMass(5.0F); // Más liviano que la nave (10.0) + body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo) + body_.restitution = 1.0F; // Rebote elástico perfecto contra paredes + body_.linear_damping = 0.0F; // Sin fricción: mantienen velocidad + body_.angular_damping = 0.0F; // Idem } void Enemy::init(EnemyType type, const Vec2* ship_pos) { - // Guardar type type_ = type; - // Carregar shape segons el type - const char* shape_file; - float drotacio_min; - float drotacio_max; + const char* shape_file = nullptr; + float base_speed = 0.0F; + float drotacio_min = 0.0F; + float drotacio_max = 0.0F; + float type_mass = 5.0F; switch (type_) { case EnemyType::PENTAGON: shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; - velocity_ = Defaults::Enemies::Pentagon::VELOCITAT; + base_speed = Defaults::Enemies::Pentagon::VELOCITAT; drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; + type_mass = 5.0F; break; case EnemyType::QUADRAT: shape_file = Defaults::Enemies::Cuadrado::SHAPE_FILE; - velocity_ = Defaults::Enemies::Cuadrado::VELOCITAT; + base_speed = Defaults::Enemies::Cuadrado::VELOCITAT; drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX; + type_mass = 8.0F; // Más pesado, "tanque" tracking_timer_ = 0.0F; break; case EnemyType::MOLINILLO: shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE; - velocity_ = Defaults::Enemies::Molinillo::VELOCITAT; + base_speed = Defaults::Enemies::Molinillo::VELOCITAT; drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX; + type_mass = 4.0F; // Más liviano, ágil break; default: - // Fallback segur: usar valors de PENTAGON - std::cerr << "[Enemy] Error: type desconegut (" - << static_cast(type_) << "), utilitzant PENTAGON\n"; + std::cerr << "[Enemy] Error: tipo desconocido (" + << static_cast(type_) << "), usando PENTAGON\n"; shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; - velocity_ = Defaults::Enemies::Pentagon::VELOCITAT; + base_speed = Defaults::Enemies::Pentagon::VELOCITAT; drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; break; } - // Carregar shape + body_.setMass(type_mass); + body_.radius = Defaults::Entities::ENEMY_RADIUS; + + // Cargar shape shape_ = Graphics::ShapeLoader::load(shape_file); if (!shape_ || !shape_->isValid()) { - std::cerr << "[Enemy] Error: no s'ha pogut load " << shape_file << '\n'; + std::cerr << "[Enemy] Error: no se ha podido cargar " << shape_file << '\n'; } - // [MODIFIED] Posición aleatòria con comprobación de seguretat + // Posición aleatoria con comprobación de seguridad float min_x; float max_x; float min_y; float max_y; - Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, - min_x, - max_x, - min_y, - max_y); + Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); if (ship_pos != nullptr) { - // [NEW] Safe spawn: attempt to find position away from ship bool found_safe_position = false; - for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { float candidate_x; float candidate_y; - - if (intent_spawn_safe(*ship_pos, candidate_x, candidate_y)) { + if (attemptSafeSpawn(*ship_pos, candidate_x, candidate_y)) { center_.x = candidate_x; center_.y = candidate_y; found_safe_position = true; break; } } - if (!found_safe_position) { - // Fallback: spawn anywhere (user's preference) - int range_x = static_cast(max_x - min_x); - int range_y = static_cast(max_y - min_y); - center_.x = static_cast((std::rand() % range_x) + static_cast(min_x)); - center_.y = static_cast((std::rand() % range_y) + static_cast(min_y)); - - std::cout << "[Enemy] Advertència: spawn sin zona segura después de " - << Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intents" << '\n'; + const int RANGE_X = static_cast(max_x - min_x); + const int RANGE_Y = static_cast(max_y - min_y); + center_.x = static_cast((std::rand() % RANGE_X) + static_cast(min_x)); + center_.y = static_cast((std::rand() % RANGE_Y) + static_cast(min_y)); + std::cout << "[Enemy] Advertencia: spawn sin zona segura tras " + << Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intentos\n"; } } else { - // [EXISTING] No ship position: spawn anywhere (backward compatibility) - int range_x = static_cast(max_x - min_x); - int range_y = static_cast(max_y - min_y); - center_.x = static_cast((std::rand() % range_x) + static_cast(min_x)); - center_.y = static_cast((std::rand() % range_y) + static_cast(min_y)); + const int RANGE_X = static_cast(max_x - min_x); + const int RANGE_Y = static_cast(max_y - min_y); + center_.x = static_cast((std::rand() % RANGE_X) + static_cast(min_x)); + center_.y = static_cast((std::rand() % RANGE_Y) + static_cast(min_y)); } - // Angle aleatori de movement - angle_ = (std::rand() % 360) * Constants::PI / 180.0F; + // 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); - // Rotación visual aleatòria (rad/s) dins del rang del type - float drotacio_range = drotacio_max - drotacio_min; - drotacio_ = drotacio_min + ((static_cast(std::rand()) / RAND_MAX) * drotacio_range); + // 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 DROTACIO_RANGE = drotacio_max - drotacio_min; + drotacio_ = drotacio_min + ((static_cast(std::rand()) / RAND_MAX) * DROTACIO_RANGE); rotacio_ = 0.0F; - // Inicialitzar state de animación - animacio_ = EnemyAnimation(); // Reset to defaults + // Estado de animación + animacio_ = EnemyAnimation(); animacio_.drotacio_base = drotacio_; animacio_.drotacio_objetivo = drotacio_; - animacio_.drotacio_t = 1.0F; // Start without interpolating + animacio_.drotacio_t = 1.0F; - // [NEW] Inicialitzar invulnerabilitat - timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; // 3.0s - brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; // 0.3f + // Invulnerabilidad post-spawn + timer_invulnerabilitat_ = 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; - // Activar esta_ = true; } void Enemy::update(float delta_time) { + if (!esta_) { + return; + } + + // Decremento de invulnerabilidad + LERP de brightness + if (timer_invulnerabilitat_ > 0.0F) { + timer_invulnerabilitat_ -= delta_time; + timer_invulnerabilitat_ = std::max(timer_invulnerabilitat_, 0.0F); + + const float T_INV = timer_invulnerabilitat_ / Defaults::Enemies::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; + brightness_ = START + ((END - START) * SMOOTH_T); + } + + // Comportamiento por tipo (ajusta body_.velocity, NO mueve posición) + switch (type_) { + case EnemyType::PENTAGON: + behaviorPentagon(delta_time); + break; + case EnemyType::QUADRAT: + behaviorQuadrat(delta_time); + break; + case EnemyType::MOLINILLO: + behaviorMolinillo(delta_time); + break; + } + + // Animaciones (palpitación + rotación acelerada) + updateAnimation(delta_time); + + // Rotación visual (decoración, no afecta movimiento) + rotacio_ += drotacio_ * delta_time; +} + +void Enemy::postUpdate(float /*delta_time*/) { + // Sincronizar mirror tras la integración del world. if (esta_) { - // [NEW] Update invulnerability timer and brightness - if (timer_invulnerabilitat_ > 0.0F) { - timer_invulnerabilitat_ -= delta_time; - - timer_invulnerabilitat_ = std::max(timer_invulnerabilitat_, 0.0F); - - // [NEW] Update brightness with LERP during invulnerability - float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; - float t = 1.0F - t_inv; // 0.0 → 1.0 - float smooth_t = t * t * (3.0F - 2.0F * t); // smoothstep - - constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; - constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END; - brightness_ = START + ((END - START) * smooth_t); - } - - // Moviment autònom - mou(delta_time); - - // Actualitzar animaciones (palpitació, rotación accelerada) - actualitzar_animacio(delta_time); - - // Rotación visual (time-based: drotacio_ está en rad/s) - rotacio_ += drotacio_ * delta_time; + center_ = body_.position; } } void Enemy::draw() const { - if (esta_ && shape_) { - // Calculate animated scale (includes invulnerability LERP) - float scale = calcular_escala_actual(); - - // brightness_ is already updated in update() - Rendering::render_shape(renderer_, shape_, center_, rotacio_, scale, 1.0F, brightness_); + if (!esta_ || !shape_) { + return; } + const float SCALE = computeCurrentScale(); + Rendering::render_shape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_); } -void Enemy::mou(float delta_time) { - // Dispatcher: crida el comportament específic segons el type - switch (type_) { - case EnemyType::PENTAGON: - comportament_pentagon(delta_time); - break; - case EnemyType::QUADRAT: - comportament_quadrat(delta_time); - break; - case EnemyType::MOLINILLO: - comportament_molinillo(delta_time); - break; - default: - // Fallback: comportament bàsic (Pentagon) - comportament_pentagon(delta_time); - break; - } +void Enemy::destruir() { + esta_ = false; + body_.velocity = Vec2{}; + body_.angular_velocity = 0.0F; + body_.radius = 0.0F; // No colisiona mientras está inactivo } -void Enemy::comportament_pentagon(float delta_time) { - // Pentagon: zigzag esquivador (frequent direction changes) - // Similar a comportament original pero con probabilitat més alta - - float velocitat_efectiva = velocity_ * delta_time; - - // Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt) - float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F)); - float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F)); - - float new_y = center_.y + dy; - float new_x = center_.x + dx; - - // Obtenir límits segurs - float min_x; - float max_x; - float min_y; - float max_y; - Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, - min_x, - max_x, - min_y, - max_y); - - // Zigzag: canvi de angle més freqüent en tocar límits - if (new_y >= min_y && new_y <= max_y) { - center_.y = new_y; +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 { - // Probabilitat més alta de canvi de angle - if (static_cast(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) { - float rand_angle = (static_cast(std::rand()) / RAND_MAX) * - Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX; - angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle; - } - } - - if (new_x >= min_x && new_x <= max_x) { - center_.x = new_x; - } else { - if (static_cast(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) { - float rand_angle = (static_cast(std::rand()) / RAND_MAX) * - Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX; - angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle; - } + // Sin dirección actual: usar ángulo aleatorio + const float A = (std::rand() % 360) * Constants::PI / 180.0F; + setVelocityFromAngle(A, speed); } } -void Enemy::comportament_quadrat(float delta_time) { - // Cuadrado: perseguidor (tracks player position) +void Enemy::setVelocityFromAngle(float angle_movement, float speed) { + body_.velocity = angleToDirection(angle_movement) * speed; +} - // Update tracking timer +// 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. +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). + constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; + const float RAND_VAL = static_cast(std::rand()) / RAND_MAX; + if (RAND_VAL < ZIGZAG_PROB_PER_SECOND * delta_time) { + const float CURRENT_ANGLE = velocityToAngle(body_.velocity); + const float DELTA = (static_cast(std::rand()) / RAND_MAX) * + Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX; + const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA); + const float SPEED = body_.velocity.length(); + setVelocityFromAngle(NEW_ANGLE, SPEED); + direction_change_timer_ = 0.0F; + } +} + +// QUADRAT: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección +// hacia el ship mezclando con tracking_strength_. +void Enemy::behaviorQuadrat(float delta_time) { tracking_timer_ += delta_time; - // Periodically update angle toward ship - if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL) { + if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL && ship_position_ != nullptr) { tracking_timer_ = 0.0F; - if (ship_position_ != nullptr) { - // Calculate angle to ship - float dx = ship_position_->x - center_.x; - float dy = ship_position_->y - center_.y; - float target_angle = std::atan2(dy, dx) + (Constants::PI / 2.0F); + const Vec2 TO_SHIP = *ship_position_ - center_; + const float DIST = TO_SHIP.length(); + if (DIST > 0.0F) { + const Vec2 DESIRED_DIR = TO_SHIP / DIST; + const float SPEED = body_.velocity.length(); + const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED; - // Interpolate toward target angle - float angle_diff = target_angle - angle_; + // Mezcla LERP: velocidad actual con la deseada según tracking_strength_. + body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) + + (DESIRED_VEL * tracking_strength_); - // Normalize angle difference to [-π, π] - while (angle_diff > Constants::PI) { - angle_diff -= 2.0F * Constants::PI; + // 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); } - while (angle_diff < -Constants::PI) { - angle_diff += 2.0F * Constants::PI; - } - - // Apply tracking strength (uses member variable, defaults to 0.5) - angle_ += angle_diff * tracking_strength_; } } - - // Move in current direction - float velocitat_efectiva = velocity_ * delta_time; - float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F)); - float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F)); - - float new_y = center_.y + dy; - float new_x = center_.x + dx; - - // Obtenir límits segurs - float min_x; - float max_x; - float min_y; - float max_y; - Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, - min_x, - max_x, - min_y, - max_y); - - // Bounce on walls (simple reflection) - if (new_y >= min_y && new_y <= max_y) { - center_.y = new_y; - } else { - angle_ = -angle_; // Vertical reflection - } - - if (new_x >= min_x && new_x <= max_x) { - center_.x = new_x; - } else { - angle_ = Constants::PI - angle_; // Horizontal reflection - } } -void Enemy::comportament_molinillo(float delta_time) { - // Molinillo: agressiu (fast, straight lines, proximity spin-up) - - // Check proximity to ship for spin-up effect +// MOLINILLO: 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::behaviorMolinillo(float /*delta_time*/) { + // Boost de rotación visual por proximidad al ship if (ship_position_ != nullptr) { - float dx = ship_position_->x - center_.x; - float dy = ship_position_->y - center_.y; - float distance = std::sqrt((dx * dx) + (dy * dy)); - - if (distance < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) { - // Temporarily boost rotation speed when near ship - float boost = Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER; - drotacio_ = animacio_.drotacio_base * boost; + const Vec2 TO_SHIP = *ship_position_ - center_; + const float DIST = TO_SHIP.length(); + if (DIST < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) { + drotacio_ = animacio_.drotacio_base * Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER; } else { - // Normal rotation speed drotacio_ = animacio_.drotacio_base; } } - - // Fast straight-line movement - float velocitat_efectiva = velocity_ * delta_time; - float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F)); - float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F)); - - float new_y = center_.y + dy; - float new_x = center_.x + dx; - - // Obtenir límits segurs - float min_x; - float max_x; - float min_y; - float max_y; - Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, - min_x, - max_x, - min_y, - max_y); - - // Rare angle changes on wall hits - if (new_y >= min_y && new_y <= max_y) { - center_.y = new_y; - } else { - if (static_cast(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) { - float rand_angle = (static_cast(std::rand()) / RAND_MAX) * - Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX; - angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle; - } - } - - if (new_x >= min_x && new_x <= max_x) { - center_.x = new_x; - } else { - if (static_cast(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) { - float rand_angle = (static_cast(std::rand()) / RAND_MAX) * - Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX; - angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle; - } - } + // Movimiento lineal puro: el world se encarga de integrar y rebotar. } -void Enemy::actualitzar_animacio(float delta_time) { - actualitzar_palpitacio(delta_time); - actualitzar_rotacio_accelerada(delta_time); +void Enemy::updateAnimation(float delta_time) { + updatePalpitation(delta_time); + updateRotationAcceleration(delta_time); } -void Enemy::actualitzar_palpitacio(float delta_time) { +void Enemy::updatePalpitation(float delta_time) { if (animacio_.palpitacio_activa) { - // Advance phase (2π * frequency * dt) animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time; - - // Decrement timer animacio_.palpitacio_temps_restant -= delta_time; - - // Deactivate when timer expires if (animacio_.palpitacio_temps_restant <= 0.0F) { animacio_.palpitacio_activa = false; } } else { - // Random trigger (probability per second) - float rand_val = static_cast(std::rand()) / RAND_MAX; - float trigger_prob = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time; - - if (rand_val < trigger_prob) { - // Activate palpitation + const float RAND_VAL = static_cast(std::rand()) / RAND_MAX; + const float TRIGGER_PROB = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time; + if (RAND_VAL < TRIGGER_PROB) { animacio_.palpitacio_activa = true; animacio_.palpitacio_fase = 0.0F; - // Randomize parameters - float freq_range = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX - - Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN; + const float FREQ_RANGE = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX - + Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN; animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN + - ((static_cast(std::rand()) / RAND_MAX) * freq_range); + ((static_cast(std::rand()) / RAND_MAX) * FREQ_RANGE); - float amp_range = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX - - Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN; + const float AMP_RANGE = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX - + Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN; animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN + - ((static_cast(std::rand()) / RAND_MAX) * amp_range); + ((static_cast(std::rand()) / RAND_MAX) * AMP_RANGE); - float dur_range = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX - - Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN; + const float DUR_RANGE = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX - + Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN; animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN + - ((static_cast(std::rand()) / RAND_MAX) * dur_range); + ((static_cast(std::rand()) / RAND_MAX) * DUR_RANGE); } } } -void Enemy::actualitzar_rotacio_accelerada(float delta_time) { +void Enemy::updateRotationAcceleration(float delta_time) { if (animacio_.drotacio_t < 1.0F) { - // Transitioning to new target animacio_.drotacio_t += delta_time / animacio_.drotacio_duracio; - if (animacio_.drotacio_t >= 1.0F) { animacio_.drotacio_t = 1.0F; - animacio_.drotacio_base = animacio_.drotacio_objetivo; // Reached target + animacio_.drotacio_base = animacio_.drotacio_objetivo; drotacio_ = animacio_.drotacio_base; } else { - // Smoothstep interpolation: t² * (3 - 2t) - float t = animacio_.drotacio_t; - float smooth_t = t * t * (3.0F - 2.0F * t); - - // Interpolate between base and target - float initial = animacio_.drotacio_base; - float target = animacio_.drotacio_objetivo; - drotacio_ = initial + ((target - initial) * smooth_t); + const float T = animacio_.drotacio_t; + const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); + const float INITIAL = animacio_.drotacio_base; + const float TARGET = animacio_.drotacio_objetivo; + drotacio_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T); } } else { - // Random trigger for new acceleration - float rand_val = static_cast(std::rand()) / RAND_MAX; - float trigger_prob = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time; - - if (rand_val < trigger_prob) { - // Start new transition + const float RAND_VAL = static_cast(std::rand()) / RAND_MAX; + const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time; + if (RAND_VAL < TRIGGER_PROB) { animacio_.drotacio_t = 0.0F; - // Randomize target speed (multiplier * base) - float mult_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX - - Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN; - float multiplier = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN + - ((static_cast(std::rand()) / RAND_MAX) * mult_range); + const float MULT_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX - + Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN; + const float MULTIPLIER = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN + + ((static_cast(std::rand()) / RAND_MAX) * MULT_RANGE); + animacio_.drotacio_objetivo = animacio_.drotacio_base * MULTIPLIER; - animacio_.drotacio_objetivo = animacio_.drotacio_base * multiplier; - - // Randomize duration - float dur_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX - - Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN; + const float DUR_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX - + Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN; animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN + - ((static_cast(std::rand()) / RAND_MAX) * dur_range); + ((static_cast(std::rand()) / RAND_MAX) * DUR_RANGE); } } } -float Enemy::calcular_escala_actual() const { +auto Enemy::computeCurrentScale() const -> float { float scale = 1.0F; - - // [NEW] Invulnerability LERP prioritza sobre palpitació if (timer_invulnerabilitat_ > 0.0F) { - // Calculate t: 0.0 at spawn → 1.0 at end - float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; - float t = 1.0F - t_inv; // 0.0 → 1.0 - - // Apply smoothstep: t² * (3 - 2t) - float smooth_t = t * t * (3.0F - 2.0F * t); - - // LERP scale from 0.0 to 1.0 + const float T_INV = timer_invulnerabilitat_ / Defaults::Enemies::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; - scale = START + ((END - START) * smooth_t); + scale = START + ((END - START) * SMOOTH_T); } else if (animacio_.palpitacio_activa) { - // [EXISTING] Palpitació solo cuando no invulnerable scale += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase); } - return scale; } -// [NEW] Stage system API implementations - -float Enemy::get_base_velocity() const { +auto Enemy::getBaseVelocity() const -> float { switch (type_) { case EnemyType::PENTAGON: return Defaults::Enemies::Pentagon::VELOCITAT; @@ -506,46 +417,34 @@ float Enemy::get_base_velocity() const { case EnemyType::MOLINILLO: return Defaults::Enemies::Molinillo::VELOCITAT; default: - return Defaults::Enemies::Pentagon::VELOCITAT; // Fallback segur + return Defaults::Enemies::Pentagon::VELOCITAT; } } -float Enemy::get_base_rotation() const { - // Return the base rotation speed (drotacio_base if available, otherwise current drotacio_) +auto Enemy::getBaseRotation() const -> float { return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_; } -void Enemy::set_tracking_strength(float strength) { - // Only applies to QUADRAT type +void Enemy::setTrackingStrength(float strength) { if (type_ == EnemyType::QUADRAT) { tracking_strength_ = strength; } } -// [NEW] Safe spawn helper - checks if position is away from ship -bool Enemy::intent_spawn_safe(const Vec2& ship_pos, float& out_x, float& out_y) { - // Generate random position within safe bounds +auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool { float min_x; float max_x; float min_y; float max_y; - Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, - min_x, - max_x, - min_y, - max_y); + Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); - int range_x = static_cast(max_x - min_x); - int range_y = static_cast(max_y - min_y); + const int RANGE_X = static_cast(max_x - min_x); + const int RANGE_Y = static_cast(max_y - min_y); + out_x = static_cast((std::rand() % RANGE_X) + static_cast(min_x)); + out_y = static_cast((std::rand() % RANGE_Y) + static_cast(min_y)); - out_x = static_cast((std::rand() % range_x) + static_cast(min_x)); - out_y = static_cast((std::rand() % range_y) + static_cast(min_y)); - - // Check Euclidean distance to ship - float dx = out_x - ship_pos.x; - float dy = out_y - ship_pos.y; - float distance = std::sqrt((dx * dx) + (dy * dy)); - - // Return true if position is safe (>= 36px from ship) - return distance >= Defaults::Enemies::Spawn::SAFETY_DISTANCE; + 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; } diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index acbd292..ccfebaf 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -1,39 +1,37 @@ -// enemy.hpp - Clase para enemigos (ORNIs pentágonos) +// enemy.hpp - Clase para enemigos (ORNIs) // © 1999 Visente i Sergi (versión Pascal) // © 2025 Port a C++20 con SDL3 #pragma once #include -#include #include #include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/types.hpp" -#include "game/constants.hpp" // Tipo de enemy enum class EnemyType : uint8_t { PENTAGON = 0, // Pentágono esquivador (zigzag) QUADRAT = 1, // Cuadrado perseguidor (tracks ship) - MOLINILLO = 2 // Molinillo agressiu (fast, spinning) + MOLINILLO = 2 // Molinillo agresivo (rápido, girando) }; -// Estat de animación (palpitació i rotación accelerada) +// Estado de animación (palpitación + rotación acelerada) struct EnemyAnimation { - // Palpitation (breathing effect) + // Palpitación (efecto respiración) bool palpitacio_activa = false; - float palpitacio_fase = 0.0F; // Phase in cycle (0.0-2π) - float palpitacio_frequencia = 2.0F; // Hz (cycles per second) - float palpitacio_amplitud = 0.15F; // Scale variation (±15%) - float palpitacio_temps_restant = 0.0F; // Time remaining (seconds) + float palpitacio_fase = 0.0F; + float palpitacio_frequencia = 2.0F; + float palpitacio_amplitud = 0.15F; + float palpitacio_temps_restant = 0.0F; - // Rotation acceleration (long-term spin modulation) - float drotacio_base = 0.0F; // Base rotation speed (rad/s) - float drotacio_objetivo = 0.0F; // Target rotation speed (rad/s) - float drotacio_t = 0.0F; // Interpolation progress (0.0-1.0) - float drotacio_duracio = 0.0F; // Duration of transition (seconds) + // Aceleración de rotación visual (modulación a largo plazo) + float drotacio_base = 0.0F; + float drotacio_objetivo = 0.0F; + float drotacio_t = 0.0F; + float drotacio_duracio = 0.0F; }; class Enemy : public Entities::Entity { @@ -45,79 +43,78 @@ class Enemy : public Entities::Entity { void init() override { init(EnemyType::PENTAGON, nullptr); } void init(EnemyType type, const Vec2* ship_pos = nullptr); void update(float delta_time) override; + void postUpdate(float delta_time) override; void draw() const override; - // Override: Interfície d'Entity - [[nodiscard]] bool isActive() const override { return esta_; } + // Override: Interfaz de Entity + [[nodiscard]] auto isActive() const -> bool override { return esta_; } - // Override: Interfície de colisión - [[nodiscard]] float getCollisionRadius() const override { + // Override: Interfaz de colisión + [[nodiscard]] auto getCollisionRadius() const -> float override { return Defaults::Entities::ENEMY_RADIUS; } - [[nodiscard]] bool isCollidable() const override { + [[nodiscard]] auto isCollidable() const -> bool override { return esta_ && timer_invulnerabilitat_ <= 0.0F; } - // Getters (API pública sin canvis) - void destruir() { esta_ = false; } - [[nodiscard]] float get_drotacio() const { return drotacio_; } - [[nodiscard]] Vec2 getVelocityVector() const { - return { - .x = velocity_ * std::cos(angle_ - (Constants::PI / 2.0F)), - .y = velocity_ * std::sin(angle_ - (Constants::PI / 2.0F))}; - } + // Marcar destruido (desactiva el cuerpo físicamente: radius=0) + void destruir(); + + // Getters + [[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; } + [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } // Set ship position reference for tracking behavior - void set_ship_position(const Vec2* ship_pos) { ship_position_ = ship_pos; } + void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } - // [NEW] Getters for stage system (base stats) - [[nodiscard]] float get_base_velocity() const; - [[nodiscard]] float get_base_rotation() const; - [[nodiscard]] EnemyType getType() const { return type_; } + // Stage system API (base stats) + [[nodiscard]] auto getBaseVelocity() const -> float; + [[nodiscard]] auto getBaseRotation() const -> float; + [[nodiscard]] auto getType() const -> EnemyType { return type_; } - // [NEW] Setters for difficulty multipliers (stage system) - void set_velocity(float vel) { velocity_ = vel; } - void set_rotation(float rot) { + // Setters para multiplicadores de dificultad (stage system). + // Establecen la velocidad escalar deseada manteniendo la dirección + // actual del body_.velocity. + void setVelocity(float speed); + void setRotation(float rot) { drotacio_ = rot; animacio_.drotacio_base = rot; } - void set_tracking_strength(float strength); + void setTrackingStrength(float strength); - // [NEW] Invulnerability queries - [[nodiscard]] bool isInvulnerable() const { return timer_invulnerabilitat_ > 0.0F; } - [[nodiscard]] float get_temps_invulnerabilitat() const { return timer_invulnerabilitat_; } + // Invulnerabilidad + [[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; } + [[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; } private: - // Membres específics d'Enemy (heretats: renderer_, shape_, center_, angle_, brightness_) - float velocity_; - float drotacio_; // Delta rotación visual (rad/s) - float rotacio_; // Rotación visual acumulada + // Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_) + float drotacio_; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity + float rotacio_; // Rotación visual acumulada (no afecta movimiento) bool esta_; - // [NEW] Enemy type and configuration EnemyType type_; - - // [NEW] Animation state EnemyAnimation animacio_; - // [NEW] Behavior state (type-specific) - float tracking_timer_; // For Cuadrado: time since last angle update - const Vec2* ship_position_; // Pointer to ship position (for tracking) - float tracking_strength_; // For Cuadrado: tracking intensity (0.0-1.5), default 0.5 + // Comportamiento type-specific + float tracking_timer_; // Quadrat: tiempo desde último update de dirección + const Vec2* ship_position_; // Puntero a posición de la nave (para tracking) + float tracking_strength_; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5 + float direction_change_timer_; // Pentagon: tiempo para próximo cambio de dirección - // [NEW] Invulnerability state - float timer_invulnerabilitat_; // Countdown timer (seconds), 0.0f = vulnerable + // Invulnerabilidad post-spawn + float timer_invulnerabilitat_; - // [EXISTING] Private methods - void mou(float delta_time); + // Métodos privados + void updateAnimation(float delta_time); + void updatePalpitation(float delta_time); + void updateRotationAcceleration(float delta_time); + void behaviorPentagon(float delta_time); + void behaviorQuadrat(float delta_time); + void behaviorMolinillo(float delta_time); + [[nodiscard]] auto computeCurrentScale() const -> float; + auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool; - // [NEW] Private methods - void actualitzar_animacio(float delta_time); - void actualitzar_palpitacio(float delta_time); - void actualitzar_rotacio_accelerada(float delta_time); - void comportament_pentagon(float delta_time); - void comportament_quadrat(float delta_time); - void comportament_molinillo(float delta_time); - [[nodiscard]] float calcular_escala_actual() const; // Returns scale with palpitation applied - bool intent_spawn_safe(const Vec2& ship_pos, float& out_x, float& out_y); + // Helper: setear body_.velocity desde un ángulo y magnitud. + // angle_movement=0 apunta hacia arriba (eje Y negativo SDL). + void setVelocityFromAngle(float angle_movement, float speed); }; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 3686cec..ccc40ef 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -150,7 +150,7 @@ void GameScene::init() { stage_manager_->init(); // [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking) - stage_manager_->getSpawnController().set_ship_position(&ships_[0].getCenter()); + stage_manager_->getSpawnController().setShipPosition(&ships_[0].getCenter()); // Inicialitzar timers de muerte per player hit_timer_per_player_[0] = 0.0F; @@ -195,10 +195,13 @@ void GameScene::init() { } } - // [MODIFIED] Initialize enemies as inactive (stage system will spawn them) + // [MODIFIED] Initialize enemies as inactive (stage system will spawn them). + // Registramos el body al world incluso inactivo: con radius=0 no colisiona + // ni se mueve, y al init() del stage system se activa sin re-registrar. for (auto& enemy : enemies_) { enemy = Enemy(sdl_.getRenderer()); - enemy.set_ship_position(&ships_[0].getCenter()); // Set ship reference (P1 for now) + enemy.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now) + physics_world_.addBody(&enemy.getBody()); // DON'T call enemy.init() here - stage system handles spawning } @@ -224,6 +227,9 @@ void GameScene::update(float delta_time) { for (auto& ship : ships_) { ship.postUpdate(delta_time); } + for (auto& enemy : enemies_) { + enemy.postUpdate(delta_time); + } // Processar disparos (state-based, no event-based) if (game_over_state_ == GameOverState::NONE) { @@ -1009,7 +1015,7 @@ void GameScene::detectar_col·lisions_bales_enemics() { VELOCITAT_EXPLOSIO, // 50 px/s (explosión suau) enemy.getBrightness(), // Heredar brightness vel_enemic, // Heredar velocity - enemy.get_drotacio(), // Heredar velocity angular (trayectorias curvas) + enemy.getRotationDelta(), // Heredar velocity angular (trayectorias curvas) 0.0F // Sin herencia visual (rotación aleatoria) ); diff --git a/source/game/stage_system/spawn_controller.cpp b/source/game/stage_system/spawn_controller.cpp index cd687e2..2f415e1 100644 --- a/source/game/stage_system/spawn_controller.cpp +++ b/source/game/stage_system/spawn_controller.cpp @@ -163,15 +163,15 @@ void SpawnController::aplicar_multiplicadors(Enemy& enemy) const { } // Apply velocity multiplier - float base_vel = enemy.get_base_velocity(); - enemy.set_velocity(base_vel * config_->multiplicadors.velocity); + float base_vel = enemy.getBaseVelocity(); + enemy.setVelocity(base_vel * config_->multiplicadors.velocity); // Apply rotation multiplier - float base_rot = enemy.get_base_rotation(); - enemy.set_rotation(base_rot * config_->multiplicadors.rotation); + float base_rot = enemy.getBaseRotation(); + enemy.setRotation(base_rot * config_->multiplicadors.rotation); // Apply tracking strength (only affects QUADRAT) - enemy.set_tracking_strength(config_->multiplicadors.tracking_strength); + enemy.setTrackingStrength(config_->multiplicadors.tracking_strength); } } // namespace StageSystem diff --git a/source/game/stage_system/spawn_controller.hpp b/source/game/stage_system/spawn_controller.hpp index 5f5cde6..cc60115 100644 --- a/source/game/stage_system/spawn_controller.hpp +++ b/source/game/stage_system/spawn_controller.hpp @@ -39,7 +39,7 @@ class SpawnController { [[nodiscard]] uint8_t get_enemics_spawnejats() const; // [NEW] Set ship position reference for safe spawn - void set_ship_position(const Vec2* ship_pos) { ship_position_ = ship_pos; } + void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } private: const StageConfig* config_; // Non-owning pointer to current stage config