// 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)), }; } // 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; // 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) { type_ = type; config_ = &EnemyRegistry::get(type); const EnemyConfig& cfg = *config_; ai_state_ = EnemyAiState{}; ai_state_.tracking_strength = cfg.ai.movement.tracking_strength; // Timers paral·lels a tick: random [0, interval) per evitar que tots els // enemics del mateix tipus disparin sincronitzats al spawn. ai_tick_timers_.resize(cfg.ai.tick.size()); for (std::size_t i = 0; i < cfg.ai.tick.size(); ++i) { ai_tick_timers_[i] = randFloat01() * cfg.ai.tick[i].interval; } shape_ = Graphics::ShapeLoader::load(cfg.shape.path); if (!shape_ || !shape_->isValid()) { std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n'; } // 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ó aleatòria amb comprovació de safety_distance. 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_, cfg.spawn.safety_distance, 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)); } const float ANGLE_INICIAL = static_cast(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ó 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(); animation_.rotation_delta_base = rotation_delta_; animation_.rotation_delta_target = rotation_delta_; animation_.rotation_delta_t = 1.0F; invulnerability_timer_ = cfg.spawn.invulnerability_duration; brightness_ = cfg.spawn.invulnerability_brightness_start; 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_ / config_->spawn.invulnerability_duration; const float T = 1.0F - T_INV; const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float START = config_->spawn.invulnerability_brightness_start; const float END = config_->spawn.invulnerability_brightness_end; brightness_ = START + ((END - START) * SMOOTH_T); } // El moviment es delega a Systems::EnemyAi::tick, invocat des de l'scene // ABANS d'aquest update (manté l'ordre: AI escriu velocity/rotation_delta, // després animation pot modular rotation_delta via rotation_accel). 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 = config_->shape.scale * computeCurrentScale(); SDL_Color color = config_->colors.normal; if (wounded_timer_ > 0.0F) { 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; } } 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_ = config_->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 = static_cast(std::rand() % 360) * Constants::PI / 180.0F; setVelocityFromAngle(A, speed); } } void Enemy::setVelocityFromAngle(float angle_movement, float speed) { body_.velocity = angleToDirection(angle_movement) * speed; } void Enemy::updateAnimation(float delta_time) { updatePulse(delta_time); updateRotationAcceleration(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; } 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) { 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); } 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_ / config_->spawn.invulnerability_duration; const float T = 1.0F - T_INV; const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); 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); } 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) { ai_state_.tracking_strength = strength; } } 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; 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 >= safety_distance; }