feat(playfield): reaccions orbit al pas de la nau i pulse al spawn de fireworks

This commit is contained in:
2026-05-21 23:03:48 +02:00
parent ffeff3d69d
commit 20f5b83649
6 changed files with 238 additions and 13 deletions
+169 -8
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <limits>
#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<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};
}
} // 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<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;
}
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<int>(line.start.x),
static_cast<int>(line.start.y),
static_cast<int>(START_X),
static_cast<int>(START_Y),
static_cast<int>(CURRENT_X),
static_cast<int>(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<int>(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<int>(P1_X),
static_cast<int>(P1_Y),
static_cast<int>(P2_X),
static_cast<int>(P2_Y),
1.0F,
0.0F,
SEG_COLOR);
}
}
}