// init_hud_animator.cpp - Implementación de la animación inicial del HUD #include "game/systems/init_hud_animator.hpp" #include #include #include #include "core/defaults.hpp" #include "core/defaults/hud.hpp" #include "core/math/easing.hpp" #include "core/rendering/line_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 { // Vides com a text (2 dígits) — provisional fins a migrar-les a icones. auto livesText(int lives) -> std::string { const std::string S = std::to_string(lives); return (lives < 10) ? "0" + S : S; } // Pinta el bloc d'un jugador "punts vides" amb el seu color. Si // right_align, el bloc acaba a anchor_x (ancorat a la dreta); si no, // comença a anchor_x (ancorat a l'esquerra). void drawPlayerBlock(const Graphics::VectorText& text, const std::string& score, int lives, SDL_Color color, float anchor_x, float top_y, float scale, float spacing, bool right_align) { const std::string LIVES_STR = livesText(lives); const float W_SCORE = Graphics::VectorText::getTextWidth(score, scale, spacing); const float W_LIVES = Graphics::VectorText::getTextWidth(LIVES_STR, scale, spacing); const float BLOCK_W = W_SCORE + Defaults::Hud::Layout::BLOCK_INNER_GAP + W_LIVES; float x = right_align ? (anchor_x - BLOCK_W) : anchor_x; text.render(score, {.x = x, .y = top_y}, scale, spacing, 1.0F, color); x += W_SCORE + Defaults::Hud::Layout::BLOCK_INNER_GAP; text.render(LIVES_STR, {.x = x, .y = top_y}, scale, spacing, 1.0F, color); } // Pinta el nivell ("NIVELL" + número) centrat a la pantalla. 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; text.render(value, {.x = x, .y = top_y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LEVEL_BRIGHT); } } // namespace void drawScoreboardAt(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(text, data.score_p1, data.lives_p1, Defaults::Hud::Colors::P1_BRIGHT, LEFT, TOP_Y, scale, spacing, false); drawPlayerBlock(text, data.score_p2, data.lives_p2, Defaults::Hud::Colors::P2_BRIGHT, RIGHT, TOP_Y, scale, spacing, true); drawLevel(text, data.level_label, data.level_value, TOP_Y, scale, spacing); } void drawScoreboardAnimated(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(text, data, Y_ANIM, SCALE, SPACING); } } // namespace Systems::InitHud