// enemy.cpp - Implementación de enemigos (ORNIs) // © 2026 JailDesigner #include "game/entities/enemy.hpp" #include #include #include #include #include "core/audio/audio.hpp" #include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/graphics/shape_loader.hpp" #include "core/rendering/shape_renderer.hpp" #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(Rendering::Renderer* renderer) : Entity(renderer), tracking_strength_(Defaults::Enemies::Cuadrado::TRACKING_STRENGTH) { 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). 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; } void Enemy::init(EnemyType type, const Vec2* ship_pos) { type_ = type; const char* shape_file = nullptr; float base_speed = 0.0F; float drotacio_min = 0.0F; float drotacio_max = 0.0F; float type_mass = Defaults::Enemies::Body::DEFAULT_MASS; switch (type_) { case EnemyType::PENTAGON: shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; base_speed = Defaults::Enemies::Pentagon::VELOCITAT; drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; type_mass = Defaults::Enemies::Pentagon::MASS; break; case EnemyType::QUADRAT: shape_file = Defaults::Enemies::Cuadrado::SHAPE_FILE; base_speed = Defaults::Enemies::Cuadrado::VELOCITAT; drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX; type_mass = Defaults::Enemies::Cuadrado::MASS; tracking_timer_ = 0.0F; break; case EnemyType::MOLINILLO: shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE; base_speed = Defaults::Enemies::Molinillo::VELOCITAT; drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX; type_mass = Defaults::Enemies::Molinillo::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::VELOCITAT; drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; break; } 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 se ha podido cargar " << shape_file << '\n'; } // Posición aleatoria con comprobación de seguridad 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); 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)) { center_.x = candidate_x; center_.y = candidate_y; found_safe_position = true; break; } } if (!found_safe_position) { 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 { 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)); } // 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); // 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()) / static_cast(RAND_MAX)) * DROTACIO_RANGE); rotacio_ = 0.0F; // Estado de animación animacio_ = EnemyAnimation(); animacio_.drotacio_base = drotacio_; animacio_.drotacio_objetivo = drotacio_; animacio_.drotacio_t = 1.0F; // 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; esta_ = true; } void Enemy::update(float delta_time) { if (!esta_) { 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; if (wounded_timer_ <= 0.0F) { wounded_timer_ = 0.0F; wound_expired_this_frame_ = true; } } // 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). // 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: 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_) { center_ = body_.position; } } void Enemy::draw() const { if (!esta_ || !shape_) { return; } const float SCALE = computeCurrentScale(); SDL_Color color{}; switch (type_) { case EnemyType::PENTAGON: color = Defaults::Palette::PENTAGON; break; case EnemyType::QUADRAT: color = Defaults::Palette::QUADRAT; break; case EnemyType::MOLINILLO: color = Defaults::Palette::MOLINILLO; break; } // Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado // a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo). 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; } } Rendering::renderShape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_, color); } void Enemy::destruir() { esta_ = false; body_.velocity = Vec2{}; body_.angular_velocity = 0.0F; body_.radius = 0.0F; // No colisiona mientras está inactivo wounded_timer_ = 0.0F; wound_expired_this_frame_ = false; last_hit_by_ = 0xFF; } void Enemy::herir(uint8_t shooter_id) { wounded_timer_ = Defaults::Enemies::Wounded::DURATION; last_hit_by_ = shooter_id; Audio::get()->playSound(Defaults::Sound::HIT, Audio::Group::GAME); } void Enemy::applyImpulse(const Vec2& impulse) { body_.applyImpulse(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); } } 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. 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) { const float CURRENT_ANGLE = velocityToAngle(body_.velocity); const float DELTA = (static_cast(std::rand()) / static_cast(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; if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL && ship_position_ != nullptr) { tracking_timer_ = 0.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; // 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); } } } } // 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) { 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 { drotacio_ = animacio_.drotacio_base; } } // Movimiento lineal puro: el world se encarga de integrar y rebotar. } void Enemy::updateAnimation(float delta_time) { updatePalpitation(delta_time); updateRotationAcceleration(delta_time); } void Enemy::updatePalpitation(float delta_time) { if (animacio_.palpitacio_activa) { animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time; animacio_.palpitacio_temps_restant -= delta_time; if (animacio_.palpitacio_temps_restant <= 0.0F) { animacio_.palpitacio_activa = false; } } else { const float RAND_VAL = static_cast(std::rand()) / static_cast(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; 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()) / static_cast(RAND_MAX)) * FREQ_RANGE); 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()) / static_cast(RAND_MAX)) * AMP_RANGE); 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()) / static_cast(RAND_MAX)) * DUR_RANGE); } } } void Enemy::updateRotationAcceleration(float delta_time) { if (animacio_.drotacio_t < 1.0F) { 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; drotacio_ = animacio_.drotacio_base; } else { 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 { const float RAND_VAL = static_cast(std::rand()) / static_cast(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; 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()) / static_cast(RAND_MAX)) * MULT_RANGE); animacio_.drotacio_objetivo = animacio_.drotacio_base * MULTIPLIER; 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()) / static_cast(RAND_MAX)) * DUR_RANGE); } } } auto Enemy::computeCurrentScale() const -> float { float scale = 1.0F; if (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_SCALE_START; constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END; scale = START + ((END - START) * SMOOTH_T); } else if (animacio_.palpitacio_activa) { scale += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase); } return scale; } auto Enemy::getBaseVelocity() const -> float { switch (type_) { case EnemyType::PENTAGON: return Defaults::Enemies::Pentagon::VELOCITAT; case EnemyType::QUADRAT: return Defaults::Enemies::Cuadrado::VELOCITAT; case EnemyType::MOLINILLO: return Defaults::Enemies::Molinillo::VELOCITAT; default: return Defaults::Enemies::Pentagon::VELOCITAT; } } auto Enemy::getBaseRotation() const -> float { return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_; } void Enemy::setTrackingStrength(float strength) { if (type_ == EnemyType::QUADRAT) { tracking_strength_ = strength; } } 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::getSafePlayAreaBounds(Defaults::Entities::ENEMY_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); out_x = static_cast((std::rand() % RANGE_X) + static_cast(min_x)); out_y = static_cast((std::rand() % RANGE_Y) + static_cast(min_y)); 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; }