diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index bedba23..c978a14 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -26,6 +26,7 @@ #include "core/defaults/playfield.hpp" #include "core/defaults/rendering.hpp" #include "core/defaults/ship.hpp" +#include "core/defaults/starfield_parallax.hpp" #include "core/defaults/title.hpp" #include "core/defaults/trail.hpp" #include "core/defaults/window.hpp" diff --git a/source/core/defaults/playfield.hpp b/source/core/defaults/playfield.hpp index 87332e4..512722c 100644 --- a/source/core/defaults/playfield.hpp +++ b/source/core/defaults/playfield.hpp @@ -8,7 +8,7 @@ namespace Defaults::Playfield { // Estructura de la graella (cel·les omplen tota la PLAYAREA) constexpr int COLUMNS = 16; // cell_w = PLAYAREA.w / 16 constexpr int ROWS = 8; // cell_h = PLAYAREA.h / 8 - constexpr int SUBDIVISIONS = 5; // cada cel·la principal es divideix en N subcel·les + constexpr int SUBDIVISIONS = 4; // cada cel·la principal es divideix en N subcel·les // Brillo respecte al color global (border = 1.0) constexpr float GRID_BRIGHTNESS = 0.15F; @@ -25,20 +25,32 @@ 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) + // Ripples: deformacions circulars que travessen la graella com ones d'aigua. + // Cada ripple desplaça radialment cap a fora els vèrtexs de les línies que + // travessa, amb una envoltant que decau a les vores de l'anell i amb el temps. + namespace Ripple { + constexpr int POOL_SIZE = 32; - // 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; + // Ones grans (explosions / fireworks). + constexpr float BIG_AMPLITUDE_PX = 10.0F; + constexpr float BIG_SPEED_PX_S = 320.0F; + constexpr float BIG_LIFETIME_S = 1.4F; + constexpr float BIG_THICKNESS_PX = 40.0F; + + // Ones petites (pas de nau, cadència estil trail). + constexpr float SMALL_AMPLITUDE_PX = 2.5F; + constexpr float SMALL_SPEED_PX_S = 160.0F; + constexpr float SMALL_LIFETIME_S = 0.55F; + constexpr float SMALL_THICKNESS_PX = 18.0F; + + // Cadència "soltar gotetes" per nau (patró TrailManager). + constexpr float SHIP_COOLDOWN_S = 0.10F; + constexpr float SHIP_COOLDOWN_JITTER_S = 0.03F; + constexpr float SHIP_SPEED_THRESHOLD_PX_S = 80.0F; + + // Subdivisió de línies quan estan dins una ripple. + constexpr int MAIN_SEGMENTS = 24; // línies principals + constexpr int SUB_SEGMENTS = 12; // sub-graella + } // namespace Ripple } // namespace Defaults::Playfield diff --git a/source/core/defaults/starfield_parallax.hpp b/source/core/defaults/starfield_parallax.hpp new file mode 100644 index 0000000..76fca6b --- /dev/null +++ b/source/core/defaults/starfield_parallax.hpp @@ -0,0 +1,36 @@ +// starfield_parallax.hpp - Capa de fons del playfield: estrelles 2D amb parallax +// © 2026 JailDesigner +// +// 3 capes de profunditat. Cada capa té estrelles amb brillantor, mida i +// factor parallax propis. Les més properes són més brillants i grans i es +// mouen més ràpid quan el món es desplaça; les més llunyanes són tènues i +// petites i amb prou feines es mouen. + +#pragma once + +namespace Defaults::StarfieldParallax { + + namespace Far { + constexpr int COUNT = 60; + constexpr float BRIGHTNESS = 0.15F; + constexpr float PARALLAX_FACTOR = 0.15F; // multiplicador sobre world_velocity + constexpr int SIZE_PX = 1; // 1 px (punt) + } // namespace Far + + namespace Mid { + constexpr int COUNT = 50; + constexpr float BRIGHTNESS = 0.30F; + constexpr float PARALLAX_FACTOR = 0.35F; + constexpr int SIZE_PX = 2; // creu de 3x3 (extensió ±1) + } // namespace Mid + + namespace Near { + constexpr int COUNT = 40; + constexpr float BRIGHTNESS = 0.55F; + constexpr float PARALLAX_FACTOR = 0.70F; + constexpr int SIZE_PX = 3; // creu de 5x5 (extensió ±2) + } // namespace Near + + constexpr int TOTAL_COUNT = Far::COUNT + Mid::COUNT + Near::COUNT; + +} // namespace Defaults::StarfieldParallax diff --git a/source/core/graphics/playfield.cpp b/source/core/graphics/playfield.cpp index 9e7f51f..8d74d42 100644 --- a/source/core/graphics/playfield.cpp +++ b/source/core/graphics/playfield.cpp @@ -5,8 +5,8 @@ #include #include +#include #include -#include #include "core/defaults.hpp" #include "core/rendering/line_renderer.hpp" @@ -21,20 +21,38 @@ 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}; + 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)); + } + + // Desplaçament radial acumulat al punt (px, py) sumant totes les ripples + // que el toquen. Retorna {dx, dy} a sumar a la posició original. + auto computeRippleDisplacement(float px, float py, const Playfield::Ripple* const* hits, int n_hits) -> Vec2 { + float dx_total = 0.0F; + float dy_total = 0.0F; + for (int i = 0; i < n_hits; i++) { + const auto& r = *hits[i]; + const float RADIUS = r.age_s * r.speed_px_s; + const float THICKNESS = r.thickness_px; + const float DX = px - r.center.x; + const float DY = py - r.center.y; + const float D = std::sqrt((DX * DX) + (DY * DY)); + if (D < 0.001F) { + continue; // centre exacte: no hi ha direcció radial + } + const float PHASE = (D - RADIUS) / THICKNESS; + if (std::fabs(PHASE) >= 1.0F) { + continue; // fora de l'anell d'aquesta ripple + } + const float ENVELOPE = std::cos(PHASE * Defaults::Math::PI * 0.5F); + const float AMP_EFF = r.amplitude_px * (1.0F - (r.age_s / r.lifetime_s)); + const float UX = DX / D; + const float UY = DY / D; + dx_total += UX * AMP_EFF * ENVELOPE; + dy_total += UY * AMP_EFF * ENVELOPE; + } + return Vec2{.x = dx_total, .y = dy_total}; } } // namespace @@ -46,95 +64,80 @@ 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; - } + for (auto& ripple : ripples_) { + if (!ripple.active) { + continue; + } + ripple.age_s += delta_time; + if (ripple.age_s >= ripple.lifetime_s) { + ripple.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; + auto Playfield::findFreeRipple() -> Ripple* { + Ripple* oldest = nullptr; + for (auto& ripple : ripples_) { + if (!ripple.active) { + return &ripple; + } + if (oldest == nullptr || ripple.age_s > oldest->age_s) { + oldest = &ripple; } } - // 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); + return oldest; // pool ple: substituïm la més vella } - 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) { + void Playfield::spawnBig(Vec2 pos) { + Ripple* r = findFreeRipple(); + if (r == nullptr) { 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; - } + r->center = pos; + r->age_s = 0.0F; + r->lifetime_s = Defaults::Playfield::Ripple::BIG_LIFETIME_S; + r->speed_px_s = Defaults::Playfield::Ripple::BIG_SPEED_PX_S; + r->amplitude_px = Defaults::Playfield::Ripple::BIG_AMPLITUDE_PX; + r->thickness_px = Defaults::Playfield::Ripple::BIG_THICKNESS_PX; + r->active = true; + } + + void Playfield::spawnSmall(Vec2 pos) { + Ripple* r = findFreeRipple(); + if (r == nullptr) { + return; } + r->center = pos; + r->age_s = 0.0F; + r->lifetime_s = Defaults::Playfield::Ripple::SMALL_LIFETIME_S; + r->speed_px_s = Defaults::Playfield::Ripple::SMALL_SPEED_PX_S; + r->amplitude_px = Defaults::Playfield::Ripple::SMALL_AMPLITUDE_PX; + r->thickness_px = Defaults::Playfield::Ripple::SMALL_THICKNESS_PX; + r->active = true; + } + + void Playfield::notifyExplosion(Vec2 pos) { + spawnBig(pos); + } + + void Playfield::notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time) { + if (player_id >= ship_ripple_cooldown_.size()) { + return; + } + if (speed_px_s < Defaults::Playfield::Ripple::SHIP_SPEED_THRESHOLD_PX_S) { + ship_ripple_cooldown_[player_id] = 0.0F; + return; + } + ship_ripple_cooldown_[player_id] -= delta_time; + if (ship_ripple_cooldown_[player_id] > 0.0F) { + return; + } + spawnSmall(pos); + const float JITTER = randUniform( + -Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S, + Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S); + ship_ripple_cooldown_[player_id] = + Defaults::Playfield::Ripple::SHIP_COOLDOWN_S + JITTER; } void Playfield::buildLines() { @@ -161,10 +164,7 @@ namespace Graphics { .end = {.x = X, .y = zona.y + zona.h}, .brightness = BRIGHTNESS, .spawn_time_s = 0.0F, - .is_vertical = true, - .orbit_amplitude = 0.0F, - .orbit_phase = 0.0F, - .pulses = {}}); + .is_vertical = true}); } // Horitzontals: posicions j ∈ [1, SUB_HORIZ-1]. @@ -179,10 +179,7 @@ namespace Graphics { .end = {.x = zona.x + zona.w, .y = Y}, .brightness = BRIGHTNESS, .spawn_time_s = 0.0F, - .is_vertical = false, - .orbit_amplitude = 0.0F, - .orbit_phase = 0.0F, - .pulses = {}}); + .is_vertical = false}); } // Ona diagonal: la línia esquerra/superior naix a t=0 i les següents @@ -215,90 +212,100 @@ namespace Graphics { } void Playfield::draw() const { + // Recollir ripples actives (punters per accés ràpid al hot loop). + std::array active{}; + int n_active = 0; + for (const auto& ripple : ripples_) { + if (ripple.active) { + active[n_active++] = &ripple; + } + } for (const auto& line : lines_) { - const float RAW_P = computeLineProgress(line); - if (RAW_P <= 0.0F) { + drawLine(line, active.data(), n_active); + } + } + + void Playfield::drawLine(const Line& line, const Ripple* const* active, int n_active) const { + const float RAW_P = computeLineProgress(line); + if (RAW_P <= 0.0F) { + return; + } + const float P = easeOutCubic(RAW_P); + + const float START_X = line.start.x; + const float START_Y = line.start.y; + const float DX = line.end.x - line.start.x; + const float DY = line.end.y - line.start.y; + const float END_X = START_X + (DX * P); + const float END_Y = START_Y + (DY * P); + + // AABB de la porció visible de la línia + filtre de ripples. + const float LINE_MIN_X = std::min(START_X, END_X); + const float LINE_MAX_X = std::max(START_X, END_X); + const float LINE_MIN_Y = std::min(START_Y, END_Y); + const float LINE_MAX_Y = std::max(START_Y, END_Y); + std::array hits{}; + int n_hits = 0; + for (int i = 0; i < n_active; i++) { + const auto& r = *active[i]; + const float R_MAX = (r.age_s * r.speed_px_s) + r.thickness_px; + if ((r.center.x + R_MAX) < LINE_MIN_X || (r.center.x - R_MAX) > LINE_MAX_X || + (r.center.y + R_MAX) < LINE_MIN_Y || (r.center.y - R_MAX) > LINE_MAX_Y) { continue; } - const float P = easeOutCubic(RAW_P); + hits[n_hits++] = &r; + } - // 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 = START_X + (DX * P); - const float CURRENT_Y = START_Y + (DY * P); - - // Tram base (brillo de la línia). + if (n_hits == 0) { + // Camí ràpid: una sola crida com abans. Rendering::linea( renderer_, static_cast(START_X), static_cast(START_Y), - static_cast(CURRENT_X), - static_cast(CURRENT_Y), + static_cast(END_X), + static_cast(END_Y), line.brightness); - - // Cap brillant mentre creix: l'últim tram de la línia es repinta més brillant. + // Cap brillant mentre creix. if (P < 1.0F) { 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 = START_X + (DX * HEAD_T); - const float HEAD_Y = START_Y + (DY * HEAD_T); Rendering::linea( renderer_, - static_cast(HEAD_X), - static_cast(HEAD_Y), - static_cast(CURRENT_X), - static_cast(CURRENT_Y), + static_cast(START_X + (DX * HEAD_T)), + static_cast(START_Y + (DY * HEAD_T)), + static_cast(END_X), + static_cast(END_Y), Defaults::Playfield::HEAD_BRIGHTNESS); } } + return; + } - // 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); - } + // Camí deformat: subdividir en N segments i desplaçar cada vèrtex. + const bool IS_MAIN = line.brightness >= Defaults::Playfield::GRID_BRIGHTNESS; + const int N = IS_MAIN + ? Defaults::Playfield::Ripple::MAIN_SEGMENTS + : Defaults::Playfield::Ripple::SUB_SEGMENTS; + const Vec2 D0 = computeRippleDisplacement(START_X, START_Y, hits.data(), n_hits); + float prev_x = START_X + D0.x; + float prev_y = START_Y + D0.y; + for (int i = 1; i <= N; i++) { + const float T = static_cast(i) / static_cast(N); + const float X = START_X + (DX * P * T); + const float Y = START_Y + (DY * P * T); + const Vec2 D = computeRippleDisplacement(X, Y, hits.data(), n_hits); + const float NX = X + D.x; + const float NY = Y + D.y; + Rendering::linea( + renderer_, + static_cast(prev_x), + static_cast(prev_y), + static_cast(NX), + static_cast(NY), + line.brightness); + prev_x = NX; + prev_y = NY; } } diff --git a/source/core/graphics/playfield.hpp b/source/core/graphics/playfield.hpp index 4a037be..ba0216b 100644 --- a/source/core/graphics/playfield.hpp +++ b/source/core/graphics/playfield.hpp @@ -5,13 +5,16 @@ // rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció // li toca dibuixar segons el seu slot a la timeline. // -// Disseny preparat per a futures capacitats: -// - Línies "vives" que reaccionen a explosions / pas de la nau (reaction_intensity). -// - Capes addicionals al fons (estrelles, gradients, scanlines). +// Reaccions disponibles: +// - Ripples: deformacions circulars (ones d'aigua) que travessen la graella. +// Disparades per explosions (grans) i pas de la nau (petites, cadència estil +// trail). Cada vèrtex d'una línia afectada es desplaça radialment cap a fora +// amb una envoltant en cos(·) que decau a les vores de l'anell i amb el temps. #pragma once #include +#include #include #include "core/defaults/playfield.hpp" @@ -24,44 +27,51 @@ namespace Graphics { public: explicit Playfield(Rendering::Renderer* renderer); - // Avança timers interns (creació + reaccions). + // Avança timers interns (creació + ripples). void update(float delta_time); - // Pinta la graella. La porció dibuixada de cada línia depèn del timer intern. + // Pinta la graella. La porció dibuixada de cada línia depèn del timer intern, + // i s'aplica deformació radial per cada ripple activa que afecti la línia. 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 que una nau ha passat per (pos) a (speed_px_s). Genera ones + // petites darrere la nau a cadència regular amb jitter (estil TrailManager). + void notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time); - // 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); + // Notifica una explosió a (pos): genera una ripple gran centrada al punt. + void notifyExplosion(Vec2 pos); - private: - struct Pulse { - bool active{false}; - float center_t{0.5F}; // posició al llarg de la línia (0..1) + // Pública per accés des d'helpers a l'anonymous namespace del .cpp. + struct Ripple { + Vec2 center{}; float age_s{0.0F}; + float lifetime_s{0.0F}; + float speed_px_s{0.0F}; + float amplitude_px{0.0F}; + float thickness_px{0.0F}; + bool active{false}; }; + private: 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 - 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; + 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ó }; void buildLines(); + void drawLine(const Line& line, const Ripple* const* active, int n_active) const; [[nodiscard]] auto computeLineProgress(const Line& line) const -> float; - static void spawnPulseAt(Line& line, float center_t); + void spawnBig(Vec2 pos); + void spawnSmall(Vec2 pos); + auto findFreeRipple() -> Ripple*; Rendering::Renderer* renderer_; std::vector lines_; + std::array ripples_{}; + std::array ship_ripple_cooldown_{}; float elapsed_s_{0.0F}; }; diff --git a/source/core/graphics/starfield_parallax.cpp b/source/core/graphics/starfield_parallax.cpp new file mode 100644 index 0000000..32b4da3 --- /dev/null +++ b/source/core/graphics/starfield_parallax.cpp @@ -0,0 +1,145 @@ +// starfield_parallax.cpp - Implementació del starfield 2D amb parallax +// © 2026 JailDesigner + +#include "core/graphics/starfield_parallax.hpp" + +#include + +#include "core/defaults.hpp" +#include "core/rendering/line_renderer.hpp" + +namespace Graphics { + + namespace { + + 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)); + } + + } // namespace + + StarfieldParallax::StarfieldParallax(Rendering::Renderer* renderer) + : renderer_(renderer) { + buildStars(); + } + + void StarfieldParallax::buildStars() { + const SDL_FRect& zona = Defaults::Zones::PLAYAREA; + const float MIN_X = zona.x; + const float MAX_X = zona.x + zona.w; + const float MIN_Y = zona.y; + const float MAX_Y = zona.y + zona.h; + + // Tint aleatori entre blanc (255,255,255) i cyan (0,255,255) per estrella. + // T ∈ [0,1]: 0 → blanc; 1 → cyan. R = 255·(1-T), G=B=255. + const auto FILL_LAYER = [&](int layer, int count, int& idx) { + for (int i = 0; i < count; i++) { + const float T = randUniform(0.0F, 1.0F); + stars_[idx++] = Star{ + .x = randUniform(MIN_X, MAX_X), + .y = randUniform(MIN_Y, MAX_Y), + .layer = layer, + .color = SDL_Color{ + .r = static_cast(255.0F * (1.0F - T)), + .g = 255, + .b = 255, + .a = 255}}; + } + }; + + int idx = 0; + FILL_LAYER(0, Defaults::StarfieldParallax::Far::COUNT, idx); + FILL_LAYER(1, Defaults::StarfieldParallax::Mid::COUNT, idx); + FILL_LAYER(2, Defaults::StarfieldParallax::Near::COUNT, idx); + } + + auto StarfieldParallax::layerBrightness(int layer) -> float { + switch (layer) { + case 0: + return Defaults::StarfieldParallax::Far::BRIGHTNESS; + case 1: + return Defaults::StarfieldParallax::Mid::BRIGHTNESS; + case 2: + return Defaults::StarfieldParallax::Near::BRIGHTNESS; + default: + return 0.0F; + } + } + + auto StarfieldParallax::layerParallax(int layer) -> float { + switch (layer) { + case 0: + return Defaults::StarfieldParallax::Far::PARALLAX_FACTOR; + case 1: + return Defaults::StarfieldParallax::Mid::PARALLAX_FACTOR; + case 2: + return Defaults::StarfieldParallax::Near::PARALLAX_FACTOR; + default: + return 0.0F; + } + } + + auto StarfieldParallax::layerSize(int layer) -> int { + switch (layer) { + case 0: + return Defaults::StarfieldParallax::Far::SIZE_PX; + case 1: + return Defaults::StarfieldParallax::Mid::SIZE_PX; + case 2: + return Defaults::StarfieldParallax::Near::SIZE_PX; + default: + return 1; + } + } + + void StarfieldParallax::update(float delta_time, Vec2 world_velocity) { + const SDL_FRect& zona = Defaults::Zones::PLAYAREA; + const float MIN_X = zona.x; + const float MAX_X = zona.x + zona.w; + const float MIN_Y = zona.y; + const float MAX_Y = zona.y + zona.h; + const float W = zona.w; + const float H = zona.h; + + for (auto& star : stars_) { + const float FACTOR = layerParallax(star.layer); + star.x += world_velocity.x * FACTOR * delta_time; + star.y += world_velocity.y * FACTOR * delta_time; + + // Wraparound (PLAYAREA torica). + while (star.x < MIN_X) { + star.x += W; + } + while (star.x > MAX_X) { + star.x -= W; + } + while (star.y < MIN_Y) { + star.y += H; + } + while (star.y > MAX_Y) { + star.y -= H; + } + } + } + + void StarfieldParallax::draw() const { + for (const auto& star : stars_) { + const float B = layerBrightness(star.layer); + const int SIZE = layerSize(star.layer); + const int X = static_cast(star.x); + const int Y = static_cast(star.y); + + if (SIZE <= 1) { + // Punt d'1 px: línia degenerada horitzontal de 1 px. + Rendering::linea(renderer_, X, Y, X + 1, Y, B, 0.0F, star.color); + } else { + // Creu "+" amb extensió HALF des del centre en cada direcció. + const int HALF = SIZE - 1; // SIZE=2 → ±1 (creu 3x3); SIZE=3 → ±2 (creu 5x5) + Rendering::linea(renderer_, X - HALF, Y, X + HALF + 1, Y, B, 0.0F, star.color); + Rendering::linea(renderer_, X, Y - HALF, X, Y + HALF + 1, B, 0.0F, star.color); + } + } + } + +} // namespace Graphics diff --git a/source/core/graphics/starfield_parallax.hpp b/source/core/graphics/starfield_parallax.hpp new file mode 100644 index 0000000..7378708 --- /dev/null +++ b/source/core/graphics/starfield_parallax.hpp @@ -0,0 +1,51 @@ +// starfield_parallax.hpp - Capa més profunda del fons: estrelles 2D amb parallax +// © 2026 JailDesigner +// +// Estrelles 2D distribuïdes en 3 capes de profunditat. Cada capa té el seu +// factor parallax: el "món" es desplaça amb world_velocity i les estrelles +// d'una capa es mouen amb world_velocity * parallax_factor. Les més +// properes es mouen més (factor alt) → sensació de profunditat. +// Quan una estrella surt de PLAYAREA, reapareix per la banda oposada +// (wraparound). + +#pragma once + +#include + +#include + +#include "core/defaults/starfield_parallax.hpp" +#include "core/rendering/render_context.hpp" +#include "core/types.hpp" + +namespace Graphics { + + class StarfieldParallax { + public: + explicit StarfieldParallax(Rendering::Renderer* renderer); + + // Avança el desplaçament de les estrelles segons world_velocity (vector + // del moviment del món en px/s; típicament = -ship_velocity). + // world_velocity == {0, 0} → estrelles quietes. + void update(float delta_time, Vec2 world_velocity); + + void draw() const; + + private: + struct Star { + float x{0.0F}; + float y{0.0F}; + int layer{0}; // 0=Far, 1=Mid, 2=Near + SDL_Color color{}; // tint precomputat entre blanc i cyan + }; + + void buildStars(); + static auto layerBrightness(int layer) -> float; + static auto layerParallax(int layer) -> float; + static auto layerSize(int layer) -> int; + + Rendering::Renderer* renderer_; + std::array stars_{}; + }; + +} // namespace Graphics diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index b7b35ca..e0282df 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -30,6 +30,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) floating_score_manager_(sdl.getRenderer()), trail_manager_(sdl.getRenderer()), text_(sdl.getRenderer()), + starfield_parallax_(sdl.getRenderer()), playfield_(sdl.getRenderer()), border_(sdl.getRenderer()) { // Recuperar configuración de match des del context @@ -75,9 +76,9 @@ 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. + // Fireworks generen una ripple gran al playfield (ona d'aigua centrada al burst). firework_manager_.setSpawnCallback([this](Vec2 origen) { - playfield_.notifyFireworkSpawn(origen); + playfield_.notifyExplosion(origen); }); // Explosions properes a una paret també generen bump (falloff lineal amb la distància). @@ -213,13 +214,34 @@ void GameScene::stepPhysics(float delta_time) { bullet.postUpdate(delta_time); } trail_manager_.update(delta_time, ships_); + // Starfield: world_velocity = -mitjana_de_naus_actives. Si dues naus van en + // sentits oposats, es cancel·len → estrelles quietes (cap jugador "guanya"). + // Si només n'hi ha una activa, segueix la seva velocitat. + Vec2 ship_vel_avg{.x = 0.0F, .y = 0.0F}; + int n_active = 0; + for (const auto& ship : ships_) { + if (ship.isActive()) { + const Vec2 V = ship.getVelocityVector(); + ship_vel_avg.x += V.x; + ship_vel_avg.y += V.y; + n_active++; + } + } + if (n_active > 0) { + ship_vel_avg.x /= static_cast(n_active); + ship_vel_avg.y /= static_cast(n_active); + } + starfield_parallax_.update(delta_time, Vec2{.x = -ship_vel_avg.x, .y = -ship_vel_avg.y}); 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()); + // Notificar al playfield que la nau es mou (genera ripples petites a cadència). + for (std::size_t id = 0; id < ships_.size(); id++) { + if (ships_[id].isActive()) { + playfield_.notifyShipMoving(static_cast(id), + ships_[id].getCenter(), + ships_[id].getSpeed(), + delta_time); } } } @@ -566,6 +588,7 @@ void GameScene::drawActiveShipsAlive() const { } void GameScene::drawContinueState() { + starfield_parallax_.draw(); border_.draw(); drawEnemies(); drawBullets(); @@ -577,6 +600,7 @@ void GameScene::drawContinueState() { } void GameScene::drawGameOverState() { + starfield_parallax_.draw(); border_.draw(); drawEnemies(); drawBullets(); @@ -622,6 +646,8 @@ void GameScene::drawInitHudState() { Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP2_RATIO_END); + // Capa de fons més profunda: estrelles 2D (apareixen senceres des del frame 0). + starfield_parallax_.draw(); // Graella de fons al darrere (timer intern propi, cobreix tot l'INIT_HUD). playfield_.draw(); @@ -647,6 +673,7 @@ void GameScene::drawInitHudState() { } void GameScene::drawLevelStartState() { + starfield_parallax_.draw(); playfield_.draw(); border_.draw(); trail_manager_.draw(); @@ -660,6 +687,7 @@ void GameScene::drawLevelStartState() { } void GameScene::drawPlayingState() { + starfield_parallax_.draw(); playfield_.draw(); border_.draw(); trail_manager_.draw(); @@ -673,6 +701,7 @@ void GameScene::drawPlayingState() { } void GameScene::drawLevelCompletedState() { + starfield_parallax_.draw(); playfield_.draw(); border_.draw(); trail_manager_.draw(); diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 9083f37..09688d8 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -10,6 +10,7 @@ #include "core/graphics/border.hpp" #include "core/graphics/playfield.hpp" +#include "core/graphics/starfield_parallax.hpp" #include "core/graphics/vector_text.hpp" #include "core/physics/physics_world.hpp" #include "core/rendering/sdl_manager.hpp" @@ -82,6 +83,9 @@ class GameScene final : public Scene { // Text vectorial Graphics::VectorText text_; + // Capa més profunda del fons: estrelles 2D amb parallax (estàtiques de moment). + Graphics::StarfieldParallax starfield_parallax_; + // Fons del playfield (graella + futures capes) Graphics::Playfield playfield_;