236 lines
10 KiB
C++
236 lines
10 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 <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 {
|
|
|
|
// Alçada de la icona de vida (px lògics) derivada de l'alçada del marcador.
|
|
auto lifeIconHeight() -> float {
|
|
return Defaults::Hud::Lives::ICON_HEIGHT_RATIO * Defaults::Zones::SCOREBOARD_BOTTOM_H;
|
|
}
|
|
|
|
// Nombre d'icones de vida a dibuixar (acotat a MAX_ICONS, mai negatiu).
|
|
auto lifeIconCount(int lives) -> int {
|
|
return std::clamp(lives, 0, Defaults::Hud::Lives::MAX_ICONS);
|
|
}
|
|
|
|
// Ample del bloc de vides (0 si no hi ha vides). N icones = (N-1) passos
|
|
// de separació + l'amplada d'una icona.
|
|
auto livesBlockWidth(int lives) -> float {
|
|
const int N = lifeIconCount(lives);
|
|
if (N <= 0) {
|
|
return 0.0F;
|
|
}
|
|
const float ICON_H = lifeIconHeight();
|
|
const float STEP = ICON_H * Defaults::Hud::Lives::ICON_SPACING_FACTOR;
|
|
return (static_cast<float>(N - 1) * STEP) + ICON_H;
|
|
}
|
|
|
|
// Dibuixa les vides com a icones de la nau (apuntant amunt, color del
|
|
// jugador). El glow el posa el shader. x_left = vora esquerra del bloc.
|
|
void drawLives(Rendering::Renderer* renderer,
|
|
const std::shared_ptr<Graphics::Shape>& shape,
|
|
int lives,
|
|
SDL_Color color,
|
|
float x_left,
|
|
float center_y) {
|
|
const int N = lifeIconCount(lives);
|
|
if (N <= 0 || !shape) {
|
|
return;
|
|
}
|
|
const float ICON_H = lifeIconHeight();
|
|
const float STEP = ICON_H * Defaults::Hud::Lives::ICON_SPACING_FACTOR;
|
|
// Escala que ajusta el cercle circumscrit de la shape a l'alçada
|
|
// objectiu (mida predictible independent del .shp).
|
|
const float RADIUS = shape->getBoundingRadius();
|
|
const float SCALE = (RADIUS > 0.001F) ? (ICON_H / (2.0F * RADIUS)) : 1.0F;
|
|
for (int i = 0; i < N; i++) {
|
|
const Vec2 POS = {.x = x_left + (ICON_H / 2.0F) + (static_cast<float>(i) * STEP), .y = center_y};
|
|
Rendering::renderShape(renderer, shape, POS, 0.0F, SCALE, 1.0F, 1.0F, color);
|
|
}
|
|
}
|
|
|
|
// 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). Puntuació 0 (tot zeros) → tot atenuat.
|
|
void drawScore(const Graphics::VectorText& text,
|
|
const std::string& score,
|
|
SDL_Color bright,
|
|
SDL_Color dim,
|
|
float x,
|
|
float top_y,
|
|
float scale,
|
|
float spacing) {
|
|
const size_t SIG = score.find_first_not_of('0');
|
|
const std::string PREFIX = (SIG == std::string::npos) ? score : score.substr(0, SIG);
|
|
const std::string REST = (SIG == std::string::npos) ? std::string{} : 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, bright);
|
|
}
|
|
}
|
|
|
|
// Pinta el bloc d'un jugador "punts vides" amb el seu color (punts amb
|
|
// zeros atenuats, vides com a icones de nau en brillant). Si right_align,
|
|
// el bloc acaba a anchor_x (ancorat a la dreta); si no, comença a
|
|
// anchor_x (esquerra).
|
|
void drawPlayerBlock(Rendering::Renderer* renderer,
|
|
const Graphics::VectorText& text,
|
|
const std::shared_ptr<Graphics::Shape>& shape,
|
|
const std::string& score,
|
|
int lives,
|
|
SDL_Color bright,
|
|
SDL_Color dim,
|
|
float anchor_x,
|
|
float center_y,
|
|
float scale,
|
|
float spacing,
|
|
bool right_align) {
|
|
const float TOP_Y = center_y - (Graphics::VectorText::getTextHeight(scale) / 2.0F);
|
|
const float W_SCORE = Graphics::VectorText::getTextWidth(score, scale, spacing);
|
|
const float W_LIVES = livesBlockWidth(lives);
|
|
const float BLOCK_W = W_SCORE + Defaults::Hud::Layout::BLOCK_INNER_GAP + W_LIVES;
|
|
|
|
float x = right_align ? (anchor_x - BLOCK_W) : anchor_x;
|
|
drawScore(text, score, bright, dim, x, TOP_Y, scale, spacing);
|
|
x += W_SCORE + Defaults::Hud::Layout::BLOCK_INNER_GAP;
|
|
drawLives(renderer, shape, lives, bright, x, center_y);
|
|
}
|
|
|
|
// Pinta el nivell centrat: etiqueta "NIVELL" en verd atenuat i el número
|
|
// en verd brillant.
|
|
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_DIM);
|
|
x += W_LABEL;
|
|
text.render(value, {.x = x, .y = top_y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LEVEL_BRIGHT);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void drawScoreboardAt(Rendering::Renderer* renderer,
|
|
const Graphics::VectorText& text,
|
|
const ScoreboardData& data,
|
|
float center_y,
|
|
float scale,
|
|
float spacing) {
|
|
// Els blocs s'ancoren a les verticals del PLAYAREA (sota el marc).
|
|
const SDL_FRect& play = Defaults::Zones::PLAYAREA;
|
|
const float LEFT = play.x;
|
|
const float RIGHT = play.x + play.w;
|
|
const float TOP_Y = center_y - (Graphics::VectorText::getTextHeight(scale) / 2.0F);
|
|
|
|
drawPlayerBlock(renderer, text, data.shape_p1, data.score_p1, data.lives_p1, Defaults::Hud::Colors::P1_BRIGHT, Defaults::Hud::Colors::P1_DIM, LEFT, center_y, scale, spacing, false);
|
|
drawPlayerBlock(renderer, text, data.shape_p2, data.score_p2, data.lives_p2, Defaults::Hud::Colors::P2_BRIGHT, Defaults::Hud::Colors::P2_DIM, RIGHT, center_y, scale, spacing, true);
|
|
drawLevel(text, data.level_label, data.level_value, TOP_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
|