// init_hud_animator.cpp - Implementación de la animación inicial del HUD #include "game/systems/init_hud_animator.hpp" #include #include #include #include #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(zone.x); const int Y1 = static_cast(zone.y); const int X2 = static_cast(zone.x + zone.w); const int Y2 = static_cast(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(CX - ((CX - X1) * P)); const int X_RIGHT = static_cast(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(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(X1 + ((CX - X1) * P)); const int X_RIGHT = static_cast(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& shape) -> float { float min_y = std::numeric_limits::max(); float max_y = std::numeric_limits::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(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& 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(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& 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(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