diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index 11f5a84..3353f94 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -25,6 +25,7 @@ #include "core/defaults/rendering.hpp" #include "core/defaults/ship.hpp" #include "core/defaults/title.hpp" +#include "core/defaults/trail.hpp" #include "core/defaults/window.hpp" #include "core/defaults/zones.hpp" // IWYU pragma: end_exports diff --git a/source/core/defaults/trail.hpp b/source/core/defaults/trail.hpp new file mode 100644 index 0000000..d646652 --- /dev/null +++ b/source/core/defaults/trail.hpp @@ -0,0 +1,35 @@ +// trail.hpp - Configuració de l'estela de partícules de la nau +// © 2026 JailDesigner + +#pragma once + +namespace Defaults::Trail { + + constexpr int POOL_SIZE = 200; + + constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180) + constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal + constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown + constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement + constexpr float REAR_OFFSET_PX = 10.0F; // distància darrere center_ (cua) + + constexpr float LIFETIME_BASE_S = 0.65F; + constexpr float LIFETIME_JITTER_S = 0.15F; + + constexpr float SCALE_MIN = 0.7F; // × estrella starfield (3 px punta) + constexpr float SCALE_MAX = 1.2F; + + constexpr float OSCILLATION_AMP_PX = 1.8F; + constexpr float OSCILLATION_FREQ_HZ = 6.0F; + + constexpr float PULSE_FREQ_HZ = 2.5F; + + // Colors del pulse (interpolats sinusoïdalment per partícula) + constexpr unsigned char COLOR_A_R = 255; + constexpr unsigned char COLOR_A_G = 255; + constexpr unsigned char COLOR_A_B = 0; // #FFFF00 — groc viu + constexpr unsigned char COLOR_B_R = 218; + constexpr unsigned char COLOR_B_G = 165; + constexpr unsigned char COLOR_B_B = 32; // #DAA520 — daurat clàssic + +} // namespace Defaults::Trail diff --git a/source/game/effects/trail_manager.cpp b/source/game/effects/trail_manager.cpp new file mode 100644 index 0000000..25f499f --- /dev/null +++ b/source/game/effects/trail_manager.cpp @@ -0,0 +1,188 @@ +// 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 diff --git a/source/game/effects/trail_manager.hpp b/source/game/effects/trail_manager.hpp new file mode 100644 index 0000000..1d2cd7b --- /dev/null +++ b/source/game/effects/trail_manager.hpp @@ -0,0 +1,57 @@ +// trail_manager.hpp - Estela de partícules daurades darrere la nau +// © 2026 JailDesigner + +#pragma once + +#include + +#include +#include + +#include "core/defaults/trail.hpp" +#include "core/graphics/shape.hpp" +#include "core/rendering/render_context.hpp" +#include "core/types.hpp" +#include "game/entities/ship.hpp" + +namespace Effects { + + // Manté un pool fixe de partícules amb forma d'estrella (star.shp) que + // pulsen entre groc i daurat, oscil·len al voltant del seu naixement i + // encongeixen fins a desaparèixer. Les naus actives emeten quan superen + // SPEED_THRESHOLD_PX_S. + class TrailManager { + public: + explicit TrailManager(Rendering::Renderer* renderer); + + void update(float delta_time, const std::array& ships); + void draw() const; + void reset(); + + [[nodiscard]] auto getActiveCount() const -> int; + + private: + struct Particle { + bool active{false}; + Vec2 origin{}; // punt de naixement (no es desplaça) + float phase_x{0.0F}; // fase oscil·lació horitzontal + float phase_y{0.0F}; + float phase_pulse{0.0F}; + float age{0.0F}; + float lifetime{0.0F}; + float scale{1.0F}; + }; + + void tryEmitFromShip(const Ship& ship, std::size_t player_id, float delta_time); + void emitAt(Vec2 pos); + auto findFreeSlot() -> int; + void drawParticle(const Particle& particle) const; + + Rendering::Renderer* renderer_; + std::shared_ptr star_shape_; + std::array pool_{}; + std::array emit_cooldown_{0.0F, 0.0F}; + float time_accumulator_{0.0F}; + }; + +} // namespace Effects diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index fb3136a..1d4c64c 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -29,6 +29,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) debris_manager_(sdl.getRenderer()), firework_manager_(sdl.getRenderer()), floating_score_manager_(sdl.getRenderer()), + trail_manager_(sdl.getRenderer()), text_(sdl.getRenderer()) { // Recuperar configuración de match des del context match_config_ = context_.getMatchConfig(); @@ -179,6 +180,7 @@ void GameScene::stepPhysics(float delta_time) { for (auto& bullet : bullets_) { bullet.postUpdate(delta_time); } + trail_manager_.update(delta_time, ships_); } void GameScene::stepShootingInput() { @@ -594,6 +596,7 @@ void GameScene::drawInitHudState() { void GameScene::drawLevelStartState() { drawMargins(); + trail_manager_.draw(); drawActiveShipsAlive(); drawBullets(); debris_manager_.draw(); @@ -605,6 +608,7 @@ void GameScene::drawLevelStartState() { void GameScene::drawPlayingState() { drawMargins(); + trail_manager_.draw(); drawActiveShipsAlive(); drawEnemies(); drawBullets(); @@ -616,6 +620,7 @@ void GameScene::drawPlayingState() { void GameScene::drawLevelCompletedState() { drawMargins(); + trail_manager_.draw(); drawActiveShipsAlive(); drawBullets(); debris_manager_.draw(); diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 320c785..6b99b5b 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -19,6 +19,7 @@ #include "game/effects/debris_manager.hpp" #include "game/effects/firework_manager.hpp" #include "game/effects/floating_score_manager.hpp" +#include "game/effects/trail_manager.hpp" #include "game/entities/bullet.hpp" #include "game/entities/enemy.hpp" #include "game/entities/ship.hpp" @@ -56,6 +57,7 @@ class GameScene final : public Scene { Effects::DebrisManager debris_manager_; Effects::FireworkManager firework_manager_; Effects::FloatingScoreManager floating_score_manager_; + Effects::TrailManager trail_manager_; // Estat del juego std::array ships_; // [0]=P1, [1]=P2