// enemy.cpp - Implementación de enemigos (ORNIs) // © 2026 JailDesigner #include "game/entities/enemy.hpp" #include #include #include #include #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::Square::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 rotation_delta_min = 0.0F; float rotation_delta_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; 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::VELOCITAT; 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::VELOCITAT; 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::VELOCITAT; rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN; rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_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 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); 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; } void Enemy::update(float delta_time) { if (!is_active_) { 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 (invulnerability_timer_ > 0.0F) { 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 = 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::SQUARE: behaviorSquare(delta_time); break; case EnemyType::PINWHEEL: behaviorPinwheel(delta_time); break; } } // 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; } } void Enemy::draw() const { if (!is_active_ || !shape_) { 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; } // 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_, rotation_, SCALE, 1.0F, brightness_, color); } void Enemy::destroy() { is_active_ = 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::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) { 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; } } // 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) { 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); } } } } // 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; } else { rotation_delta_ = animation_.rotation_delta_base; } } // Movimiento lineal puro: el world se encarga de integrar y rebotar. } void Enemy::updateAnimation(float delta_time) { updatePulse(delta_time); updateRotationAcceleration(delta_time); } void Enemy::updatePulse(float delta_time) { 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_DURACIO_MAX - Defaults::Enemies::Animation::PULSE_DURACIO_MIN; animation_.pulse_time_remaining = Defaults::Enemies::Animation::PULSE_DURACIO_MIN + ((static_cast(std::rand()) / static_cast(RAND_MAX)) * DUR_RANGE); } } } void Enemy::updateRotationAcceleration(float delta_time) { if (animation_.rotation_delta_t < 1.0F) { animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration; if (animation_.rotation_delta_t >= 1.0F) { animation_.rotation_delta_t = 1.0F; animation_.rotation_delta_base = animation_.rotation_delta_target; rotation_delta_ = animation_.rotation_delta_base; } else { const float T = animation_.rotation_delta_t; const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float INITIAL = animation_.rotation_delta_base; 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_DURACIO_MAX - Defaults::Enemies::Animation::ROTATION_ACCEL_DURACIO_MIN; animation_.rotation_delta_duration = Defaults::Enemies::Animation::ROTATION_ACCEL_DURACIO_MIN + ((static_cast(std::rand()) / static_cast(RAND_MAX)) * DUR_RANGE); } } } 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 = 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 (animation_.pulse_active) { scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase); } return scale; } auto Enemy::getBaseVelocity() const -> float { switch (type_) { case EnemyType::PENTAGON: return Defaults::Enemies::Pentagon::VELOCITAT; case EnemyType::SQUARE: return Defaults::Enemies::Square::VELOCITAT; case EnemyType::PINWHEEL: return Defaults::Enemies::Pinwheel::VELOCITAT; default: return Defaults::Enemies::Pentagon::VELOCITAT; } } auto Enemy::getBaseRotation() const -> float { return animation_.rotation_delta_base != 0.0F ? animation_.rotation_delta_base : rotation_delta_; } void Enemy::setTrackingStrength(float strength) { if (type_ == EnemyType::SQUARE) { 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; }