// firework_manager.cpp - Implementació del gestor de fireworks // © 2026 JailDesigner #include "firework_manager.hpp" #include #include #include #include "core/defaults.hpp" #include "core/rendering/line_renderer.hpp" namespace Effects { namespace { // Random float in [-1, 1]. auto randSigned() -> float { return ((std::rand() / static_cast(RAND_MAX)) * 2.0F) - 1.0F; } // Rebot del head contra els límits del PLAYAREA (mateix patró que // DebrisManager::bounceOffPlayArea). void bounceOffPlayArea(Vec2& head, Vec2& velocity) { const auto& bounds = Defaults::Zones::PLAYAREA; constexpr float REST = Defaults::FX::Firework::RESTITUTION_BOUNDS; const float MIN_X = bounds.x; const float MAX_X = bounds.x + bounds.w; const float MIN_Y = bounds.y; const float MAX_Y = bounds.y + bounds.h; if (head.x < MIN_X) { head.x = MIN_X; if (velocity.x < 0.0F) { velocity.x = -velocity.x * REST; } } if (head.x > MAX_X) { head.x = MAX_X; if (velocity.x > 0.0F) { velocity.x = -velocity.x * REST; } } if (head.y < MIN_Y) { head.y = MIN_Y; if (velocity.y < 0.0F) { velocity.y = -velocity.y * REST; } } if (head.y > MAX_Y) { head.y = MAX_Y; if (velocity.y > 0.0F) { velocity.y = -velocity.y * REST; } } } } // namespace FireworkManager::FireworkManager(Rendering::Renderer* renderer) : renderer_(renderer) { for (auto& fw : pool_) { fw.active = false; } } void FireworkManager::spawn(const Vec2& origen, SDL_Color color, float initial_speed, int n_points, float initial_brightness) { if (n_points <= 0) { return; } // Notificar als subscriptors (playfield pulses, etc.). if (spawn_callback_) { spawn_callback_(origen); } const float ANGLE_STEP = 2.0F * Defaults::Math::PI / static_cast(n_points); const float JITTER_RAD = Defaults::FX::Firework::ANGULAR_JITTER_DEG * Defaults::Math::PI / 180.0F; for (int i = 0; i < n_points; i++) { Firework* fw = findFreeSlot(); if (fw == nullptr) { return; // Pool ple } const float BASE_ANGLE = ANGLE_STEP * static_cast(i); const float JITTER = randSigned() * JITTER_RAD; const float ANGLE = BASE_ANGLE + JITTER; const float SPEED = initial_speed + (randSigned() * Defaults::FX::Firework::SPEED_VARIATION); fw->head = origen; fw->velocity = {.x = std::cos(ANGLE) * SPEED, .y = std::sin(ANGLE) * SPEED}; fw->acceleration = Defaults::FX::Firework::FRICTION; fw->current_length = 0.0F; fw->max_length = Defaults::FX::Firework::MAX_LENGTH; fw->grow_duration = Defaults::FX::Firework::GROW_DURATION; fw->temps_vida = 0.0F; fw->initial_speed = SPEED; fw->brightness = initial_brightness; fw->color = color; fw->active = true; } } void FireworkManager::update(float delta_time) { for (auto& fw : pool_) { if (!fw.active) { continue; } fw.temps_vida += delta_time; // 1. Fricció lineal (aplicar en la direcció del movement). const float SPEED = std::sqrt( (fw.velocity.x * fw.velocity.x) + (fw.velocity.y * fw.velocity.y)); if (SPEED > 1.0F) { const float DIR_X = fw.velocity.x / SPEED; const float DIR_Y = fw.velocity.y / SPEED; float new_speed = SPEED + (fw.acceleration * delta_time); new_speed = std::max(new_speed, 0.0F); fw.velocity.x = DIR_X * new_speed; fw.velocity.y = DIR_Y * new_speed; } else { fw.velocity = {.x = 0.0F, .y = 0.0F}; } // 2. Avançar head. fw.head.x += fw.velocity.x * delta_time; fw.head.y += fw.velocity.y * delta_time; // 3. Rebot contra PLAYAREA. bounceOffPlayArea(fw.head, fw.velocity); // 4. Calcular longitud i brillor segons fase. if (fw.temps_vida < fw.grow_duration) { // Fase 1: creixement lineal de 0 a max_length. const float T = fw.temps_vida / fw.grow_duration; fw.current_length = fw.max_length * T; fw.brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS; } else { // Fase 2: longitud i brillor proporcionals a la velocity actual. const float CURRENT_SPEED = std::sqrt( (fw.velocity.x * fw.velocity.x) + (fw.velocity.y * fw.velocity.y)); const float RATIO = (fw.initial_speed > 0.01F) ? std::min(CURRENT_SPEED / fw.initial_speed, 1.0F) : 0.0F; fw.current_length = fw.max_length * RATIO; fw.brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS * RATIO; } // 5. Morir si la longitud o el brillor cauen sota llindar. if (fw.current_length < Defaults::FX::Firework::MIN_LENGTH || fw.brightness < Defaults::FX::Firework::MIN_BRIGHTNESS) { fw.active = false; } } } void FireworkManager::draw() const { for (const auto& fw : pool_) { if (!fw.active) { continue; } // tail = head − velocity_normalitzada × current_length. const float SPEED = std::sqrt( (fw.velocity.x * fw.velocity.x) + (fw.velocity.y * fw.velocity.y)); if (SPEED < 0.01F) { continue; // Sense direcció no podem orientar la línia. } const float DIR_X = fw.velocity.x / SPEED; const float DIR_Y = fw.velocity.y / SPEED; const Vec2 TAIL = { .x = fw.head.x - (DIR_X * fw.current_length), .y = fw.head.y - (DIR_Y * fw.current_length), }; Rendering::linea(renderer_, static_cast(fw.head.x), static_cast(fw.head.y), static_cast(TAIL.x), static_cast(TAIL.y), fw.brightness, 0.0F, fw.color); } } void FireworkManager::reset() { for (auto& fw : pool_) { fw.active = false; } } auto FireworkManager::getActiveCount() const -> int { int count = 0; for (const auto& fw : pool_) { if (fw.active) { count++; } } return count; } auto FireworkManager::findFreeSlot() -> Firework* { for (auto& fw : pool_) { if (!fw.active) { return &fw; } } return nullptr; } } // namespace Effects