Files
coffee_crisis_arcade_edition/source/scoreboard.cpp

645 lines
25 KiB
C++

#include "scoreboard.h"
#include <SDL3/SDL.h> // 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 <algorithm> // Para max
#include <cmath> // Para roundf
#include <iomanip> // Para operator<<, setfill, setw
#include <sstream> // 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<Sprite>(game_power_meter_texture_)),
text_(Resource::get()->getText("8bithud")) {
// Inicializa variables
for (size_t i = 0; i < static_cast<size_t>(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;
previous_mode_.at(i) = Mode::SCORE;
text_slide_offset_.at(i) = 0.0f;
is_sliding_.at(i) = false;
}
panel_.at(static_cast<size_t>(Id::LEFT)).mode = Mode::SCORE;
panel_.at(static_cast<size_t>(Id::RIGHT)).mode = Mode::SCORE;
panel_.at(static_cast<size_t>(Id::CENTER)).mode = Mode::STAGE_INFO;
// Recalcula las anclas de los elementos
recalculateAnchors();
power_meter_sprite_->setPosition(SDL_FRect{
static_cast<float>(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) {
size_t idx = static_cast<size_t>(id);
// Guardar referencia a EnterName
enter_name_ref_.at(idx) = enter_name_ptr;
if (!enter_name_ptr || selected_index < 0) {
return;
}
// Primera inicialización: posicionar directamente sin animar
if (carousel_prev_index_.at(idx) == -1) {
carousel_position_.at(idx) = static_cast<float>(selected_index);
carousel_target_.at(idx) = static_cast<float>(selected_index);
carousel_prev_index_.at(idx) = selected_index;
} else {
// Detectar cambio en el índice del carácter seleccionado
int prev_index = carousel_prev_index_.at(idx);
if (selected_index != prev_index) {
// Calcular dirección del movimiento
int direction = selected_index - prev_index;
// Obtener tamaño de la lista para manejar wrap-around
const int LIST_SIZE = enter_name_ptr->getCharacterList().size();
// Manejar wrap-around circular
if (direction > LIST_SIZE / 2) {
direction = -(LIST_SIZE - direction); // Wrap backward (ej: Z → A)
} else if (direction < -LIST_SIZE / 2) {
direction = LIST_SIZE + direction; // Wrap forward (ej: A → Z)
}
// Normalizar a -1 o +1
direction = (direction > 0) ? 1 : ((direction < 0) ? -1 : 0);
if (direction != 0) {
// Actualizar target con movimiento relativo
carousel_target_.at(idx) = carousel_position_.at(idx) + static_cast<float>(direction);
// Guardar nuevo índice
carousel_prev_index_.at(idx) = selected_index;
}
}
}
}
// 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 animación de deslizamiento de texto (transición ENTER_NAME -> SHOW_NAME)
void Scoreboard::updateTextSlideAnimation(float deltaTime) {
const float ROW_SIZE = rect_.h / 4.0f; // Altura de una fila
for (size_t i = 0; i < static_cast<size_t>(Id::SIZE); ++i) {
Mode current_mode = panel_.at(i).mode;
// Detectar transición de ENTER_NAME a SHOW_NAME
if (previous_mode_.at(i) == Mode::ENTER_NAME && current_mode == Mode::SHOW_NAME) {
// Iniciar animación
is_sliding_.at(i) = true;
text_slide_offset_.at(i) = 0.0f;
}
// Actualizar offset durante animación
if (is_sliding_.at(i)) {
// Incrementar offset basado en tiempo
float slide_speed = ROW_SIZE / TEXT_SLIDE_DURATION; // Píxeles por segundo
text_slide_offset_.at(i) += slide_speed * deltaTime;
// Terminar animación cuando se complete
if (text_slide_offset_.at(i) >= ROW_SIZE) {
text_slide_offset_.at(i) = ROW_SIZE;
is_sliding_.at(i) = false;
}
}
// Actualizar modo previo para la próxima iteración
previous_mode_.at(i) = current_mode;
}
}
// Actualiza la lógica del marcador
void Scoreboard::update(float deltaTime) {
fillBackgroundTexture();
updateTimeCounter();
updateNameColorIndex();
updateCarouselAnimation(deltaTime);
updateTextSlideAnimation(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<int>(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) {
// Calcular offset de animación de deslizamiento
const float ROW_SIZE = rect_.h / 4.0f;
float y_offset = 0.0f;
if (is_sliding_.at(panel_index)) {
// Durante animación: offset va de -ROW_SIZE (arriba) a 0 (posición final)
y_offset = -ROW_SIZE + text_slide_offset_.at(panel_index);
}
// NOMBRE DEL JUGADOR (texto0 - entra desde arriba)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + y_offset, name_.at(panel_index), 1, text_color1_);
// SCORE (texto1 - se desplaza hacia abajo)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y + y_offset, updateScoreText(score_.at(panel_index)), 1, text_color2_);
// "ENTER NAME" (texto2 - se desplaza hacia abajo)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y + y_offset, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_);
// NOMBRE INTRODUCIDO (texto3 - se desplaza hacia abajo)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y + y_offset, 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<int>(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<int>(Id::SIZE);
for (int i = 0; i < static_cast<int>(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) {
return;
}
// Obtener la lista completa de caracteres
const std::string& char_list = enter_name->getCharacterList();
if (char_list.empty()) {
return;
}
// Espacio extra entre letras
constexpr int EXTRA_SPACING = 2;
// Carrusel extendido: usar constante de clase
constexpr int HALF_VISIBLE = CAROUSEL_VISIBLE_LETTERS / 2; // 4
// Posición flotante actual del carrusel (índice en character_list_)
const float carousel_pos = carousel_position_.at(panel_index);
// Calcular ancho promedio de una letra (asumimos ancho uniforme)
std::string sample_char(1, char_list[0]);
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<int>(fractional_offset * CHAR_STEP);
// Índice base en character_list_ (centro del carrusel)
const int base_index = static_cast<int>(std::floor(carousel_pos));
const int char_list_size = static_cast<int>(char_list.size());
// 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 character_list_ (con wrap-around circular)
int char_index = base_index + i;
// Wrap-around circular
char_index = char_index % char_list_size;
if (char_index < 0) {
char_index += char_list_size;
}
// Obtener el carácter directamente de character_list_
std::string single_char(1, char_list[char_index]);
// Calcular distancia flotante al centro visual basada en posición real del carácter
float distance_from_center = std::abs(static_cast<float>(char_index) - carousel_pos);
// Manejar wrap-around circular: elegir el camino más corto
if (distance_from_center > static_cast<float>(char_list_size) / 2.0f) {
distance_from_center = static_cast<float>(char_list_size) - distance_from_center;
}
// Calcular color con LERP dinámico continuo
Color letter_color;
if (distance_from_center < 0.5f) {
// Letra cerca del centro: LERP hacia animated_color_
// distance_from_center va de 0.0 (centro exacto) a 0.5 (borde)
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: LERP hacia color_ (fade out)
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 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);
}
}