feat(playfield): ones d'aigua a la rejilla per explosions i pas de nau
This commit is contained in:
+173
-166
@@ -5,8 +5,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
|
||||
#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<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
||||
return static_cast<unsigned char>(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<float>(std::rand()) / static_cast<float>(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<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) {
|
||||
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<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_) {
|
||||
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<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;
|
||||
}
|
||||
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<int>(START_X),
|
||||
static_cast<int>(START_Y),
|
||||
static_cast<int>(CURRENT_X),
|
||||
static_cast<int>(CURRENT_Y),
|
||||
static_cast<int>(END_X),
|
||||
static_cast<int>(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<int>(HEAD_X),
|
||||
static_cast<int>(HEAD_Y),
|
||||
static_cast<int>(CURRENT_X),
|
||||
static_cast<int>(CURRENT_Y),
|
||||
static_cast<int>(START_X + (DX * HEAD_T)),
|
||||
static_cast<int>(START_Y + (DY * HEAD_T)),
|
||||
static_cast<int>(END_X),
|
||||
static_cast<int>(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<int>(P1_X),
|
||||
static_cast<int>(P1_Y),
|
||||
static_cast<int>(P2_X),
|
||||
static_cast<int>(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<float>(i) / static_cast<float>(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<int>(prev_x),
|
||||
static_cast<int>(prev_y),
|
||||
static_cast<int>(NX),
|
||||
static_cast<int>(NY),
|
||||
line.brightness);
|
||||
prev_x = NX;
|
||||
prev_y = NY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user