diff --git a/source/core/defaults/playfield.hpp b/source/core/defaults/playfield.hpp index fd0146b..87332e4 100644 --- a/source/core/defaults/playfield.hpp +++ b/source/core/defaults/playfield.hpp @@ -25,4 +25,20 @@ namespace Defaults::Playfield { constexpr float HEAD_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border) + // Orbit (oscil·lació transversal de la línia quan la nau hi passa a prop). + constexpr float ORBIT_AMPLITUDE_MAX_PX = 3.0F; // desplaçament transversal màxim + constexpr float ORBIT_DECAY_PER_S = 4.0F; // decaiment de l'amplitud (px/s) + constexpr float ORBIT_FREQ_HZ = 8.0F; // freqüència del sin + constexpr float ORBIT_PROXIMITY_PX = 12.0F; // distància max de la línia per excitar-la + constexpr float ORBIT_SHIP_SPEED_THRESHOLD = 60.0F; // velocitat mínima per excitar (px/s) + + // Pulse (reacció a fireworks: punt brillant que es propaga al llarg de la + // línia a partir del punt de spawn). + constexpr int MAX_PULSES_PER_LINE = 2; + constexpr float PULSE_LIFETIME_S = 1.0F; // temps total fins desaparèixer + constexpr float PULSE_SPREAD_PER_S = 300.0F; // px/s de propagació (cap a cada extrem) + constexpr unsigned char PULSE_COLOR_R = 180; + constexpr unsigned char PULSE_COLOR_G = 230; + constexpr unsigned char PULSE_COLOR_B = 255; + } // namespace Defaults::Playfield diff --git a/source/core/graphics/playfield.cpp b/source/core/graphics/playfield.cpp index 145bd35..9e7f51f 100644 --- a/source/core/graphics/playfield.cpp +++ b/source/core/graphics/playfield.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "core/defaults.hpp" #include "core/rendering/line_renderer.hpp" @@ -20,6 +21,22 @@ namespace Graphics { return 1.0F - (INV * INV * INV); } + // Lerp del color base actual (oscil·lador) cap a un color destí en + // funció de f ∈ [0, 1]. Alpha > 0 perquè line_renderer l'usi directe. + auto lerpColor(SDL_Color target, float f) -> SDL_Color { + const float CLAMPED = std::clamp(f, 0.0F, 1.0F); + const SDL_Color BASE = Rendering::getLineColor(); + const auto LERP_U8 = [&](unsigned char a, unsigned char b) { + const float OUT = (static_cast(a) * (1.0F - CLAMPED)) + (static_cast(b) * CLAMPED); + return static_cast(OUT); + }; + return SDL_Color{ + .r = LERP_U8(BASE.r, target.r), + .g = LERP_U8(BASE.g, target.g), + .b = LERP_U8(BASE.b, target.b), + .a = 255}; + } + } // namespace Playfield::Playfield(Rendering::Renderer* renderer) @@ -29,6 +46,95 @@ namespace Graphics { void Playfield::update(float delta_time) { elapsed_s_ += delta_time; + + // Decau l'orbit i avança la fase del sin per cada línia. + const float ORBIT_DELTA_PHASE = Defaults::Playfield::ORBIT_FREQ_HZ * 2.0F * Defaults::Math::PI * delta_time; + const float ORBIT_DEC = Defaults::Playfield::ORBIT_DECAY_PER_S * delta_time; + for (auto& line : lines_) { + line.orbit_phase += ORBIT_DELTA_PHASE; + line.orbit_amplitude = std::max(0.0F, line.orbit_amplitude - ORBIT_DEC); + + // Avança els pulses; els desactiva quan acaben de vida. + for (auto& pulse : line.pulses) { + if (!pulse.active) { + continue; + } + pulse.age_s += delta_time; + if (pulse.age_s >= Defaults::Playfield::PULSE_LIFETIME_S) { + pulse.active = false; + } + } + } + } + + void Playfield::spawnPulseAt(Line& line, float center_t) { + for (auto& pulse : line.pulses) { + if (!pulse.active) { + pulse.active = true; + pulse.age_s = 0.0F; + pulse.center_t = std::clamp(center_t, 0.0F, 1.0F); + return; + } + } + // Cap slot lliure: substituïm el més vell. + Pulse* oldest = line.pulses.data(); + for (auto& pulse : line.pulses) { + if (pulse.age_s > oldest->age_s) { + oldest = &pulse; + } + } + oldest->active = true; + oldest->age_s = 0.0F; + oldest->center_t = std::clamp(center_t, 0.0F, 1.0F); + } + + void Playfield::notifyFireworkSpawn(Vec2 pos) { + // Línia vertical més propera (per posició x) i horitzontal més propera (per y). + Line* closest_v = nullptr; + Line* closest_h = nullptr; + float min_dx = std::numeric_limits::max(); + float min_dy = std::numeric_limits::max(); + for (auto& line : lines_) { + if (line.is_vertical) { + const float DX = std::abs(pos.x - line.start.x); + if (DX < min_dx) { + min_dx = DX; + closest_v = &line; + } + } else { + const float DY = std::abs(pos.y - line.start.y); + if (DY < min_dy) { + min_dy = DY; + closest_h = &line; + } + } + } + if (closest_v != nullptr) { + const float LINE_LEN = closest_v->end.y - closest_v->start.y; + const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.y - closest_v->start.y) / LINE_LEN : 0.5F; + spawnPulseAt(*closest_v, CENTER_T); + } + if (closest_h != nullptr) { + const float LINE_LEN = closest_h->end.x - closest_h->start.x; + const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.x - closest_h->start.x) / LINE_LEN : 0.5F; + spawnPulseAt(*closest_h, CENTER_T); + } + } + + void Playfield::notifyShipPass(Vec2 pos, float speed_px_s) { + if (speed_px_s < Defaults::Playfield::ORBIT_SHIP_SPEED_THRESHOLD) { + return; + } + const float MAX_DIST = Defaults::Playfield::ORBIT_PROXIMITY_PX; + for (auto& line : lines_) { + // Distància perpendicular del punt a la línia (que és horitzontal o vertical). + const float DIST = line.is_vertical + ? std::abs(pos.x - line.start.x) + : std::abs(pos.y - line.start.y); + if (DIST < MAX_DIST) { + line.orbit_amplitude = Defaults::Playfield::ORBIT_AMPLITUDE_MAX_PX; + } + } } void Playfield::buildLines() { @@ -54,7 +160,11 @@ namespace Graphics { .start = {.x = X, .y = zona.y}, .end = {.x = X, .y = zona.y + zona.h}, .brightness = BRIGHTNESS, - .spawn_time_s = 0.0F}); + .spawn_time_s = 0.0F, + .is_vertical = true, + .orbit_amplitude = 0.0F, + .orbit_phase = 0.0F, + .pulses = {}}); } // Horitzontals: posicions j ∈ [1, SUB_HORIZ-1]. @@ -68,7 +178,11 @@ namespace Graphics { .start = {.x = zona.x, .y = Y}, .end = {.x = zona.x + zona.w, .y = Y}, .brightness = BRIGHTNESS, - .spawn_time_s = 0.0F}); + .spawn_time_s = 0.0F, + .is_vertical = false, + .orbit_amplitude = 0.0F, + .orbit_phase = 0.0F, + .pulses = {}}); } // Ona diagonal: la línia esquerra/superior naix a t=0 i les següents @@ -108,16 +222,23 @@ namespace Graphics { } const float P = easeOutCubic(RAW_P); + // Desplaçament perpendicular per orbit (verticals → x, horitzontals → y). + const float ORBIT_OFFSET = line.orbit_amplitude * std::sin(line.orbit_phase); + const float ORBIT_DX = line.is_vertical ? ORBIT_OFFSET : 0.0F; + const float ORBIT_DY = line.is_vertical ? 0.0F : ORBIT_OFFSET; + + const float START_X = line.start.x + ORBIT_DX; + const float START_Y = line.start.y + ORBIT_DY; const float DX = line.end.x - line.start.x; const float DY = line.end.y - line.start.y; - const float CURRENT_X = line.start.x + (DX * P); - const float CURRENT_Y = line.start.y + (DY * P); + const float CURRENT_X = START_X + (DX * P); + const float CURRENT_Y = START_Y + (DY * P); // Tram base (brillo de la línia). Rendering::linea( renderer_, - static_cast(line.start.x), - static_cast(line.start.y), + static_cast(START_X), + static_cast(START_Y), static_cast(CURRENT_X), static_cast(CURRENT_Y), line.brightness); @@ -127,8 +248,8 @@ namespace Graphics { const float LENGTH = std::sqrt((DX * DX) + (DY * DY)); if (LENGTH > 0.0F) { const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH)); - const float HEAD_X = line.start.x + (DX * HEAD_T); - const float HEAD_Y = line.start.y + (DY * HEAD_T); + const float HEAD_X = START_X + (DX * HEAD_T); + const float HEAD_Y = START_Y + (DY * HEAD_T); Rendering::linea( renderer_, static_cast(HEAD_X), @@ -138,6 +259,46 @@ namespace Graphics { Defaults::Playfield::HEAD_BRIGHTNESS); } } + + // Pulses: cada un és un segment brillant centrat a center_t que + // s'expandeix amb el temps i s'apaga. + const float LINE_LENGTH = std::sqrt((DX * DX) + (DY * DY)); + if (LINE_LENGTH <= 0.0F) { + continue; + } + const SDL_Color PULSE_TARGET = { + .r = Defaults::Playfield::PULSE_COLOR_R, + .g = Defaults::Playfield::PULSE_COLOR_G, + .b = Defaults::Playfield::PULSE_COLOR_B, + .a = 255}; + for (const auto& pulse : line.pulses) { + if (!pulse.active) { + continue; + } + const float HALF_WIDTH_T = (pulse.age_s * Defaults::Playfield::PULSE_SPREAD_PER_S) / LINE_LENGTH; + const float INTENSITY = std::max( + 0.0F, + 1.0F - (pulse.age_s / Defaults::Playfield::PULSE_LIFETIME_S)); + const float T1 = std::clamp(pulse.center_t - HALF_WIDTH_T, 0.0F, 1.0F); + const float T2 = std::clamp(pulse.center_t + HALF_WIDTH_T, 0.0F, 1.0F); + if (T2 <= T1) { + continue; + } + const float P1_X = START_X + (DX * T1); + const float P1_Y = START_Y + (DY * T1); + const float P2_X = START_X + (DX * T2); + const float P2_Y = START_Y + (DY * T2); + const SDL_Color SEG_COLOR = lerpColor(PULSE_TARGET, INTENSITY); + Rendering::linea( + renderer_, + static_cast(P1_X), + static_cast(P1_Y), + static_cast(P2_X), + static_cast(P2_Y), + 1.0F, + 0.0F, + SEG_COLOR); + } } } diff --git a/source/core/graphics/playfield.hpp b/source/core/graphics/playfield.hpp index 5baf9aa..4a037be 100644 --- a/source/core/graphics/playfield.hpp +++ b/source/core/graphics/playfield.hpp @@ -11,8 +11,10 @@ #pragma once +#include #include +#include "core/defaults/playfield.hpp" #include "core/rendering/render_context.hpp" #include "core/types.hpp" @@ -22,22 +24,41 @@ namespace Graphics { public: explicit Playfield(Rendering::Renderer* renderer); - // Avança el timer intern de creació. + // Avança timers interns (creació + reaccions). void update(float delta_time); // Pinta la graella. La porció dibuixada de cada línia depèn del timer intern. void draw() const; + // Notifica que una nau ha passat per (pos) a velocitat (speed_px_s). + // Si està prop d'alguna línia i va prou ràpida, la línia entra en orbit. + void notifyShipPass(Vec2 pos, float speed_px_s); + + // Notifica el spawn d'un firework a (pos). Les línies V i H més properes + // generen un pulse brillant que es propaga. + void notifyFireworkSpawn(Vec2 pos); + private: + struct Pulse { + bool active{false}; + float center_t{0.5F}; // posició al llarg de la línia (0..1) + float age_s{0.0F}; + }; + struct Line { - Vec2 start; // top (verticals) o left (horitzontals) - Vec2 end; // bottom (verticals) o right (horitzontals) - float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS) - float spawn_time_s; // moment de naixement (verticals i horitzontals tenen ritmes independents) + Vec2 start; // top (verticals) o left (horitzontals) + Vec2 end; // bottom (verticals) o right (horitzontals) + float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS) + float spawn_time_s; // moment de naixement + bool is_vertical; // direcció (per saber el perpendicular de l'orbit) + float orbit_amplitude; // amplitud actual de l'orbit (px, ≥ 0) + float orbit_phase; // fase del sin (avança contínuament) + std::array pulses; }; void buildLines(); [[nodiscard]] auto computeLineProgress(const Line& line) const -> float; + static void spawnPulseAt(Line& line, float center_t); Rendering::Renderer* renderer_; std::vector lines_; diff --git a/source/game/effects/firework_manager.cpp b/source/game/effects/firework_manager.cpp index 1e54aad..b2ebfac 100644 --- a/source/game/effects/firework_manager.cpp +++ b/source/game/effects/firework_manager.cpp @@ -70,6 +70,11 @@ namespace Effects { 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; diff --git a/source/game/effects/firework_manager.hpp b/source/game/effects/firework_manager.hpp index 189041e..3f1d04b 100644 --- a/source/game/effects/firework_manager.hpp +++ b/source/game/effects/firework_manager.hpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include "core/defaults.hpp" #include "core/rendering/render_context.hpp" @@ -18,8 +20,15 @@ namespace Effects { // d'`origen`. Cada partícula viu independent (update/draw/rebot). class FireworkManager { public: + // Notificació opcional cada vegada que es genera un burst. + using SpawnCallback = std::function; + explicit FireworkManager(Rendering::Renderer* renderer); + void setSpawnCallback(SpawnCallback callback) { + spawn_callback_ = std::move(callback); + } + // Emet un burst radial: // origen: punt central del burst. // color: color de les línies (heretat del pare). @@ -40,6 +49,7 @@ namespace Effects { private: Rendering::Renderer* renderer_; + SpawnCallback spawn_callback_; static constexpr int POOL_SIZE = Defaults::FX::Firework::POOL_SIZE; std::array pool_; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 3a8e2f5..f76f6df 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -75,6 +75,11 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) border_.bumpAt(hit.contact_point, STRENGTH); }); + // Fireworks generen un pulse a les línies V i H més properes del playfield. + firework_manager_.setSpawnCallback([this](Vec2 origen) { + playfield_.notifyFireworkSpawn(origen); + }); + // Explosions properes a una paret també generen bump (falloff lineal amb la distància). debris_manager_.setExplosionCallback([this](Vec2 center) { const SDL_FRect& zona = Defaults::Zones::PLAYAREA; @@ -210,6 +215,13 @@ void GameScene::stepPhysics(float delta_time) { trail_manager_.update(delta_time, ships_); playfield_.update(delta_time); border_.update(delta_time); + + // Notificar al playfield que la nau ha passat (per excitar línies properes). + for (const auto& ship : ships_) { + if (ship.isActive()) { + playfield_.notifyShipPass(ship.getCenter(), ship.getSpeed()); + } + } } void GameScene::stepShootingInput() {