feat(playfield): ones d'aigua a la rejilla per explosions i pas de nau

This commit is contained in:
2026-05-22 19:22:09 +02:00
parent 044a3a3bbf
commit 88b002b277
4 changed files with 242 additions and 210 deletions
+26 -14
View File
@@ -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_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant
constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border) 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). // Ripples: deformacions circulars que travessen la graella com ones d'aigua.
constexpr float ORBIT_AMPLITUDE_MAX_PX = 3.0F; // desplaçament transversal màxim // Cada ripple desplaça radialment cap a fora els vèrtexs de les línies que
constexpr float ORBIT_DECAY_PER_S = 4.0F; // decaiment de l'amplitud (px/s) // travessa, amb una envoltant que decau a les vores de l'anell i amb el temps.
constexpr float ORBIT_FREQ_HZ = 8.0F; // freqüència del sin namespace Ripple {
constexpr float ORBIT_PROXIMITY_PX = 12.0F; // distància max de la línia per excitar-la constexpr int POOL_SIZE = 32;
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 // Ones grans (explosions / fireworks).
// línia a partir del punt de spawn). constexpr float BIG_AMPLITUDE_PX = 10.0F;
constexpr int MAX_PULSES_PER_LINE = 2; constexpr float BIG_SPEED_PX_S = 320.0F;
constexpr float PULSE_LIFETIME_S = 1.0F; // temps total fins desaparèixer constexpr float BIG_LIFETIME_S = 1.4F;
constexpr float PULSE_SPREAD_PER_S = 300.0F; // px/s de propagació (cap a cada extrem) constexpr float BIG_THICKNESS_PX = 40.0F;
constexpr unsigned char PULSE_COLOR_R = 180;
constexpr unsigned char PULSE_COLOR_G = 230; // Ones petites (pas de nau, cadència estil trail).
constexpr unsigned char PULSE_COLOR_B = 255; 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 } // namespace Defaults::Playfield
+163 -156
View File
@@ -5,8 +5,8 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <limits>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
@@ -21,20 +21,38 @@ namespace Graphics {
return 1.0F - (INV * INV * INV); return 1.0F - (INV * INV * INV);
} }
// Lerp del color base actual (oscil·lador) cap a un color destí en auto randUniform(float min_v, float max_v) -> float {
// funció de f ∈ [0, 1]. Alpha > 0 perquè line_renderer l'usi directe. const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
auto lerpColor(SDL_Color target, float f) -> SDL_Color { return min_v + (NORM * (max_v - min_v));
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) { // Desplaçament radial acumulat al punt (px, py) sumant totes les ripples
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED); // que el toquen. Retorna {dx, dy} a sumar a la posició original.
return static_cast<unsigned char>(OUT); auto computeRippleDisplacement(float px, float py, const Playfield::Ripple* const* hits, int n_hits) -> Vec2 {
}; float dx_total = 0.0F;
return SDL_Color{ float dy_total = 0.0F;
.r = LERP_U8(BASE.r, target.r), for (int i = 0; i < n_hits; i++) {
.g = LERP_U8(BASE.g, target.g), const auto& r = *hits[i];
.b = LERP_U8(BASE.b, target.b), const float RADIUS = r.age_s * r.speed_px_s;
.a = 255}; 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 } // namespace
@@ -46,95 +64,80 @@ namespace Graphics {
void Playfield::update(float delta_time) { void Playfield::update(float delta_time) {
elapsed_s_ += delta_time; elapsed_s_ += delta_time;
for (auto& ripple : ripples_) {
// Decau l'orbit i avança la fase del sin per cada línia. if (!ripple.active) {
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; continue;
} }
pulse.age_s += delta_time; ripple.age_s += delta_time;
if (pulse.age_s >= Defaults::Playfield::PULSE_LIFETIME_S) { if (ripple.age_s >= ripple.lifetime_s) {
pulse.active = false; ripple.active = false;
}
} }
} }
} }
void Playfield::spawnPulseAt(Line& line, float center_t) { auto Playfield::findFreeRipple() -> Ripple* {
for (auto& pulse : line.pulses) { Ripple* oldest = nullptr;
if (!pulse.active) { for (auto& ripple : ripples_) {
pulse.active = true; if (!ripple.active) {
pulse.age_s = 0.0F; return &ripple;
pulse.center_t = std::clamp(center_t, 0.0F, 1.0F); }
if (oldest == nullptr || ripple.age_s > oldest->age_s) {
oldest = &ripple;
}
}
return oldest; // pool ple: substituïm la més vella
}
void Playfield::spawnBig(Vec2 pos) {
Ripple* r = findFreeRipple();
if (r == nullptr) {
return; return;
} }
} r->center = pos;
// Cap slot lliure: substituïm el més vell. r->age_s = 0.0F;
Pulse* oldest = line.pulses.data(); r->lifetime_s = Defaults::Playfield::Ripple::BIG_LIFETIME_S;
for (auto& pulse : line.pulses) { r->speed_px_s = Defaults::Playfield::Ripple::BIG_SPEED_PX_S;
if (pulse.age_s > oldest->age_s) { r->amplitude_px = Defaults::Playfield::Ripple::BIG_AMPLITUDE_PX;
oldest = &pulse; r->thickness_px = Defaults::Playfield::Ripple::BIG_THICKNESS_PX;
} r->active = true;
}
oldest->active = true;
oldest->age_s = 0.0F;
oldest->center_t = std::clamp(center_t, 0.0F, 1.0F);
} }
void Playfield::notifyFireworkSpawn(Vec2 pos) { void Playfield::spawnSmall(Vec2 pos) {
// Línia vertical més propera (per posició x) i horitzontal més propera (per y). Ripple* r = findFreeRipple();
Line* closest_v = nullptr; if (r == nullptr) {
Line* closest_h = nullptr;
float min_dx = std::numeric_limits<float>::max();
float min_dy = std::numeric_limits<float>::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; return;
} }
const float MAX_DIST = Defaults::Playfield::ORBIT_PROXIMITY_PX; r->center = pos;
for (auto& line : lines_) { r->age_s = 0.0F;
// Distància perpendicular del punt a la línia (que és horitzontal o vertical). r->lifetime_s = Defaults::Playfield::Ripple::SMALL_LIFETIME_S;
const float DIST = line.is_vertical r->speed_px_s = Defaults::Playfield::Ripple::SMALL_SPEED_PX_S;
? std::abs(pos.x - line.start.x) r->amplitude_px = Defaults::Playfield::Ripple::SMALL_AMPLITUDE_PX;
: std::abs(pos.y - line.start.y); r->thickness_px = Defaults::Playfield::Ripple::SMALL_THICKNESS_PX;
if (DIST < MAX_DIST) { r->active = true;
line.orbit_amplitude = Defaults::Playfield::ORBIT_AMPLITUDE_MAX_PX;
} }
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() { void Playfield::buildLines() {
@@ -161,10 +164,7 @@ namespace Graphics {
.end = {.x = X, .y = zona.y + zona.h}, .end = {.x = X, .y = zona.y + zona.h},
.brightness = BRIGHTNESS, .brightness = BRIGHTNESS,
.spawn_time_s = 0.0F, .spawn_time_s = 0.0F,
.is_vertical = true, .is_vertical = true});
.orbit_amplitude = 0.0F,
.orbit_phase = 0.0F,
.pulses = {}});
} }
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1]. // Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
@@ -179,10 +179,7 @@ namespace Graphics {
.end = {.x = zona.x + zona.w, .y = Y}, .end = {.x = zona.x + zona.w, .y = Y},
.brightness = BRIGHTNESS, .brightness = BRIGHTNESS,
.spawn_time_s = 0.0F, .spawn_time_s = 0.0F,
.is_vertical = false, .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 // 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 { void Playfield::draw() const {
// Recollir ripples actives (punters per accés ràpid al hot loop).
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> active{};
int n_active = 0;
for (const auto& ripple : ripples_) {
if (ripple.active) {
active[n_active++] = &ripple;
}
}
for (const auto& line : lines_) { for (const auto& line : lines_) {
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); const float RAW_P = computeLineProgress(line);
if (RAW_P <= 0.0F) { if (RAW_P <= 0.0F) {
continue; return;
} }
const float P = easeOutCubic(RAW_P); const float P = easeOutCubic(RAW_P);
// Desplaçament perpendicular per orbit (verticals → x, horitzontals → y). const float START_X = line.start.x;
const float ORBIT_OFFSET = line.orbit_amplitude * std::sin(line.orbit_phase); const float START_Y = line.start.y;
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 DX = line.end.x - line.start.x;
const float DY = line.end.y - line.start.y; const float DY = line.end.y - line.start.y;
const float CURRENT_X = START_X + (DX * P); const float END_X = START_X + (DX * P);
const float CURRENT_Y = START_Y + (DY * P); const float END_Y = START_Y + (DY * P);
// Tram base (brillo de la línia). // 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<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> 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;
}
hits[n_hits++] = &r;
}
if (n_hits == 0) {
// Camí ràpid: una sola crida com abans.
Rendering::linea( Rendering::linea(
renderer_, renderer_,
static_cast<int>(START_X), static_cast<int>(START_X),
static_cast<int>(START_Y), static_cast<int>(START_Y),
static_cast<int>(CURRENT_X), static_cast<int>(END_X),
static_cast<int>(CURRENT_Y), static_cast<int>(END_Y),
line.brightness); line.brightness);
// Cap brillant mentre creix.
// Cap brillant mentre creix: l'últim tram de la línia es repinta més brillant.
if (P < 1.0F) { if (P < 1.0F) {
const float LENGTH = std::sqrt((DX * DX) + (DY * DY)); const float LENGTH = std::sqrt((DX * DX) + (DY * DY));
if (LENGTH > 0.0F) { if (LENGTH > 0.0F) {
const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH)); 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( Rendering::linea(
renderer_, renderer_,
static_cast<int>(HEAD_X), static_cast<int>(START_X + (DX * HEAD_T)),
static_cast<int>(HEAD_Y), static_cast<int>(START_Y + (DY * HEAD_T)),
static_cast<int>(CURRENT_X), static_cast<int>(END_X),
static_cast<int>(CURRENT_Y), static_cast<int>(END_Y),
Defaults::Playfield::HEAD_BRIGHTNESS); Defaults::Playfield::HEAD_BRIGHTNESS);
} }
} }
return;
}
// Pulses: cada un és un segment brillant centrat a center_t que // Camí deformat: subdividir en N segments i desplaçar cada vèrtex.
// s'expandeix amb el temps i s'apaga. const bool IS_MAIN = line.brightness >= Defaults::Playfield::GRID_BRIGHTNESS;
const float LINE_LENGTH = std::sqrt((DX * DX) + (DY * DY)); const int N = IS_MAIN
if (LINE_LENGTH <= 0.0F) { ? Defaults::Playfield::Ripple::MAIN_SEGMENTS
continue; : Defaults::Playfield::Ripple::SUB_SEGMENTS;
} const Vec2 D0 = computeRippleDisplacement(START_X, START_Y, hits.data(), n_hits);
const SDL_Color PULSE_TARGET = { float prev_x = START_X + D0.x;
.r = Defaults::Playfield::PULSE_COLOR_R, float prev_y = START_Y + D0.y;
.g = Defaults::Playfield::PULSE_COLOR_G, for (int i = 1; i <= N; i++) {
.b = Defaults::Playfield::PULSE_COLOR_B, const float T = static_cast<float>(i) / static_cast<float>(N);
.a = 255}; const float X = START_X + (DX * P * T);
for (const auto& pulse : line.pulses) { const float Y = START_Y + (DY * P * T);
if (!pulse.active) { const Vec2 D = computeRippleDisplacement(X, Y, hits.data(), n_hits);
continue; const float NX = X + D.x;
} const float NY = Y + D.y;
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( Rendering::linea(
renderer_, renderer_,
static_cast<int>(P1_X), static_cast<int>(prev_x),
static_cast<int>(P1_Y), static_cast<int>(prev_y),
static_cast<int>(P2_X), static_cast<int>(NX),
static_cast<int>(P2_Y), static_cast<int>(NY),
1.0F, line.brightness);
0.0F, prev_x = NX;
SEG_COLOR); prev_y = NY;
}
} }
} }
+30 -20
View File
@@ -5,13 +5,16 @@
// rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció // rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció
// li toca dibuixar segons el seu slot a la timeline. // li toca dibuixar segons el seu slot a la timeline.
// //
// Disseny preparat per a futures capacitats: // Reaccions disponibles:
// - Línies "vives" que reaccionen a explosions / pas de la nau (reaction_intensity). // - Ripples: deformacions circulars (ones d'aigua) que travessen la graella.
// - Capes addicionals al fons (estrelles, gradients, scanlines). // 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 #pragma once
#include <array> #include <array>
#include <cstdint>
#include <vector> #include <vector>
#include "core/defaults/playfield.hpp" #include "core/defaults/playfield.hpp"
@@ -24,44 +27,51 @@ namespace Graphics {
public: public:
explicit Playfield(Rendering::Renderer* renderer); explicit Playfield(Rendering::Renderer* renderer);
// Avança timers interns (creació + reaccions). // Avança timers interns (creació + ripples).
void update(float delta_time); 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; void draw() const;
// Notifica que una nau ha passat per (pos) a velocitat (speed_px_s). // Notifica que una nau ha passat per (pos) a (speed_px_s). Genera ones
// Si està prop d'alguna línia i va prou ràpida, la línia entra en orbit. // petites darrere la nau a cadència regular amb jitter (estil TrailManager).
void notifyShipPass(Vec2 pos, float speed_px_s); 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 // Notifica una explosió a (pos): genera una ripple gran centrada al punt.
// generen un pulse brillant que es propaga. void notifyExplosion(Vec2 pos);
void notifyFireworkSpawn(Vec2 pos);
private: // Pública per accés des d'helpers a l'anonymous namespace del .cpp.
struct Pulse { struct Ripple {
bool active{false}; Vec2 center{};
float center_t{0.5F}; // posició al llarg de la línia (0..1)
float age_s{0.0F}; 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 { struct Line {
Vec2 start; // top (verticals) o left (horitzontals) Vec2 start; // top (verticals) o left (horitzontals)
Vec2 end; // bottom (verticals) o right (horitzontals) Vec2 end; // bottom (verticals) o right (horitzontals)
float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS) float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS)
float spawn_time_s; // moment de naixement float spawn_time_s; // moment de naixement
bool is_vertical; // direcció (per saber el perpendicular de l'orbit) bool is_vertical; // direcció
float orbit_amplitude; // amplitud actual de l'orbit (px, ≥ 0)
float orbit_phase; // fase del sin (avança contínuament)
std::array<Pulse, Defaults::Playfield::MAX_PULSES_PER_LINE> pulses;
}; };
void buildLines(); void buildLines();
void drawLine(const Line& line, const Ripple* const* active, int n_active) const;
[[nodiscard]] auto computeLineProgress(const Line& line) const -> float; [[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_; Rendering::Renderer* renderer_;
std::vector<Line> lines_; std::vector<Line> lines_;
std::array<Ripple, Defaults::Playfield::Ripple::POOL_SIZE> ripples_{};
std::array<float, 2> ship_ripple_cooldown_{};
float elapsed_s_{0.0F}; float elapsed_s_{0.0F};
}; };
+9 -6
View File
@@ -75,9 +75,9 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
border_.bumpAt(hit.contact_point, STRENGTH); 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) { 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). // Explosions properes a una paret també generen bump (falloff lineal amb la distància).
@@ -216,10 +216,13 @@ void GameScene::stepPhysics(float delta_time) {
playfield_.update(delta_time); playfield_.update(delta_time);
border_.update(delta_time); border_.update(delta_time);
// Notificar al playfield que la nau ha passat (per excitar línies properes). // Notificar al playfield que la nau es mou (genera ripples petites a cadència).
for (const auto& ship : ships_) { for (std::size_t id = 0; id < ships_.size(); id++) {
if (ship.isActive()) { if (ships_[id].isActive()) {
playfield_.notifyShipPass(ship.getCenter(), ship.getSpeed()); playfield_.notifyShipMoving(static_cast<std::uint8_t>(id),
ships_[id].getCenter(),
ships_[id].getSpeed(),
delta_time);
} }
} }
} }