// enemic.cpp - Implementació d'enemics (ORNIs) // © 1999 Visente i Sergi (versió Pascal) // © 2025 Port a C++20 amb SDL3 #include "game/entities/enemic.hpp" #include #include #include #include #include "core/defaults.hpp" #include "core/entities/entitat.hpp" #include "core/graphics/shape_loader.hpp" #include "core/rendering/shape_renderer.hpp" #include "core/types.hpp" #include "game/constants.hpp" Enemic::Enemic(SDL_Renderer* renderer) : Entitat(renderer), velocitat_(0.0F), drotacio_(0.0F), rotacio_(0.0F), esta_(false), tipus_(TipusEnemic::PENTAGON), tracking_timer_(0.0F), ship_position_(nullptr), tracking_strength_(0.5F), // Default tracking strength timer_invulnerabilitat_(0.0F) { // Start vulnerable // [NUEVO] Brightness específic per enemics brightness_ = Defaults::Brightness::ENEMIC; // [NUEVO] Forma es carrega a inicialitzar() segons el tipus // Constructor no carrega forma per permetre tipus diferents } void Enemic::inicialitzar(TipusEnemic tipus, const Punt* ship_pos) { // Guardar tipus tipus_ = tipus; // Carregar forma segons el tipus const char* shape_file; float drotacio_min; float drotacio_max; switch (tipus_) { case TipusEnemic::PENTAGON: shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; velocitat_ = Defaults::Enemies::Pentagon::VELOCITAT; drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; break; case TipusEnemic::QUADRAT: shape_file = Defaults::Enemies::Quadrat::SHAPE_FILE; velocitat_ = Defaults::Enemies::Quadrat::VELOCITAT; drotacio_min = Defaults::Enemies::Quadrat::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Quadrat::DROTACIO_MAX; tracking_timer_ = 0.0F; break; case TipusEnemic::MOLINILLO: shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE; velocitat_ = Defaults::Enemies::Molinillo::VELOCITAT; drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN; drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX; break; } // Carregar forma forma_ = Graphics::ShapeLoader::load(shape_file); if (!forma_ || !forma_->es_valida()) { std::cerr << "[Enemic] Error: no s'ha pogut carregar " << shape_file << '\n'; } // [MODIFIED] Posició aleatòria amb comprovació de seguretat float min_x; float max_x; float min_y; float max_y; Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); if (ship_pos != nullptr) { // [NEW] Safe spawn: attempt to find position away from ship bool found_safe_position = false; for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { float candidate_x; float candidate_y; if (intent_spawn_safe(*ship_pos, candidate_x, candidate_y)) { centre_.x = candidate_x; centre_.y = candidate_y; found_safe_position = true; break; } } if (!found_safe_position) { // Fallback: spawn anywhere (user's preference) int range_x = static_cast(max_x - min_x); int range_y = static_cast(max_y - min_y); centre_.x = static_cast((std::rand() % range_x) + static_cast(min_x)); centre_.y = static_cast((std::rand() % range_y) + static_cast(min_y)); std::cout << "[Enemic] Advertència: spawn sense zona segura després de " << Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intents" << '\n'; } } else { // [EXISTING] No ship position: spawn anywhere (backward compatibility) int range_x = static_cast(max_x - min_x); int range_y = static_cast(max_y - min_y); centre_.x = static_cast((std::rand() % range_x) + static_cast(min_x)); centre_.y = static_cast((std::rand() % range_y) + static_cast(min_y)); } // Angle aleatori de moviment angle_ = (std::rand() % 360) * Constants::PI / 180.0F; // Rotació visual aleatòria (rad/s) dins del rang del tipus float drotacio_range = drotacio_max - drotacio_min; drotacio_ = drotacio_min + ((static_cast(std::rand()) / RAND_MAX) * drotacio_range); rotacio_ = 0.0F; // Inicialitzar estat d'animació animacio_ = AnimacioEnemic(); // Reset to defaults animacio_.drotacio_base = drotacio_; animacio_.drotacio_objetivo = drotacio_; animacio_.drotacio_t = 1.0F; // Start without interpolating // [NEW] Inicialitzar invulnerabilitat timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; // 3.0s brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; // 0.3f // Activar esta_ = true; } void Enemic::actualitzar(float delta_time) { if (esta_) { // [NEW] Update invulnerability timer and brightness if (timer_invulnerabilitat_ > 0.0F) { timer_invulnerabilitat_ -= delta_time; timer_invulnerabilitat_ = std::max(timer_invulnerabilitat_, 0.0F); // [NEW] Update brightness with LERP during invulnerability float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; float t = 1.0F - t_inv; // 0.0 → 1.0 float smooth_t = t * t * (3.0F - 2.0F * t); // smoothstep constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END; brightness_ = START + ((END - START) * smooth_t); } // Moviment autònom mou(delta_time); // Actualitzar animacions (palpitació, rotació accelerada) actualitzar_animacio(delta_time); // Rotació visual (time-based: drotacio_ està en rad/s) rotacio_ += drotacio_ * delta_time; } } void Enemic::dibuixar() const { if (esta_ && forma_) { // Calculate animated scale (includes invulnerability LERP) float escala = calcular_escala_actual(); // brightness_ is already updated in actualitzar() Rendering::render_shape(renderer_, forma_, centre_, rotacio_, escala, true, 1.0F, brightness_); } } void Enemic::mou(float delta_time) { // Dispatcher: crida el comportament específic segons el tipus switch (tipus_) { case TipusEnemic::PENTAGON: comportament_pentagon(delta_time); break; case TipusEnemic::QUADRAT: comportament_quadrat(delta_time); break; case TipusEnemic::MOLINILLO: comportament_molinillo(delta_time); break; } } void Enemic::comportament_pentagon(float delta_time) { // Pentagon: zigzag esquivador (frequent direction changes) // Similar a comportament original però amb probabilitat més alta float velocitat_efectiva = velocitat_ * delta_time; // Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt) float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F)); float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F)); float new_y = centre_.y + dy; float new_x = centre_.x + dx; // Obtenir límits segurs float min_x; float max_x; float min_y; float max_y; Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); // Zigzag: canvi d'angle més freqüent en tocar límits if (new_y >= min_y && new_y <= max_y) { centre_.y = new_y; } else { // Probabilitat més alta de canvi d'angle if (static_cast(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) { float rand_angle = (static_cast(std::rand()) / RAND_MAX) * Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX; angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle; } } if (new_x >= min_x && new_x <= max_x) { centre_.x = new_x; } else { if (static_cast(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) { float rand_angle = (static_cast(std::rand()) / RAND_MAX) * Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX; angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle; } } } void Enemic::comportament_quadrat(float delta_time) { // Quadrat: perseguidor (tracks player position) // Update tracking timer tracking_timer_ += delta_time; // Periodically update angle toward ship if (tracking_timer_ >= Defaults::Enemies::Quadrat::TRACKING_INTERVAL) { tracking_timer_ = 0.0F; if (ship_position_ != nullptr) { // Calculate angle to ship float dx = ship_position_->x - centre_.x; float dy = ship_position_->y - centre_.y; float target_angle = std::atan2(dy, dx) + (Constants::PI / 2.0F); // Interpolate toward target angle float angle_diff = target_angle - angle_; // Normalize angle difference to [-π, π] while (angle_diff > Constants::PI) { angle_diff -= 2.0F * Constants::PI; } while (angle_diff < -Constants::PI) { angle_diff += 2.0F * Constants::PI; } // Apply tracking strength (uses member variable, defaults to 0.5) angle_ += angle_diff * tracking_strength_; } } // Move in current direction float velocitat_efectiva = velocitat_ * delta_time; float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F)); float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F)); float new_y = centre_.y + dy; float new_x = centre_.x + dx; // Obtenir límits segurs float min_x; float max_x; float min_y; float max_y; Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); // Bounce on walls (simple reflection) if (new_y >= min_y && new_y <= max_y) { centre_.y = new_y; } else { angle_ = -angle_; // Vertical reflection } if (new_x >= min_x && new_x <= max_x) { centre_.x = new_x; } else { angle_ = Constants::PI - angle_; // Horizontal reflection } } void Enemic::comportament_molinillo(float delta_time) { // Molinillo: agressiu (fast, straight lines, proximity spin-up) // Check proximity to ship for spin-up effect if (ship_position_ != nullptr) { float dx = ship_position_->x - centre_.x; float dy = ship_position_->y - centre_.y; float distance = std::sqrt((dx * dx) + (dy * dy)); if (distance < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) { // Temporarily boost rotation speed when near ship float boost = Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER; drotacio_ = animacio_.drotacio_base * boost; } else { // Normal rotation speed drotacio_ = animacio_.drotacio_base; } } // Fast straight-line movement float velocitat_efectiva = velocitat_ * delta_time; float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F)); float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F)); float new_y = centre_.y + dy; float new_x = centre_.x + dx; // Obtenir límits segurs float min_x; float max_x; float min_y; float max_y; Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); // Rare angle changes on wall hits if (new_y >= min_y && new_y <= max_y) { centre_.y = new_y; } else { if (static_cast(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) { float rand_angle = (static_cast(std::rand()) / RAND_MAX) * Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX; angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle; } } if (new_x >= min_x && new_x <= max_x) { centre_.x = new_x; } else { if (static_cast(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) { float rand_angle = (static_cast(std::rand()) / RAND_MAX) * Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX; angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle; } } } void Enemic::actualitzar_animacio(float delta_time) { actualitzar_palpitacio(delta_time); actualitzar_rotacio_accelerada(delta_time); } void Enemic::actualitzar_palpitacio(float delta_time) { if (animacio_.palpitacio_activa) { // Advance phase (2π * frequency * dt) animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time; // Decrement timer animacio_.palpitacio_temps_restant -= delta_time; // Deactivate when timer expires if (animacio_.palpitacio_temps_restant <= 0.0F) { animacio_.palpitacio_activa = false; } } else { // Random trigger (probability per second) float rand_val = static_cast(std::rand()) / RAND_MAX; float trigger_prob = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time; if (rand_val < trigger_prob) { // Activate palpitation animacio_.palpitacio_activa = true; animacio_.palpitacio_fase = 0.0F; // Randomize parameters 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()) / RAND_MAX) * freq_range); 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()) / RAND_MAX) * amp_range); 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()) / RAND_MAX) * dur_range); } } } void Enemic::actualitzar_rotacio_accelerada(float delta_time) { if (animacio_.drotacio_t < 1.0F) { // Transitioning to new target 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; // Reached target drotacio_ = animacio_.drotacio_base; } else { // Smoothstep interpolation: t² * (3 - 2t) float t = animacio_.drotacio_t; float smooth_t = t * t * (3.0F - 2.0F * t); // Interpolate between base and target float initial = animacio_.drotacio_base; float target = animacio_.drotacio_objetivo; drotacio_ = initial + ((target - initial) * smooth_t); } } else { // Random trigger for new acceleration float rand_val = static_cast(std::rand()) / RAND_MAX; float trigger_prob = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time; if (rand_val < trigger_prob) { // Start new transition animacio_.drotacio_t = 0.0F; // Randomize target speed (multiplier * base) float mult_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX - Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN; float multiplier = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN + ((static_cast(std::rand()) / RAND_MAX) * mult_range); animacio_.drotacio_objetivo = animacio_.drotacio_base * multiplier; // Randomize duration 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()) / RAND_MAX) * dur_range); } } } float Enemic::calcular_escala_actual() const { float escala = 1.0F; // [NEW] Invulnerability LERP prioritza sobre palpitació if (timer_invulnerabilitat_ > 0.0F) { // Calculate t: 0.0 at spawn → 1.0 at end float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; float t = 1.0F - t_inv; // 0.0 → 1.0 // Apply smoothstep: t² * (3 - 2t) float smooth_t = t * t * (3.0F - 2.0F * t); // LERP scale from 0.0 to 1.0 constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START; constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END; escala = START + ((END - START) * smooth_t); } else if (animacio_.palpitacio_activa) { // [EXISTING] Palpitació només quan no invulnerable escala += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase); } return escala; } // [NEW] Stage system API implementations float Enemic::get_base_velocity() const { switch (tipus_) { case TipusEnemic::PENTAGON: return Defaults::Enemies::Pentagon::VELOCITAT; case TipusEnemic::QUADRAT: return Defaults::Enemies::Quadrat::VELOCITAT; case TipusEnemic::MOLINILLO: return Defaults::Enemies::Molinillo::VELOCITAT; } return 0.0F; } float Enemic::get_base_rotation() const { // Return the base rotation speed (drotacio_base if available, otherwise current drotacio_) return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_; } void Enemic::set_tracking_strength(float strength) { // Only applies to QUADRAT type if (tipus_ == TipusEnemic::QUADRAT) { tracking_strength_ = strength; } } // [NEW] Safe spawn helper - checks if position is away from ship bool Enemic::intent_spawn_safe(const Punt& ship_pos, float& out_x, float& out_y) { // Generate random position within safe bounds float min_x; float max_x; float min_y; float max_y; Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); int range_x = static_cast(max_x - min_x); 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)); // Check Euclidean distance to ship float dx = out_x - ship_pos.x; float dy = out_y - ship_pos.y; float distancia = std::sqrt((dx * dx) + (dy * dy)); // Return true if position is safe (>= 36px from ship) return distancia >= Defaults::Enemies::Spawn::SAFETY_DISTANCE; }