#include "scoreboard.h" #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 // Para basic_ostream, basic_ostringstream, basic_ostream::operator<<, ostringstream #include "color.h" #include "enter_name.h" // Para NAME_SIZE #include "lang.h" // Para getText #include "param.h" // Para Param, ParamScoreboard, param #include "resource.h" // Para Resource #include "screen.h" // Para Screen #include "sprite.h" // Para Sprite #include "text.h" // Para Text, Text::CENTER, Text::COLOR #include "texture.h" // Para Texture // .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; } 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); } } } // Establece el carrusel y detecta cambios para iniciar animación void Scoreboard::setCarousel(Id id, const std::string& carousel) { size_t idx = static_cast(id); if (carousel.empty()) { carousel_.at(idx) = carousel; return; } // Calcular el índice central del nuevo carrusel int new_center_index = carousel.size() / 2; // Si es la primera vez o cambió el índice, establecer nuevo target if (carousel_.at(idx).empty()) { // Primera inicialización: posicionar directamente sin animar carousel_position_.at(idx) = static_cast(new_center_index); carousel_target_.at(idx) = static_cast(new_center_index); carousel_prev_index_.at(idx) = new_center_index; } else { // Calcular índice central del carrusel anterior int prev_center_index = carousel_.at(idx).size() / 2; // Detectar si cambió (LEFT o RIGHT fue presionado) if (new_center_index != prev_center_index) { // Establecer nuevo target (la animación se hará en updateCarouselAnimation) carousel_target_.at(idx) = static_cast(new_center_index); } carousel_prev_index_.at(idx) = new_center_index; } carousel_.at(idx) = carousel; } // 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 deltaTime) { 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 * deltaTime * 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); } } } // Actualiza la lógica del marcador void Scoreboard::update(float deltaTime) { fillBackgroundTexture(); updateTimeCounter(); updateNameColorIndex(); updateCarouselAnimation(deltaTime); } // 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::ENTER_NAME: renderEnterNameMode(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::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::renderShowNameMode(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_); // NAME text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); // NOMBRE INTRODUCIDO 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) { const std::string& carousel = carousel_.at(panel_index); if (carousel.empty()) { return; } // Espacio extra entre letras constexpr int EXTRA_SPACING = 2; // Carrusel extendido: 9 letras visibles (4 izq + centro + 4 der) constexpr int VISIBLE_LETTERS = 9; constexpr int HALF_VISIBLE = VISIBLE_LETTERS / 2; // 4 // Posición flotante actual del carrusel const float carousel_pos = carousel_position_.at(panel_index); // Calcular ancho promedio de una letra (asumimos ancho uniforme para simplificar) std::string sample_char(1, carousel[carousel.size() / 2]); const int AVG_CHAR_WIDTH = text_->length(sample_char, 1); const int CHAR_STEP = AVG_CHAR_WIDTH + EXTRA_SPACING; // Calcular offset de píxeles basado en la parte fraccionaria de carousel_pos const float fractional_offset = carousel_pos - std::floor(carousel_pos); const int pixel_offset = static_cast(fractional_offset * CHAR_STEP); // Índice base (centro del carrusel) const int base_index = static_cast(std::floor(carousel_pos)); // Calcular posición X inicial (centrar el conjunto de 9 letras) int start_x = center_x - (HALF_VISIBLE * CHAR_STEP) - (AVG_CHAR_WIDTH / 2) - pixel_offset; // Renderizar las 9 letras visibles for (int i = -HALF_VISIBLE; i <= HALF_VISIBLE; ++i) { // Índice real en la lista de caracteres (con wrap-around circular) int char_index = base_index + i; const int carousel_size = static_cast(carousel.size()); // Wrap-around circular while (char_index < 0) { char_index += carousel_size; } while (char_index >= carousel_size) { char_index -= carousel_size; } // Obtener el carácter std::string single_char(1, carousel[char_index]); // Calcular distancia flotante al centro visual const float distance_from_center = std::abs(static_cast(i) + fractional_offset); // Calcular color con LERP dinámico Color letter_color; if (distance_from_center < 0.5f) { // Letra central: animated_color_ letter_color = animated_color_; } else { // Letras laterales: LERP desde text_color1_ hacia color_ // Factor: 0.0 cerca del centro, 0.85 en los extremos (85% fade) const float LERP_FACTOR = std::min((distance_from_center - 0.5f) / (HALF_VISIBLE - 0.5f), 1.0f) * 0.85f; letter_color = text_color1_.LERP(color_, LERP_FACTOR); } // Calcular posición X de esta letra const int letter_x = start_x + (i + HALF_VISIBLE) * CHAR_STEP; // Pintar la letra text_->writeDX(Text::COLOR, letter_x, y, single_char, 1, letter_color); } }