Files
orni-attack/source/game/systems/init_hud_animator.cpp
T

251 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 {
// 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);
}
// Mida d'un slot = alçada de la majúscula del dígit (mètrica del glif).
auto slotSize(float scale) -> float {
return Graphics::VectorText::getTextHeight(scale);
}
// 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;
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};
Rendering::renderShape(renderer, shape, POS, 0.0F, ICON_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). 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,
float x,
float top_y,
float scale,
float spacing) {
if (score.empty()) {
return;
}
// Primer dígit significatiu; si són tots zeros, força l'últim a encès.
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, 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 GAP = digitPitch(scale, spacing); // separació punts↔slots = un pas de dígit
const float W_LIVES = slotsBlockWidth(scale, spacing);
const float BLOCK_W = W_SCORE + 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 + GAP;
drawSlots(renderer, shape, lives, bright, dim, x, center_y, scale, spacing);
}
// 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