241 lines
7.9 KiB
C++
241 lines
7.9 KiB
C++
// firework_manager.cpp - Implementació del gestor de fireworks
|
||
// © 2026 JailDesigner
|
||
|
||
#include "firework_manager.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstdlib>
|
||
|
||
#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<float>(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,
|
||
bool glow,
|
||
SDL_Color glow_color) {
|
||
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<float>(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<float>(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->elapsed_time = 0.0F;
|
||
fw->initial_speed = SPEED;
|
||
|
||
fw->brightness = initial_brightness;
|
||
fw->color = color;
|
||
fw->glow = glow;
|
||
fw->glow_color = glow_color;
|
||
fw->active = true;
|
||
}
|
||
}
|
||
|
||
void FireworkManager::update(float delta_time) {
|
||
for (auto& fw : pool_) {
|
||
if (!fw.active) {
|
||
continue;
|
||
}
|
||
|
||
fw.elapsed_time += 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.elapsed_time < fw.grow_duration) {
|
||
// Fase 1: creixement lineal de 0 a max_length.
|
||
const float T = fw.elapsed_time / 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),
|
||
};
|
||
|
||
if (fw.glow) {
|
||
Rendering::lineaGlow(renderer_,
|
||
static_cast<int>(fw.head.x),
|
||
static_cast<int>(fw.head.y),
|
||
static_cast<int>(TAIL.x),
|
||
static_cast<int>(TAIL.y),
|
||
fw.brightness,
|
||
0.0F,
|
||
fw.color,
|
||
fw.glow_color);
|
||
} else {
|
||
Rendering::linea(renderer_,
|
||
static_cast<int>(fw.head.x),
|
||
static_cast<int>(fw.head.y),
|
||
static_cast<int>(TAIL.x),
|
||
static_cast<int>(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
|