296 lines
14 KiB
C++
296 lines
14 KiB
C++
// init_hud_animator.cpp - Implementación de la animación inicial del HUD
|
||
|
||
#include "game/systems/init_hud_animator.hpp"
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <algorithm>
|
||
#include <limits>
|
||
#include <string>
|
||
|
||
#include "core/defaults.hpp"
|
||
#include "core/defaults/hud.hpp"
|
||
#include "core/math/easing.hpp"
|
||
#include "core/rendering/line_renderer.hpp"
|
||
#include "core/rendering/shape_renderer.hpp"
|
||
|
||
namespace Systems::InitHud {
|
||
|
||
auto computeRangeProgress(float global_progress,
|
||
float ratio_init,
|
||
float ratio_end) -> float {
|
||
if (ratio_init >= ratio_end) {
|
||
return (global_progress >= ratio_end) ? 1.0F : 0.0F;
|
||
}
|
||
if (global_progress < ratio_init) {
|
||
return 0.0F;
|
||
}
|
||
if (global_progress > ratio_end) {
|
||
return 1.0F;
|
||
}
|
||
return (global_progress - ratio_init) / (ratio_end - ratio_init);
|
||
}
|
||
|
||
auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2 {
|
||
const float EASED = Easing::easeOutQuad(progress);
|
||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||
// Y inicial: bajo la zone de juego (sale desde fuera).
|
||
const float Y_INI = zone.y + zone.h + Defaults::Hud::InitAnim::SHIP_SPAWN_Y_OFFSET;
|
||
const float Y_ANIM = Y_INI + ((final_position.y - Y_INI) * EASED);
|
||
return Vec2{.x = final_position.x, .y = Y_ANIM};
|
||
}
|
||
|
||
void drawBordersAnimated(Rendering::Renderer* renderer, float progress) {
|
||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||
const float EASED = Easing::easeOutQuad(progress);
|
||
|
||
const int X1 = static_cast<int>(zone.x);
|
||
const int Y1 = static_cast<int>(zone.y);
|
||
const int X2 = static_cast<int>(zone.x + zone.w);
|
||
const int Y2 = static_cast<int>(zone.y + zone.h);
|
||
const int CX = (X1 + X2) / 2;
|
||
|
||
constexpr float PHASE_1_END = Defaults::Hud::InitAnim::BORDER_PHASE_1_END;
|
||
constexpr float PHASE_2_END = Defaults::Hud::InitAnim::BORDER_PHASE_2_END;
|
||
|
||
// Fase 1: línea superior crece desde el centro hacia los lados.
|
||
if (EASED > 0.0F) {
|
||
const float P = std::min(EASED / PHASE_1_END, 1.0F);
|
||
const int X_LEFT = static_cast<int>(CX - ((CX - X1) * P));
|
||
const int X_RIGHT = static_cast<int>(CX + ((X2 - CX) * P));
|
||
Rendering::linea(renderer, CX, Y1, X_LEFT, Y1);
|
||
Rendering::linea(renderer, CX, Y1, X_RIGHT, Y1);
|
||
}
|
||
|
||
// Fase 2: laterales bajan.
|
||
if (EASED > PHASE_1_END) {
|
||
const float P = std::min((EASED - PHASE_1_END) / (PHASE_2_END - PHASE_1_END), 1.0F);
|
||
const int Y_BOTTOM = static_cast<int>(Y1 + ((Y2 - Y1) * P));
|
||
Rendering::linea(renderer, X1, Y1, X1, Y_BOTTOM);
|
||
Rendering::linea(renderer, X2, Y1, X2, Y_BOTTOM);
|
||
}
|
||
|
||
// Fase 3: línea inferior crece desde los lados hacia el centro.
|
||
if (EASED > PHASE_2_END) {
|
||
const float P = (EASED - PHASE_2_END) / (1.0F - PHASE_2_END);
|
||
const int X_LEFT = static_cast<int>(X1 + ((CX - X1) * P));
|
||
const int X_RIGHT = static_cast<int>(X2 - ((X2 - CX) * P));
|
||
Rendering::linea(renderer, X1, Y2, X_LEFT, Y2);
|
||
Rendering::linea(renderer, X2, Y2, X_RIGHT, Y2);
|
||
}
|
||
}
|
||
|
||
namespace {
|
||
|
||
// Nombre de slots de vides: una nau menys que el màxim (la nau en joc
|
||
// no es dibuixa; els slots són repuestos). Deriva de MAX_VIDES.
|
||
constexpr int NUM_SLOTS = Defaults::Game::MAX_VIDES - 1;
|
||
|
||
// Pas d'un dígit (amplada + tracking, escalat): és la diferència entre
|
||
// l'ample de dos caràcters i el d'un. Marca el ritme de tot el bloc.
|
||
auto digitPitch(float scale, float spacing) -> float {
|
||
return Graphics::VectorText::getTextWidth("00", scale, spacing) -
|
||
Graphics::VectorText::getTextWidth("0", scale, spacing);
|
||
}
|
||
|
||
// Desplaçament vertical (unitats locals) entre el center declarat de la
|
||
// shape i el centre real del seu bbox. La nau té center (0,0) però el seu
|
||
// bbox no hi està centrat; cal per alinear el centre VISUAL de la nau amb
|
||
// la línia del marcador (els dígits sí tenen el center al mig del glif).
|
||
auto shapeVerticalOffset(const std::shared_ptr<Graphics::Shape>& shape) -> float {
|
||
float min_y = std::numeric_limits<float>::max();
|
||
float max_y = std::numeric_limits<float>::lowest();
|
||
for (const auto& prim : shape->getPrimitives()) {
|
||
for (const auto& point : prim.points) {
|
||
min_y = std::min(min_y, point.y);
|
||
max_y = std::max(max_y, point.y);
|
||
}
|
||
}
|
||
return ((min_y + max_y) / 2.0F) - shape->getCenter().y;
|
||
}
|
||
|
||
// Mida d'un slot = alçada real del glif del dígit (no la cel·la, que té
|
||
// marge vertical: usar la cel·la feia les naus el doble de grans), amb un
|
||
// xicotet factor d'ajust perquè la silueta de la nau case amb les xifres.
|
||
auto slotSize(float scale) -> float {
|
||
return Graphics::VectorText::getGlyphHeight(scale) * Defaults::Hud::LIFE_SLOT_HEIGHT_FACTOR;
|
||
}
|
||
|
||
// Ample del bloc de slots: constant, independent de les vides. NUM_SLOTS
|
||
// slots al pas del dígit (l'últim slot ocupa la seva pròpia mida).
|
||
auto slotsBlockWidth(float scale, float spacing) -> float {
|
||
if (NUM_SLOTS <= 0) {
|
||
return 0.0F;
|
||
}
|
||
return (static_cast<float>(NUM_SLOTS - 1) * digitPitch(scale, spacing)) + slotSize(scale);
|
||
}
|
||
|
||
// Dibuixa els slots de vides com a naus en miniatura en posicions FIXES.
|
||
// Slot amb vida disponible (repuesto) → color encès; slot buit → atenuat.
|
||
// Repuestos = vides − 1 (la nau en joc no compta com a slot).
|
||
void drawSlots(Rendering::Renderer* renderer,
|
||
const std::shared_ptr<Graphics::Shape>& shape,
|
||
int lives,
|
||
SDL_Color bright,
|
||
SDL_Color dim,
|
||
float x_left,
|
||
float center_y,
|
||
float scale,
|
||
float spacing) {
|
||
if (NUM_SLOTS <= 0 || !shape) {
|
||
return;
|
||
}
|
||
const float SIZE = slotSize(scale);
|
||
const float PITCH = digitPitch(scale, spacing);
|
||
// Escala que ajusta el cercle circumscrit de la shape a la mida del
|
||
// slot (mida predictible independent del .shp).
|
||
const float RADIUS = shape->getBoundingRadius();
|
||
const float ICON_SCALE = (RADIUS > 0.001F) ? (SIZE / (2.0F * RADIUS)) : 1.0F;
|
||
// Alinea el centre visual de la nau amb la línia del marcador.
|
||
const float OFFSET_Y = shapeVerticalOffset(shape) * ICON_SCALE;
|
||
const int FILLED = std::clamp(lives - 1, 0, NUM_SLOTS);
|
||
for (int i = 0; i < NUM_SLOTS; i++) {
|
||
const SDL_Color COLOR = (i < FILLED) ? bright : dim;
|
||
const Vec2 POS = {.x = x_left + (SIZE / 2.0F) + (static_cast<float>(i) * PITCH), .y = center_y - OFFSET_Y};
|
||
// glow=false: el marcador es manté net, com els dígits del text.
|
||
Rendering::renderShape(renderer, shape, POS, 0.0F, ICON_SCALE, 1.0F, 1.0F, COLOR, 0.0F, 1.0F, false);
|
||
}
|
||
}
|
||
|
||
// Pinta la puntuació amb els zeros de farciment previs al primer dígit
|
||
// significatiu en to atenuat i la resta en brillant (efecte display de 7
|
||
// segments). El dígit menys significatiu SEMPRE va encès: puntuació 0 →
|
||
// cinc zeros atenuats + l'últim "0" encès (el marcador no queda mai apagat).
|
||
void drawScore(const Graphics::VectorText& text,
|
||
const std::string& score,
|
||
SDL_Color bright,
|
||
SDL_Color dim,
|
||
bool active,
|
||
float x,
|
||
float top_y,
|
||
float scale,
|
||
float spacing) {
|
||
if (score.empty()) {
|
||
return;
|
||
}
|
||
// Jugador inactiu → marcador apagat: tots els dígits atenuats (no té
|
||
// "zero punts", simplement no en té). Jugador actiu → l'últim dígit
|
||
// sempre encès (puntuació 0 → cinc zeros atenuats + "0" encès).
|
||
const SDL_Color REST_COLOR = active ? bright : dim;
|
||
const size_t FIRST_SIG = score.find_first_not_of('0');
|
||
const size_t SIG = (FIRST_SIG == std::string::npos) ? (score.size() - 1) : FIRST_SIG;
|
||
const std::string PREFIX = score.substr(0, SIG);
|
||
const std::string REST = score.substr(SIG);
|
||
if (!PREFIX.empty()) {
|
||
text.render(PREFIX, {.x = x, .y = top_y}, scale, spacing, 1.0F, dim);
|
||
// Avança l'amplada del prefix més el buit inter-caràcter que hi
|
||
// hauria si fos un sol string (exacte per a qualsevol spacing).
|
||
x += Graphics::VectorText::getTextWidth(PREFIX, scale, spacing) + (spacing * scale);
|
||
}
|
||
if (!REST.empty()) {
|
||
text.render(REST, {.x = x, .y = top_y}, scale, spacing, 1.0F, REST_COLOR);
|
||
}
|
||
}
|
||
|
||
// Separació punts↔slots dins d'un bloc = un pas de dígit (ritme únic).
|
||
auto blockInnerGap(float scale, float spacing) -> float {
|
||
return digitPitch(scale, spacing);
|
||
}
|
||
|
||
// Ample (constant) del bloc d'un jugador: 6 dígits + separació + slots.
|
||
// No depèn de les vides, així res es recol·loca quan se'n perden.
|
||
auto playerBlockWidth(float scale, float spacing) -> float {
|
||
return Graphics::VectorText::getTextWidth("000000", scale, spacing) +
|
||
blockInnerGap(scale, spacing) + slotsBlockWidth(scale, spacing);
|
||
}
|
||
|
||
// Pinta el bloc d'un jugador "punts vides" amb el seu color (punts amb
|
||
// zeros atenuats, vides com a slots de nau). Ancorat a x_left (vora
|
||
// esquerra del bloc), mateix ordre per a P1 i P2 (no mirrored).
|
||
void drawPlayerBlock(Rendering::Renderer* renderer,
|
||
const Graphics::VectorText& text,
|
||
const std::shared_ptr<Graphics::Shape>& shape,
|
||
const std::string& score,
|
||
int lives,
|
||
bool active,
|
||
SDL_Color bright,
|
||
SDL_Color dim,
|
||
float x_left,
|
||
float center_y,
|
||
float scale,
|
||
float spacing) {
|
||
// Jugador inactiu → bloc apagat: es dibuixa igual però tot atenuat
|
||
// (punts i slots), com un display físic sense encendre.
|
||
const float TOP_Y = center_y - (Graphics::VectorText::getTextHeight(scale) / 2.0F);
|
||
const float W_SCORE = Graphics::VectorText::getTextWidth(score, scale, spacing);
|
||
|
||
float x = x_left;
|
||
drawScore(text, score, bright, dim, active, x, TOP_Y, scale, spacing);
|
||
x += W_SCORE + blockInnerGap(scale, spacing);
|
||
drawSlots(renderer, shape, lives, bright, dim, x, center_y, scale, spacing);
|
||
}
|
||
|
||
// Pinta el nivell centrat: etiqueta "NIVELL" encesa i el número com els
|
||
// punts (zeros de farciment atenuats, dígit significatiu en endavant encès).
|
||
void drawLevel(const Graphics::VectorText& text,
|
||
const std::string& label,
|
||
const std::string& value,
|
||
float top_y,
|
||
float scale,
|
||
float spacing) {
|
||
const float W_LABEL = Graphics::VectorText::getTextWidth(label, scale, spacing);
|
||
const float W_VALUE = Graphics::VectorText::getTextWidth(value, scale, spacing);
|
||
const float CX = Defaults::Game::WIDTH / 2.0F;
|
||
float x = CX - ((W_LABEL + W_VALUE) / 2.0F);
|
||
text.render(label, {.x = x, .y = top_y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LEVEL_BRIGHT);
|
||
x += W_LABEL;
|
||
drawScore(text, value, Defaults::Hud::Colors::LEVEL_BRIGHT, Defaults::Hud::Colors::LEVEL_DIM, true, x, top_y, scale, spacing);
|
||
}
|
||
|
||
} // namespace
|
||
|
||
void drawScoreboardAt(Rendering::Renderer* renderer,
|
||
const Graphics::VectorText& text,
|
||
const ScoreboardData& data,
|
||
float center_y,
|
||
float scale,
|
||
float spacing) {
|
||
// Fila centrada amb posicions FIXES: [bloc P1] · [NIVELL] · [bloc P2].
|
||
// Els blocs tenen ample constant (slots fixos), així NIVELL queda centrat
|
||
// i res es recol·loca en perdre vides. Separadors derivats del glif
|
||
// (dos espais), com el disseny original.
|
||
const float TOP_Y = center_y - (Graphics::VectorText::getTextHeight(scale) / 2.0F);
|
||
const float BLOCK_W = playerBlockWidth(scale, spacing);
|
||
const float W_LEVEL = Graphics::VectorText::getTextWidth(data.level_label, scale, spacing) +
|
||
Graphics::VectorText::getTextWidth(data.level_value, scale, spacing);
|
||
const float GAP = Graphics::VectorText::getTextWidth(" ", scale, spacing);
|
||
const float TOTAL = BLOCK_W + GAP + W_LEVEL + GAP + BLOCK_W;
|
||
|
||
float x = (Defaults::Game::WIDTH / 2.0F) - (TOTAL / 2.0F);
|
||
drawPlayerBlock(renderer, text, data.shape_p1, data.score_p1, data.lives_p1, data.p1_active, Defaults::Hud::Colors::P1_BRIGHT, Defaults::Hud::Colors::P1_DIM, x, center_y, scale, spacing);
|
||
x += BLOCK_W + GAP;
|
||
// NIVELL: drawLevel centra a WIDTH/2, que coincideix amb aquest tram.
|
||
drawLevel(text, data.level_label, data.level_value, TOP_Y, scale, spacing);
|
||
x += W_LEVEL + GAP;
|
||
drawPlayerBlock(renderer, text, data.shape_p2, data.score_p2, data.lives_p2, data.p2_active, Defaults::Hud::Colors::P2_BRIGHT, Defaults::Hud::Colors::P2_DIM, x, center_y, scale, spacing);
|
||
}
|
||
|
||
void drawScoreboardAnimated(Rendering::Renderer* renderer,
|
||
const Graphics::VectorText& text,
|
||
const ScoreboardData& data,
|
||
float progress) {
|
||
const float EASED = Easing::easeOutQuad(progress);
|
||
|
||
constexpr float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
||
constexpr float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
||
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
||
const float Y_FINAL = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
||
// Posición inicial: fuera de la pantalla por debajo.
|
||
const auto Y_INI = static_cast<float>(Defaults::Game::HEIGHT);
|
||
const float Y_ANIM = Y_INI + ((Y_FINAL - Y_INI) * EASED);
|
||
|
||
drawScoreboardAt(renderer, text, data, Y_ANIM, SCALE, SPACING);
|
||
}
|
||
|
||
} // namespace Systems::InitHud
|