feat(playfield): reaccions orbit al pas de la nau i pulse al spawn de fireworks
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#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<Pulse, Defaults::Playfield::MAX_PULSES_PER_LINE> pulses;
|
||||
};
|
||||
|
||||
void buildLines();
|
||||
[[nodiscard]] auto computeLineProgress(const Line& line) const -> float;
|
||||
static void spawnPulseAt(Line& line, float center_t);
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
std::vector<Line> lines_;
|
||||
|
||||
@@ -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<float>(n_points);
|
||||
const float JITTER_RAD =
|
||||
Defaults::FX::Firework::ANGULAR_JITTER_DEG * Defaults::Math::PI / 180.0F;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
#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<void(Vec2 origen)>;
|
||||
|
||||
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<Firework, POOL_SIZE> pool_;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user