339 lines
14 KiB
C++
339 lines
14 KiB
C++
// playfield.cpp - Implementació del fons del playfield
|
|
// © 2026 JailDesigner
|
|
|
|
#include "core/graphics/playfield.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
|
|
#include "core/defaults.hpp"
|
|
#include "core/rendering/line_renderer.hpp"
|
|
|
|
namespace Graphics {
|
|
|
|
namespace {
|
|
|
|
// Easing cubic-out: t → 1 - (1-t)^3. Decelera prop del final.
|
|
auto easeOutCubic(float t) -> float {
|
|
const float INV = 1.0F - t;
|
|
return 1.0F - (INV * INV * INV);
|
|
}
|
|
|
|
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
|
|
|
|
Playfield::Playfield(Rendering::Renderer* renderer)
|
|
: renderer_(renderer) {
|
|
buildLines();
|
|
}
|
|
|
|
void Playfield::update(float delta_time) {
|
|
elapsed_s_ += delta_time;
|
|
for (auto& ripple : ripples_) {
|
|
if (!ripple.active) {
|
|
continue;
|
|
}
|
|
ripple.age_s += delta_time;
|
|
if (ripple.age_s >= ripple.lifetime_s) {
|
|
ripple.active = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
return oldest; // pool ple: substituïm la més vella
|
|
}
|
|
|
|
void Playfield::spawnBig(Vec2 pos) {
|
|
Ripple* r = findFreeRipple();
|
|
if (r == nullptr) {
|
|
return;
|
|
}
|
|
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() {
|
|
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
|
const float CELL_W = zona.w / static_cast<float>(Defaults::Playfield::COLUMNS);
|
|
const float CELL_H = zona.h / static_cast<float>(Defaults::Playfield::ROWS);
|
|
const float SUB_W = CELL_W / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
|
|
const float SUB_H = CELL_H / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
|
|
const int SUB_VERTS = Defaults::Playfield::COLUMNS * Defaults::Playfield::SUBDIVISIONS;
|
|
const int SUB_HORIZ = Defaults::Playfield::ROWS * Defaults::Playfield::SUBDIVISIONS;
|
|
|
|
std::vector<Line> verticals;
|
|
std::vector<Line> horizontals;
|
|
|
|
// Verticals: posicions i ∈ [1, SUB_VERTS-1].
|
|
for (int i = 1; i < SUB_VERTS; i++) {
|
|
const float X = zona.x + (static_cast<float>(i) * SUB_W);
|
|
const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0;
|
|
const float BRIGHTNESS = IS_MAIN
|
|
? Defaults::Playfield::GRID_BRIGHTNESS
|
|
: Defaults::Playfield::SUBGRID_BRIGHTNESS;
|
|
verticals.push_back(Line{
|
|
.start = {.x = X, .y = zona.y},
|
|
.end = {.x = X, .y = zona.y + zona.h},
|
|
.brightness = BRIGHTNESS,
|
|
.spawn_time_s = 0.0F,
|
|
.is_vertical = true});
|
|
}
|
|
|
|
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
|
|
for (int j = 1; j < SUB_HORIZ; j++) {
|
|
const float Y = zona.y + (static_cast<float>(j) * SUB_H);
|
|
const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0;
|
|
const float BRIGHTNESS = IS_MAIN
|
|
? Defaults::Playfield::GRID_BRIGHTNESS
|
|
: Defaults::Playfield::SUBGRID_BRIGHTNESS;
|
|
horizontals.push_back(Line{
|
|
.start = {.x = zona.x, .y = Y},
|
|
.end = {.x = zona.x + zona.w, .y = Y},
|
|
.brightness = BRIGHTNESS,
|
|
.spawn_time_s = 0.0F,
|
|
.is_vertical = false});
|
|
}
|
|
|
|
// Ona diagonal: la línia esquerra/superior naix a t=0 i les següents
|
|
// propaguen cap a la dreta/inferior, en paral·lel. Verticals i
|
|
// horitzontals comparteixen la finestra temporal així el front arriba
|
|
// a la cantonada inferior-dreta alhora.
|
|
const float SPAWN_WINDOW =
|
|
Defaults::Playfield::TOTAL_ANIMATION_DURATION_S - Defaults::Playfield::LINE_GROWTH_DURATION_S;
|
|
const int NUM_V = static_cast<int>(verticals.size());
|
|
const int NUM_H = static_cast<int>(horizontals.size());
|
|
const float INTERVAL_V = (NUM_V > 1) ? SPAWN_WINDOW / static_cast<float>(NUM_V - 1) : 0.0F;
|
|
const float INTERVAL_H = (NUM_H > 1) ? SPAWN_WINDOW / static_cast<float>(NUM_H - 1) : 0.0F;
|
|
|
|
lines_.clear();
|
|
lines_.reserve(verticals.size() + horizontals.size());
|
|
|
|
// El spawn_time_s s'assigna per índex espacial perquè la diagonal de
|
|
// l'ona de creixement avanci uniformement. L'ordre dins lines_, en
|
|
// canvi, ha de garantir que el grid principal (més brillant) es
|
|
// dibuixi DESPRÉS del subgrid: així a les interseccions guanya el
|
|
// principal i no queden tallades pel subgrid.
|
|
for (int i = 0; i < NUM_V; i++) {
|
|
verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
|
|
}
|
|
for (int i = 0; i < NUM_H; i++) {
|
|
horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H;
|
|
}
|
|
|
|
// Passada 1: subgrid (verticals + horitzontals).
|
|
for (const auto& v : verticals) {
|
|
if (v.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
|
|
lines_.push_back(v);
|
|
}
|
|
}
|
|
for (const auto& h : horizontals) {
|
|
if (h.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
|
|
lines_.push_back(h);
|
|
}
|
|
}
|
|
// Passada 2: grid principal (verticals + horitzontals).
|
|
for (const auto& v : verticals) {
|
|
if (v.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
|
|
lines_.push_back(v);
|
|
}
|
|
}
|
|
for (const auto& h : horizontals) {
|
|
if (h.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
|
|
lines_.push_back(h);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto Playfield::computeLineProgress(const Line& line) const -> float {
|
|
const float LINE_ELAPSED = elapsed_s_ - line.spawn_time_s;
|
|
return std::clamp(LINE_ELAPSED / Defaults::Playfield::LINE_GROWTH_DURATION_S, 0.0F, 1.0F);
|
|
}
|
|
|
|
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_) {
|
|
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;
|
|
}
|
|
hits[n_hits++] = &r;
|
|
}
|
|
|
|
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>(END_X),
|
|
static_cast<int>(END_Y),
|
|
line.brightness);
|
|
// 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));
|
|
Rendering::linea(
|
|
renderer_,
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
} // namespace Graphics
|