// trail_manager.cpp - Implementació de l'estela de partícules // © 2026 JailDesigner #include "trail_manager.hpp" #include #include #include #include #include "core/defaults.hpp" #include "core/graphics/shape_loader.hpp" #include "core/rendering/shape_renderer.hpp" namespace Effects { namespace { constexpr float TAU = 2.0F * Defaults::Math::PI; auto randUniform(float min_v, float max_v) -> float { const float NORM = static_cast(std::rand()) / static_cast(RAND_MAX); return min_v + (NORM * (max_v - min_v)); } auto lerpU8(unsigned char a, unsigned char b, float t) -> unsigned char { const float MIX = std::clamp(t, 0.0F, 1.0F); const float OUT = static_cast(a) + (MIX * (static_cast(b) - static_cast(a))); return static_cast(std::round(OUT)); } } // namespace TrailManager::TrailManager(Rendering::Renderer* renderer) : renderer_(renderer), star_shape_(Graphics::ShapeLoader::load("star.shp")) { if (!star_shape_ || !star_shape_->isValid()) { std::cerr << "[TrailManager] Warning: no s'ha pogut load star.shp\n"; } for (auto& particle : pool_) { particle.active = false; } } void TrailManager::update(float delta_time, const std::array& ships) { time_accumulator_ += delta_time; for (auto& particle : pool_) { if (!particle.active) { continue; } particle.age += delta_time; if (particle.age >= particle.lifetime) { particle.active = false; } } for (std::size_t player_id = 0; player_id < ships.size(); player_id++) { tryEmitFromShip(ships[player_id], player_id, delta_time); } } void TrailManager::tryEmitFromShip(const Ship& ship, std::size_t player_id, float delta_time) { if (!ship.isActive()) { emit_cooldown_[player_id] = 0.0F; return; } if (ship.getSpeed() < Defaults::Trail::SPEED_THRESHOLD_PX_S) { emit_cooldown_[player_id] = 0.0F; return; } emit_cooldown_[player_id] -= delta_time; if (emit_cooldown_[player_id] > 0.0F) { return; } // Cua = center - REAR_OFFSET * forward, on forward = (cos(angle-π/2), sin(angle-π/2)) const float FORWARD_ANGLE = ship.getAngle() - (Defaults::Math::PI / 2.0F); const float COS_F = std::cos(FORWARD_ANGLE); const float SIN_F = std::sin(FORWARD_ANGLE); const Vec2 CENTER = ship.getCenter(); const float JITTER_X = randUniform(-Defaults::Trail::POSITION_JITTER_PX, Defaults::Trail::POSITION_JITTER_PX); const float JITTER_Y = randUniform(-Defaults::Trail::POSITION_JITTER_PX, Defaults::Trail::POSITION_JITTER_PX); const Vec2 REAR = { .x = CENTER.x - (Defaults::Trail::REAR_OFFSET_PX * COS_F) + JITTER_X, .y = CENTER.y - (Defaults::Trail::REAR_OFFSET_PX * SIN_F) + JITTER_Y}; emitAt(REAR); emit_cooldown_[player_id] = Defaults::Trail::EMIT_INTERVAL_S + randUniform(-Defaults::Trail::EMIT_JITTER_S, Defaults::Trail::EMIT_JITTER_S); } void TrailManager::emitAt(Vec2 pos) { const int SLOT = findFreeSlot(); if (SLOT < 0) { return; // pool ple — descart silenciós } Particle& particle = pool_[static_cast(SLOT)]; particle.active = true; particle.origin = pos; particle.phase_x = randUniform(0.0F, TAU); particle.phase_y = randUniform(0.0F, TAU); particle.phase_pulse = randUniform(0.0F, TAU); particle.age = 0.0F; particle.lifetime = Defaults::Trail::LIFETIME_BASE_S + randUniform(-Defaults::Trail::LIFETIME_JITTER_S, Defaults::Trail::LIFETIME_JITTER_S); particle.scale = randUniform(Defaults::Trail::SCALE_MIN, Defaults::Trail::SCALE_MAX); } auto TrailManager::findFreeSlot() -> int { for (std::size_t i = 0; i < pool_.size(); i++) { if (!pool_[i].active) { return static_cast(i); } } return -1; } void TrailManager::draw() const { if (!star_shape_ || !star_shape_->isValid()) { return; } for (const auto& particle : pool_) { if (!particle.active) { continue; } drawParticle(particle); } } void TrailManager::drawParticle(const Particle& particle) const { const float T_NORM = std::clamp(particle.age / particle.lifetime, 0.0F, 1.0F); const float FADE = 1.0F - T_NORM; const float OSC_DX = std::sin((TAU * Defaults::Trail::OSCILLATION_FREQ_HZ * time_accumulator_) + particle.phase_x) * Defaults::Trail::OSCILLATION_AMP_PX * FADE; const float OSC_DY = std::sin((TAU * Defaults::Trail::OSCILLATION_FREQ_HZ * time_accumulator_) + particle.phase_y) * Defaults::Trail::OSCILLATION_AMP_PX * FADE; const Vec2 POS = {.x = particle.origin.x + OSC_DX, .y = particle.origin.y + OSC_DY}; const float MIX = 0.5F + (0.5F * std::sin((TAU * Defaults::Trail::PULSE_FREQ_HZ * time_accumulator_) + particle.phase_pulse)); const SDL_Color COLOR = { .r = lerpU8(Defaults::Trail::COLOR_A_R, Defaults::Trail::COLOR_B_R, MIX), .g = lerpU8(Defaults::Trail::COLOR_A_G, Defaults::Trail::COLOR_B_G, MIX), .b = lerpU8(Defaults::Trail::COLOR_A_B, Defaults::Trail::COLOR_B_B, MIX), .a = 255}; const float CURRENT_SCALE = particle.scale * FADE; const float BRIGHTNESS = FADE; Rendering::renderShape( renderer_, star_shape_, POS, 0.0F, // sense rotació CURRENT_SCALE, 1.0F, // progress (totalment visible) BRIGHTNESS, COLOR); } void TrailManager::reset() { for (auto& particle : pool_) { particle.active = false; } emit_cooldown_[0] = 0.0F; emit_cooldown_[1] = 0.0F; time_accumulator_ = 0.0F; } auto TrailManager::getActiveCount() const -> int { int count = 0; for (const auto& particle : pool_) { if (particle.active) { count++; } } return count; } } // namespace Effects