// 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" #include "game/entities/enemy_config.hpp" #include "game/entities/enemy_registry.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). auto velocityToAngle(const Vec2& velocity) -> float { if (velocity.lengthSquared() < 0.0001F) { return 0.0F; } return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); } } // 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; } void Enemy::init(EnemyType type, const Vec2* ship_pos) { type_ = type; config_ = &EnemyRegistry::get(type); const EnemyConfig& cfg = *config_; collision_radius_ = cfg.physics.collision_radius; // Cas Square: resetejar tracking timer al spawn. if (type_ == EnemyType::SQUARE) { tracking_timer_ = 0.0F; tracking_strength_ = cfg.behavior.tracking_strength; } body_.setMass(cfg.physics.mass); body_.radius = collision_radius_; shape_ = Graphics::ShapeLoader::load(cfg.shape.path); if (!shape_ || !shape_->isValid()) { std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n'; } // Posición aleatoria con comprobación de seguridad float min_x; float max_x; float min_y; float max_y; Constants::getSafePlayAreaBounds(collision_radius_, min_x, max_x, min_y, max_y); if (ship_pos != nullptr) { bool found_safe_position = false; for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { float candidate_x; float candidate_y; if (attemptSafeSpawn(*ship_pos, collision_radius_, 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 zone 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, cfg.physics.speed); body_.position = center_; body_.angle = 0.0F; 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); rotation_ = 0.0F; animation_ = EnemyAnimation(); animation_.rotation_delta_base = rotation_delta_; 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; direction_change_timer_ = 0.0F; is_active_ = true; } void Enemy::update(float delta_time) { if (!is_active_) { return; } 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; } } 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); } 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; } } updateAnimation(delta_time); rotation_ += rotation_delta_ * delta_time; } void Enemy::postUpdate(float /*delta_time*/) { if (is_active_) { center_ = body_.position; } } void Enemy::draw() const { if (!is_active_ || !shape_) { return; } const float 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 T = std::fmod(wounded_timer_, CYCLE); if (T < (CYCLE / 2.0F)) { color = config_->colors.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; 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; } void Enemy::applyImpulse(const Vec2& impulse) { body_.applyImpulse(impulse); } void Enemy::setVelocity(float speed) { const float CURRENT_SPEED = body_.velocity.length(); if (CURRENT_SPEED > 0.0F) { body_.velocity = body_.velocity * (speed / CURRENT_SPEED); } else { 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; const float RAND_VAL = static_cast(std::rand()) / static_cast(RAND_MAX); if (RAND_VAL < config_->behavior.zigzag_prob_per_second * delta_time) { const float CURRENT_ANGLE = velocityToAngle(body_.velocity); const float DELTA = (static_cast(std::rand()) / static_cast(RAND_MAX)) * 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); 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_ >= config_->behavior.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; body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) + (DESIRED_VEL * tracking_strength_); 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. void Enemy::behaviorPinwheel(float /*delta_time*/) { if (ship_position_ != nullptr) { const Vec2 TO_SHIP = *ship_position_ - center_; const float DIST = TO_SHIP.length(); if (DIST < config_->behavior.proximity_distance) { rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier; } else { rotation_delta_ = animation_.rotation_delta_base; } } } 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_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); } } } 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_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); } } } 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 { return EnemyRegistry::get(type_).physics.speed; } 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 collision_radius, float& out_x, float& out_y) -> bool { float min_x; float max_x; float min_y; float max_y; Constants::getSafePlayAreaBounds(collision_radius, min_x, max_x, min_y, max_y); const int RANGE_X = static_cast(max_x - min_x); const int RANGE_Y = static_cast(max_y - min_y); 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; }