#include "scoreboard.hpp" #include // Para SDL_DestroyTexture, SDL_SetRenderDrawColor, SDL_SetRenderTarget, SDL_CreateTexture, SDL_GetRenderTarget, SDL_GetTicks, SDL_RenderClear, SDL_RenderLine, SDL_RenderTexture, SDL_SetTextureBlendMode, SDL_FRect, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_Texture, SDL_TextureAccess #include // Para max #include // Para roundf #include // Para operator<<, setfill, setw #include #include // Para basic_ostream, basic_ostringstream, basic_ostream::operator<<, ostringstream #include "color.hpp" #include "enter_name.hpp" // Para NAME_SIZE #include "lang.hpp" // Para getText #include "param.hpp" // Para Param, ParamScoreboard, param #include "resource.hpp" // Para Resource #include "screen.hpp" // Para Screen #include "sprite.hpp" // Para Sprite #include "text.hpp" // Para Text, Text::CENTER, Text::COLOR #include "texture.hpp" // Para Texture #include "utils.hpp" // Para easeOutCubic // .at(SINGLETON) Hay que definir las variables estáticas, desde el .h sólo la hemos declarado Scoreboard* Scoreboard::instance = nullptr; // .at(SINGLETON) Crearemos el objeto score_board con esta función estática void Scoreboard::init() { Scoreboard::instance = new Scoreboard(); } // .at(SINGLETON) Destruiremos el objeto score_board con esta función estática void Scoreboard::destroy() { delete Scoreboard::instance; } // .at(SINGLETON) Con este método obtenemos el objeto score_board y podemos trabajar con él auto Scoreboard::get() -> Scoreboard* { return Scoreboard::instance; } // Constructor Scoreboard::Scoreboard() : renderer_(Screen::get()->getRenderer()), game_power_meter_texture_(Resource::get()->getTexture("game_power_meter.png")), power_meter_sprite_(std::make_unique(game_power_meter_texture_)), text_(Resource::get()->getText("8bithud")) { // Inicializa variables for (size_t i = 0; i < static_cast(Id::SIZE); ++i) { name_.at(i).clear(); enter_name_.at(i).clear(); selector_pos_.at(i) = 0; score_.at(i) = 0; mult_.at(i) = 0; continue_counter_.at(i) = 0; carousel_prev_index_.at(i) = -1; // Inicializar a -1 para detectar primera inicialización enter_name_ref_.at(i) = nullptr; text_slide_offset_.at(i) = 0.0F; } panel_.at(static_cast(Id::LEFT)).mode = Mode::SCORE; panel_.at(static_cast(Id::RIGHT)).mode = Mode::SCORE; panel_.at(static_cast(Id::CENTER)).mode = Mode::STAGE_INFO; // Recalcula las anclas de los elementos recalculateAnchors(); power_meter_sprite_->setPosition(SDL_FRect{ static_cast(slot4_2_.x - 20), slot4_2_.y, 40, 7}); // Crea la textura de fondo background_ = nullptr; createBackgroundTexture(); // Crea las texturas de los paneles createPanelTextures(); // Rellena la textura de fondo fillBackgroundTexture(); // Inicializa el ciclo de colores para el nombre name_color_cycle_ = Colors::generateMirroredCycle(color_.INVERSE(), ColorCycleStyle::VIBRANT); animated_color_ = name_color_cycle_.at(0); } Scoreboard::~Scoreboard() { if (background_ != nullptr) { SDL_DestroyTexture(background_); } for (auto* texture : panel_texture_) { if (texture != nullptr) { SDL_DestroyTexture(texture); } } } // Configura la animación del carrusel void Scoreboard::setCarouselAnimation(Id id, int selected_index, EnterName* enter_name_ptr) { auto idx = static_cast(id); // Guardar referencia al objeto EnterName enter_name_ref_.at(idx) = enter_name_ptr; if ((enter_name_ptr == nullptr) || selected_index < 0) { return; } // ===== Primera inicialización ===== if (carousel_prev_index_.at(idx) == -1) { carousel_position_.at(idx) = static_cast(selected_index); carousel_target_.at(idx) = static_cast(selected_index); carousel_prev_index_.at(idx) = selected_index; return; } int prev_index = carousel_prev_index_.at(idx); if (selected_index == prev_index) { return; // No hay cambio } // ===== Bloqueo: si aún está animando, ignorar nueva orden ===== if (std::abs(carousel_position_.at(idx) - carousel_target_.at(idx)) > 0.01f) { return; } // ===== Calcular el salto ===== int delta = selected_index - prev_index; const int LIST_SIZE = static_cast(enter_name_ptr->getCharacterList().size()); // Ajustar para wrap-around circular (camino más corto) if (delta > LIST_SIZE / 2) { delta -= LIST_SIZE; } else if (delta < -LIST_SIZE / 2) { delta += LIST_SIZE; } // ===== Diferenciar salto corto o largo ===== if (std::abs(delta) > 2) { // Salto grande → ir directo sin animación carousel_position_.at(idx) = static_cast(selected_index); carousel_target_.at(idx) = static_cast(selected_index); } else { // Salto corto → animación suave normal carousel_position_.at(idx) = std::round(carousel_position_.at(idx)); carousel_target_.at(idx) = carousel_position_.at(idx) + static_cast(delta); } // Actualizar índice actual carousel_prev_index_.at(idx) = selected_index; } // Establece el modo del panel y gestiona transiciones void Scoreboard::setMode(Id id, Mode mode) { auto idx = static_cast(id); // Cambiar el modo panel_.at(idx).mode = mode; // Gestionar inicialización/transiciones según el nuevo modo switch (mode) { case Mode::SCORE_TO_ENTER_NAME: // Iniciar animación de transición SCORE → ENTER_NAME text_slide_offset_.at(idx) = 0.0F; // Resetear carrusel para que se inicialice correctamente en ENTER_NAME if (carousel_prev_index_.at(idx) != -1) { carousel_prev_index_.at(idx) = -1; } break; case Mode::ENTER_NAME: // Resetear carrusel al entrar en modo de entrada de nombre // Esto fuerza una reinicialización en la próxima llamada a setCarouselAnimation() if (carousel_prev_index_.at(idx) != -1) { carousel_prev_index_.at(idx) = -1; } text_slide_offset_.at(idx) = 0.0F; break; case Mode::ENTER_TO_SHOW_NAME: // Iniciar animación de transición ENTER_NAME → SHOW_NAME text_slide_offset_.at(idx) = 0.0F; break; case Mode::SHOW_NAME: // Asegurar que la animación está completa text_slide_offset_.at(idx) = 1.0F; break; // Otros modos no requieren inicialización especial default: break; } } // Transforma un valor numérico en una cadena de 7 cifras auto Scoreboard::updateScoreText(int num) -> std::string { std::ostringstream oss; oss << std::setw(7) << std::setfill('0') << num; return oss.str(); } // Actualiza el contador void Scoreboard::updateTimeCounter() { constexpr int TICKS_SPEED = 100; if (SDL_GetTicks() - ticks_ > TICKS_SPEED) { ticks_ = SDL_GetTicks(); ++time_counter_; } } // Actualiza el índice del color animado del nombre void Scoreboard::updateNameColorIndex() { constexpr Uint64 COLOR_UPDATE_INTERVAL = 100; // 100ms entre cambios de color if (SDL_GetTicks() - name_color_last_update_ >= COLOR_UPDATE_INTERVAL) { ++name_color_index_; name_color_last_update_ = SDL_GetTicks(); } // Precalcular el color actual del ciclo animated_color_ = name_color_cycle_.at(name_color_index_ % name_color_cycle_.size()); } // Actualiza la animación del carrusel void Scoreboard::updateCarouselAnimation(float delta_time) { constexpr float CAROUSEL_SPEED = 8.0F; // Posiciones por segundo for (size_t i = 0; i < carousel_position_.size(); ++i) { // Solo animar si no hemos llegado al target if (std::abs(carousel_position_.at(i) - carousel_target_.at(i)) > 0.01F) { // Determinar dirección float direction = (carousel_target_.at(i) > carousel_position_.at(i)) ? 1.0F : -1.0F; // Calcular movimiento float movement = CAROUSEL_SPEED * delta_time * direction; // Mover, pero no sobrepasar el target float new_position = carousel_position_.at(i) + movement; // Clamp para no sobrepasar if (direction > 0) { carousel_position_.at(i) = std::min(new_position, carousel_target_.at(i)); } else { carousel_position_.at(i) = std::max(new_position, carousel_target_.at(i)); } } else { // Forzar al target exacto cuando estamos muy cerca carousel_position_.at(i) = carousel_target_.at(i); } } // Después del bloque principal en updateCarouselAnimation() /*for (size_t i = 0; i < carousel_position_.size(); ++i) { if (std::abs(carousel_position_.at(i) - carousel_target_.at(i)) <= 0.01F) { carousel_position_.at(i) = std::round(carousel_target_.at(i)); // <-- redondea } }*/ } // Actualiza las animaciones de deslizamiento de texto void Scoreboard::updateTextSlideAnimation(float delta_time) { for (size_t i = 0; i < static_cast(Id::SIZE); ++i) { Mode current_mode = panel_.at(i).mode; if (current_mode == Mode::SCORE_TO_ENTER_NAME) { // Incrementar progreso de animación SCORE → ENTER_NAME (0.0 a 1.0) text_slide_offset_.at(i) += delta_time / TEXT_SLIDE_DURATION; // Terminar animación y cambiar a ENTER_NAME cuando se complete if (text_slide_offset_.at(i) >= 1.0F) { setMode(static_cast(i), Mode::ENTER_NAME); } } else if (current_mode == Mode::ENTER_TO_SHOW_NAME) { // Incrementar progreso de animación ENTER_NAME → SHOW_NAME (0.0 a 1.0) text_slide_offset_.at(i) += delta_time / TEXT_SLIDE_DURATION; // Terminar animación y cambiar a SHOW_NAME cuando se complete if (text_slide_offset_.at(i) >= 1.0F) { setMode(static_cast(i), Mode::SHOW_NAME); } } } } // Actualiza la lógica del marcador void Scoreboard::update(float delta_time) { updateTimeCounter(); updateNameColorIndex(); updateCarouselAnimation(delta_time); updateTextSlideAnimation(delta_time); fillBackgroundTexture(); // Renderizar DESPUÉS de actualizar } // Pinta el marcador void Scoreboard::render() { SDL_RenderTexture(renderer_, background_, nullptr, &rect_); } // Establece el valor de la variable void Scoreboard::setColor(Color color) { // Actualiza las variables de colores color_ = color; text_color1_ = param.scoreboard.text_autocolor ? color_.LIGHTEN(100) : param.scoreboard.text_color1; text_color2_ = param.scoreboard.text_autocolor ? color_.LIGHTEN(150) : param.scoreboard.text_color2; // Aplica los colores power_meter_sprite_->getTexture()->setColor(text_color2_); fillBackgroundTexture(); name_color_cycle_ = Colors::generateMirroredCycle(color_.INVERSE(), ColorCycleStyle::VIBRANT); } // Establece el valor de la variable void Scoreboard::setPos(SDL_FRect rect) { rect_ = rect; recalculateAnchors(); // Recalcula las anclas de los elementos createBackgroundTexture(); // Crea la textura de fondo createPanelTextures(); // Crea las texturas de los paneles fillBackgroundTexture(); // Rellena la textura de fondo } // Rellena los diferentes paneles del marcador void Scoreboard::fillPanelTextures() { // Guarda a donde apunta actualmente el renderizador auto* temp = SDL_GetRenderTarget(renderer_); // Genera el contenido de cada panel_ for (size_t i = 0; i < static_cast(Id::SIZE); ++i) { // Cambia el destino del renderizador SDL_SetRenderTarget(renderer_, panel_texture_.at(i)); // Dibuja el fondo de la textura SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); SDL_RenderClear(renderer_); renderPanelContent(i); } // Deja el renderizador apuntando donde estaba SDL_SetRenderTarget(renderer_, temp); } void Scoreboard::renderPanelContent(size_t panel_index) { switch (panel_.at(panel_index).mode) { case Mode::SCORE: renderScoreMode(panel_index); break; case Mode::DEMO: renderDemoMode(); break; case Mode::WAITING: renderWaitingMode(); break; case Mode::GAME_OVER: renderGameOverMode(); break; case Mode::STAGE_INFO: renderStageInfoMode(); break; case Mode::CONTINUE: renderContinueMode(panel_index); break; case Mode::SCORE_TO_ENTER_NAME: renderScoreToEnterNameMode(panel_index); break; case Mode::ENTER_NAME: renderEnterNameMode(panel_index); break; case Mode::ENTER_TO_SHOW_NAME: renderEnterToShowNameMode(panel_index); break; case Mode::SHOW_NAME: renderShowNameMode(panel_index); break; case Mode::GAME_COMPLETED: renderGameCompletedMode(panel_index); break; default: break; } } void Scoreboard::renderScoreMode(size_t panel_index) { // SCORE text_->writeDX(Text::COLOR | Text::CENTER, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_); text_->writeDX(Text::COLOR | Text::CENTER, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // MULT text_->writeDX(Text::COLOR | Text::CENTER, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 3"), 1, text_color1_); text_->writeDX(Text::COLOR | Text::CENTER, slot4_4_.x, slot4_4_.y, "x" + std::to_string(mult_.at(panel_index)).substr(0, 3), 1, text_color2_); } void Scoreboard::renderDemoMode() { // DEMO MODE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 6"), 1, text_color1_); // PRESS START TO PLAY if (time_counter_ % 10 < 8) { text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 8"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 9"), 1, text_color1_); } } void Scoreboard::renderWaitingMode() { // GAME OVER text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_); // PRESS START TO PLAY if (time_counter_ % 10 < 8) { text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 8"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 9"), 1, text_color1_); } } void Scoreboard::renderGameOverMode() { // GAME OVER text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_); // PLEASE WAIT if (time_counter_ % 10 < 8) { text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 12"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 13"), 1, text_color1_); } } void Scoreboard::renderStageInfoMode() { // STAGE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, Lang::getText("[SCOREBOARD] 5") + " " + std::to_string(stage_), 1, text_color1_); // POWERMETER power_meter_sprite_->setSpriteClip(0, 0, 40, 7); power_meter_sprite_->render(); power_meter_sprite_->setSpriteClip(40, 0, int(power_ * 40.0F), 7); power_meter_sprite_->render(); // HI-SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 4"), 1, text_color1_); const std::string NAME = hi_score_name_.empty() ? "" : hi_score_name_ + " - "; text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, NAME + updateScoreText(hi_score_), 1, text_color2_); } void Scoreboard::renderContinueMode(size_t panel_index) { // SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // CONTINUE text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 10"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, std::to_string(continue_counter_.at(panel_index)), 1, text_color2_); } void Scoreboard::renderScoreToEnterNameMode(size_t panel_index) { // Calcular progreso suavizado de la animación (0.0 a 1.0) const auto T = static_cast(easeInOutSine(text_slide_offset_.at(panel_index))); // Calcular desplazamientos reales entre slots (no son exactamente ROW_SIZE) const float DELTA_1_TO_2 = slot4_2_.y - slot4_1_.y; // Diferencia real entre ROW1 y ROW2 const float DELTA_2_TO_3 = slot4_3_.y - slot4_2_.y; // Diferencia real entre ROW2 y ROW3 const float DELTA_3_TO_4 = slot4_4_.y - slot4_3_.y; // Diferencia real entre ROW3 y ROW4 // ========== Texto que SALE hacia arriba ========== // name_ (sale desde ROW1 hacia arriba) text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y - (T * DELTA_1_TO_2), name_.at(panel_index), 1, text_color1_); // ========== Textos que SE MUEVEN hacia arriba ========== // score_ (se mueve de ROW2 a ROW1) text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y - (T * DELTA_1_TO_2), updateScoreText(score_.at(panel_index)), 1, text_color2_); // "ENTER NAME" (se mueve de ROW3 a ROW2) text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - (T * DELTA_2_TO_3), Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); // enter_name_ (se mueve de ROW4 a ROW3) text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - (T * DELTA_3_TO_4), enter_name_.at(panel_index), 1, text_color2_); // ========== Elemento que ENTRA desde abajo ========== // CARRUSEL (entra desde debajo de ROW4 hacia ROW4) renderCarousel(panel_index, slot4_4_.x, static_cast(slot4_4_.y + DELTA_3_TO_4 - (T * DELTA_3_TO_4))); } void Scoreboard::renderEnterNameMode(size_t panel_index) { /* // SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // ENTER NAME text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); renderNameInputField(panel_index); */ // SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // ENTER NAME text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); // NAME text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, enter_name_.at(panel_index), 1, text_color2_); // CARRUSEL renderCarousel(panel_index, slot4_4_.x, slot4_4_.y); } void Scoreboard::renderEnterToShowNameMode(size_t panel_index) { // Calcular progreso suavizado de la animación (0.0 a 1.0) const auto T = static_cast(easeInOutSine(text_slide_offset_.at(panel_index))); // Calcular desplazamientos reales entre slots (no son exactamente ROW_SIZE) const float DELTA_1_TO_2 = slot4_2_.y - slot4_1_.y; // Diferencia real entre ROW1 y ROW2 const float DELTA_2_TO_3 = slot4_3_.y - slot4_2_.y; // Diferencia real entre ROW2 y ROW3 const float DELTA_3_TO_4 = slot4_4_.y - slot4_3_.y; // Diferencia real entre ROW3 y ROW4 // ========== Texto que ENTRA desde arriba ========== // name_ (entra desde arriba hacia ROW1) // Debe venir desde donde estaría ROW0, que está a delta_1_to_2 píxeles arriba de ROW1 text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + (T * DELTA_1_TO_2) - DELTA_1_TO_2, name_.at(panel_index), 1, text_color1_); // ========== Textos que SE MUEVEN (renderizar UNA sola vez) ========== // SCORE (se mueve de ROW1 a ROW2) text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + (T * DELTA_1_TO_2), updateScoreText(score_.at(panel_index)), 1, text_color2_); // "ENTER NAME" (se mueve de ROW2 a ROW3) text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y + (T * DELTA_2_TO_3), Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); // enter_name_ (se mueve de ROW3 a ROW4) text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y + (T * DELTA_3_TO_4), enter_name_.at(panel_index), 1, text_color2_); // ========== Elemento que SALE hacia abajo ========== // CARRUSEL (sale desde ROW4 hacia abajo, fuera de pantalla) renderCarousel(panel_index, slot4_4_.x, static_cast(slot4_4_.y + (T * DELTA_3_TO_4))); } void Scoreboard::renderShowNameMode(size_t panel_index) { // NOMBRE DEL JUGADOR text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_); // SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // "ENTER NAME" text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); // NOMBRE INTRODUCIDO (con color animado) text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, enter_name_.at(panel_index), 1, animated_color_); } void Scoreboard::renderGameCompletedMode(size_t panel_index) { // GAME OVER text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_); // SCORE if (time_counter_ % 10 < 8) { text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 14"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, updateScoreText(score_.at(panel_index)), 1, text_color2_); } } // Rellena la textura de fondo void Scoreboard::fillBackgroundTexture() { // Rellena los diferentes paneles del marcador fillPanelTextures(); // Cambia el destino del renderizador SDL_Texture* temp = SDL_GetRenderTarget(renderer_); SDL_SetRenderTarget(renderer_, background_); // Dibuja el fondo del marcador SDL_SetRenderDrawColor(renderer_, color_.r, color_.g, color_.b, 255); SDL_RenderClear(renderer_); // Copia las texturas de los paneles for (int i = 0; i < static_cast(Id::SIZE); ++i) { SDL_RenderTexture(renderer_, panel_texture_.at(i), nullptr, &panel_.at(i).pos); } // Dibuja la linea que separa la zona de juego del marcador renderSeparator(); // Deja el renderizador apuntando donde estaba SDL_SetRenderTarget(renderer_, temp); } // Recalcula las anclas de los elementos void Scoreboard::recalculateAnchors() { // Recalcula la posición y el tamaño de los paneles const float PANEL_WIDTH = rect_.w / (float)static_cast(Id::SIZE); for (int i = 0; i < static_cast(Id::SIZE); ++i) { panel_.at(i).pos.x = roundf(PANEL_WIDTH * i); panel_.at(i).pos.y = 0; panel_.at(i).pos.w = roundf(PANEL_WIDTH * (i + 1)) - panel_.at(i).pos.x; panel_.at(i).pos.h = rect_.h; } // Constantes para definir las zonas del panel_: 4 filas y 1 columna const int ROW_SIZE = rect_.h / 4; const int TEXT_HEIGHT = 7; // Filas const float ROW1 = 1 + (ROW_SIZE * 0) + (TEXT_HEIGHT / 2); const float ROW2 = 1 + (ROW_SIZE * 1) + (TEXT_HEIGHT / 2) - 1; const float ROW3 = 1 + (ROW_SIZE * 2) + (TEXT_HEIGHT / 2) - 2; const float ROW4 = 1 + (ROW_SIZE * 3) + (TEXT_HEIGHT / 2) - 3; // Columna const float COL = PANEL_WIDTH / 2; // Slots de 4 slot4_1_ = {.x = COL, .y = ROW1}; slot4_2_ = {.x = COL, .y = ROW2}; slot4_3_ = {.x = COL, .y = ROW3}; slot4_4_ = {.x = COL, .y = ROW4}; // Primer cuadrado para poner el nombre de record const int ENTER_NAME_LENGTH = text_->length(std::string(EnterName::MAX_NAME_SIZE, 'A')); enter_name_pos_.x = COL - (ENTER_NAME_LENGTH / 2); enter_name_pos_.y = ROW4; // Recoloca los sprites if (power_meter_sprite_) { power_meter_sprite_->setX(slot4_2_.x - 20); power_meter_sprite_->setY(slot4_2_.y); } } // Crea la textura de fondo void Scoreboard::createBackgroundTexture() { // Elimina la textura en caso de existir if (background_ != nullptr) { SDL_DestroyTexture(background_); } // Recrea la textura de fondo background_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, rect_.w, rect_.h); SDL_SetTextureBlendMode(background_, SDL_BLENDMODE_BLEND); } // Crea las texturas de los paneles void Scoreboard::createPanelTextures() { // Elimina las texturas en caso de existir for (auto* texture : panel_texture_) { if (texture != nullptr) { SDL_DestroyTexture(texture); } } panel_texture_.clear(); // Crea las texturas para cada panel_ for (auto& i : panel_) { SDL_Texture* tex = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, i.pos.w, i.pos.h); SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); panel_texture_.push_back(tex); } } // Dibuja la linea que separa la zona de juego del marcador void Scoreboard::renderSeparator() { // Dibuja la linea que separa el marcador de la zona de juego auto color = param.scoreboard.separator_autocolor ? color_.DARKEN() : param.scoreboard.separator_color; SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 255); SDL_RenderLine(renderer_, 0, 0, rect_.w, 0); } // Pinta el carrusel de caracteres con efecto de color LERP y animación suave void Scoreboard::renderCarousel(size_t panel_index, int center_x, int y) { // Obtener referencia a EnterName EnterName* enter_name = enter_name_ref_.at(panel_index); if (enter_name == nullptr) { return; } // Obtener la lista completa de caracteres const std::string& char_list = enter_name->getCharacterList(); if (char_list.empty()) { return; } // --- Parámetros del carrusel --- constexpr int EXTRA_SPACING = 2; constexpr int HALF_VISIBLE = CAROUSEL_VISIBLE_LETTERS / 2; // 4 letras a cada lado // Posición flotante actual del carrusel (índice en la lista de caracteres) float CAROUSEL_POS = carousel_position_.at(panel_index); const int CHAR_LIST_SIZE = static_cast(char_list.size()); // Calcular ancho promedio de una letra (asumimos ancho uniforme) const int AVG_CHAR_WIDTH = text_->getCharacterSize(); const int CHAR_STEP = AVG_CHAR_WIDTH + EXTRA_SPACING; // --- Corrección visual de residuales flotantes (evita “baile”) --- float frac = CAROUSEL_POS - std::floor(CAROUSEL_POS); if (frac > 0.999f || frac < 0.001f) { CAROUSEL_POS = std::round(CAROUSEL_POS); frac = 0.0f; } const float FRACTIONAL_OFFSET = frac; const int PIXEL_OFFSET = static_cast(FRACTIONAL_OFFSET * CHAR_STEP + 0.5f); // Índice base en la lista de caracteres (posición central) const int BASE_INDEX = static_cast(std::floor(CAROUSEL_POS)); // Calcular posición X inicial (centrar las 9 letras visibles) int start_x = center_x - (HALF_VISIBLE * CHAR_STEP) - (AVG_CHAR_WIDTH / 2) - PIXEL_OFFSET; // === Renderizar las letras visibles del carrusel === for (int i = -HALF_VISIBLE; i <= HALF_VISIBLE; ++i) { // Índice real en character_list_ (con wrap-around circular) int char_index = BASE_INDEX + i; char_index = char_index % CHAR_LIST_SIZE; if (char_index < 0) { char_index += CHAR_LIST_SIZE; } // --- Calcular distancia circular correcta (corregido el bug de wrap) --- float normalized_pos = std::fmod(CAROUSEL_POS, static_cast(CHAR_LIST_SIZE)); if (normalized_pos < 0.0f) normalized_pos += static_cast(CHAR_LIST_SIZE); float diff = std::abs(static_cast(char_index) - normalized_pos); if (diff > static_cast(CHAR_LIST_SIZE) / 2.0f) diff = static_cast(CHAR_LIST_SIZE) - diff; const float distance_from_center = diff; // --- Seleccionar color con LERP según la distancia --- Color letter_color; if (distance_from_center < 0.5F) { // Letra central → transiciona hacia animated_color_ float lerp_to_animated = distance_from_center / 0.5F; // 0.0 a 1.0 letter_color = animated_color_.LERP(text_color1_, lerp_to_animated); } else { // Letras alejadas → degradan hacia color_ base float base_lerp = (distance_from_center - 0.5F) / (HALF_VISIBLE - 0.5F); base_lerp = std::min(base_lerp, 1.0F); const float LERP_FACTOR = base_lerp * 0.85F; letter_color = text_color1_.LERP(color_, LERP_FACTOR); } // Calcular posición X de la letra const int LETTER_X = start_x + ((i + HALF_VISIBLE) * CHAR_STEP); // Renderizar la letra std::string single_char(1, char_list[char_index]); text_->writeDX(Text::COLOR, LETTER_X, y, single_char, 1, letter_color); } }