// 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); } // 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_; if (type_ == EnemyType::SQUARE) { tracking_timer_ = 0.0F; tracking_strength_ = cfg.behavior.tracking_strength; } 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; 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_ / 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); } if (!isWounded()) { switch (type_) { case EnemyType::PENTAGON: case EnemyType::STAR: // STAR reusa el zigzag esquivador de Pentagon. Si en el futur // vol comportament propi, separa-li el cas. 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 = 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; } // PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics) // en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld. void Enemy::behaviorPentagon(float delta_time) { direction_change_timer_ += delta_time; if (randFloat01() < config_->behavior.zigzag_prob_per_second * delta_time) { const float CURRENT_ANGLE = velocityToAngle(body_.velocity); const float DELTA = randFloat01() * 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 discret cap a la nau cada N segons. 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: movement rectilini + boost de rotació visual prop 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) { 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) { 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; }