PHASE 2: Refactorización completa del sistema de temas unificado

Arquitectura polimórfica implementada:
- Jerarquía: Theme (base) → StaticTheme / DynamicTheme (derivadas)
- Vector unificado de 10 temas (7 estáticos + 3 dinámicos)
- Eliminada lógica dual (if(dynamic_theme_active_) scattered)

Nuevos archivos:
- source/themes/theme.h: Interfaz base abstracta
- source/themes/static_theme.h/cpp: Temas estáticos (1 keyframe)
- source/themes/dynamic_theme.h/cpp: Temas dinámicos (N keyframes animados)
- source/theme_manager.h/cpp: Gestión unificada de temas

Mejoras de API:
- switchToTheme(0-9): Cambio a cualquier tema (índice 0-9)
- cycleTheme(): Cicla por todos los temas (Tecla B)
- update(delta_time): Actualización simplificada
- getInterpolatedColor(idx): Sin parámetro balls_

Bugs corregidos:
- Tecla B ahora cicla TODOS los 10 temas (antes solo 6)
- DEMO mode elige de TODOS los temas (antes excluía LAVENDER + dinámicos)
- Eliminada duplicación de keyframes en temas dinámicos (loop=true lo maneja)

Código reducido:
- theme_manager.cpp: 558 → 320 líneas (-43%)
- engine.cpp: Eliminados ~470 líneas de lógica de temas
- Complejidad significativamente reducida

Preparado para PHASE 3 (LERP universal entre cualquier par de temas)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-09 13:17:54 +02:00
parent b93028396a
commit a134ae428f
11 changed files with 988 additions and 680 deletions

View File

@@ -204,8 +204,11 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
srand(static_cast<unsigned>(time(nullptr)));
dbg_init(renderer_);
initializeThemes();
initializeDynamicThemes();
// Inicializar ThemeManager
theme_manager_ = std::make_unique<ThemeManager>();
theme_manager_->initialize();
initBalls(scenario_);
}
@@ -289,27 +292,8 @@ void Engine::update() {
// Actualizar Modo DEMO (auto-play)
updateDemoMode();
// Actualizar transición de tema (LERP)
if (transitioning_) {
transition_progress_ += delta_time_ / transition_duration_;
if (transition_progress_ >= 1.0f) {
// Transición completa
transition_progress_ = 1.0f;
current_theme_ = target_theme_;
transitioning_ = false;
// Actualizar colores de las pelotas al tema final
const ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
for (size_t i = 0; i < balls_.size(); i++) {
size_t color_index = i % theme.ball_colors.size();
balls_[i]->setColor(theme.ball_colors[color_index]);
}
}
}
// Actualizar tema dinámico (animación de keyframes)
updateDynamicTheme();
// Actualizar transiciones de temas (delegado a ThemeManager)
theme_manager_->update(delta_time_);
}
void Engine::handleEvents() {
@@ -437,53 +421,118 @@ void Engine::handleEvents() {
// Ciclar temas de color (movido de T a B)
case SDLK_B:
// Ciclar al siguiente tema con transición suave (LERP)
{
ColorTheme next_theme = static_cast<ColorTheme>((static_cast<int>(current_theme_) + 1) % (sizeof(themes_) / sizeof(themes_[0])));
startThemeTransition(next_theme);
// Ciclar al siguiente tema con transición suave
theme_manager_->cycleTheme();
// Mostrar nombre del tema (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
// Temas de colores con teclado numérico (con transición suave)
case SDLK_KP_1:
startThemeTransition(ColorTheme::SUNSET);
theme_manager_->switchToTheme(0); // SUNSET
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
case SDLK_KP_2:
startThemeTransition(ColorTheme::OCEAN);
theme_manager_->switchToTheme(1); // OCEAN
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
case SDLK_KP_3:
startThemeTransition(ColorTheme::NEON);
theme_manager_->switchToTheme(2); // NEON
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
case SDLK_KP_4:
startThemeTransition(ColorTheme::FOREST);
theme_manager_->switchToTheme(3); // FOREST
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
case SDLK_KP_5:
startThemeTransition(ColorTheme::RGB);
theme_manager_->switchToTheme(4); // RGB
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
case SDLK_KP_6:
startThemeTransition(ColorTheme::MONOCHROME);
theme_manager_->switchToTheme(5); // MONOCHROME
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
case SDLK_KP_7:
startThemeTransition(ColorTheme::LAVENDER);
theme_manager_->switchToTheme(6); // LAVENDER
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
// Temas dinámicos (animados) - Solo Numpad 8/9/0 (teclas normales usadas para escenarios)
case SDLK_KP_8:
activateDynamicTheme(0); // SUNRISE
theme_manager_->switchToTheme(7); // SUNRISE
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
case SDLK_KP_9:
activateDynamicTheme(1); // OCEAN WAVES
theme_manager_->switchToTheme(8); // OCEAN WAVES
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
case SDLK_KP_0:
activateDynamicTheme(2); // NEON PULSE
theme_manager_->switchToTheme(9); // NEON PULSE
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
// Cambio de sprite/textura dinámico
@@ -610,7 +659,7 @@ void Engine::handleEvents() {
case SDLK_D:
// Shift+D = Pausar tema dinámico
if (event.key.mod & SDL_KMOD_SHIFT) {
pauseDynamicTheme();
theme_manager_->pauseDynamic();
} else {
// D sin Shift = Toggle modo DEMO
if (current_app_mode_ == AppMode::DEMO) {
@@ -675,8 +724,40 @@ void Engine::render() {
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); // Negro para barras de letterbox/integer
SDL_RenderClear(renderer_);
// Renderizar fondo degradado en lugar de color sólido
renderGradientBackground();
// Renderizar fondo degradado (delegado a ThemeManager)
{
float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b;
theme_manager_->getBackgroundColors(top_r, top_g, top_b, bottom_r, bottom_g, bottom_b);
// Crear quad de pantalla completa con degradado
SDL_Vertex bg_vertices[4];
// Vértice superior izquierdo
bg_vertices[0].position = {0, 0};
bg_vertices[0].tex_coord = {0.0f, 0.0f};
bg_vertices[0].color = {top_r, top_g, top_b, 1.0f};
// Vértice superior derecho
bg_vertices[1].position = {static_cast<float>(current_screen_width_), 0};
bg_vertices[1].tex_coord = {1.0f, 0.0f};
bg_vertices[1].color = {top_r, top_g, top_b, 1.0f};
// Vértice inferior derecho
bg_vertices[2].position = {static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_)};
bg_vertices[2].tex_coord = {1.0f, 1.0f};
bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f};
// Vértice inferior izquierdo
bg_vertices[3].position = {0, static_cast<float>(current_screen_height_)};
bg_vertices[3].tex_coord = {0.0f, 1.0f};
bg_vertices[3].color = {bottom_r, bottom_g, bottom_b, 1.0f};
// Índices para 2 triángulos
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
// Renderizar sin textura (nullptr)
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
}
// Limpiar batches del frame anterior
batch_vertices_.clear();
@@ -701,7 +782,7 @@ void Engine::render() {
// Renderizar en orden de profundidad (fondo → frente)
for (size_t idx : render_order) {
SDL_FRect pos = balls_[idx]->getPosition();
Color color = getInterpolatedColor(idx); // Usar color interpolado (LERP)
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
float brightness = balls_[idx]->getDepthBrightness();
float depth_scale = balls_[idx]->getDepthScale();
@@ -720,7 +801,7 @@ void Engine::render() {
size_t idx = 0;
for (auto& ball : balls_) {
SDL_FRect pos = ball->getPosition();
Color color = getInterpolatedColor(idx); // Usar color interpolado (LERP)
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f);
idx++;
}
@@ -732,26 +813,10 @@ void Engine::render() {
}
if (show_text_) {
// Obtener datos del tema actual (estático o dinámico)
// Obtener datos del tema actual (delegado a ThemeManager)
int text_color_r, text_color_g, text_color_b;
const char* theme_name_es = nullptr;
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
// Tema dinámico activo
const DynamicTheme& dyn_theme = dynamic_themes_[current_dynamic_theme_index_];
text_color_r = dyn_theme.text_color_r;
text_color_g = dyn_theme.text_color_g;
text_color_b = dyn_theme.text_color_b;
theme_name_es = dyn_theme.name_es;
} else {
// Tema estático
int theme_idx = static_cast<int>(current_theme_);
const ThemeColors& current = themes_[theme_idx];
text_color_r = current.text_color_r;
text_color_g = current.text_color_g;
text_color_b = current.text_color_b;
theme_name_es = current.name_es;
}
theme_manager_->getCurrentThemeTextColor(text_color_r, text_color_g, text_color_b);
const char* theme_name_es = theme_manager_->getCurrentThemeNameES();
// Texto del número de pelotas con color del tema
dbg_print(text_pos_, 8, text_.c_str(), text_color_r, text_color_g, text_color_b);
@@ -803,13 +868,8 @@ void Engine::render() {
dbg_print(8, 56, gravity_dir_text.c_str(), 255, 255, 0); // Amarillo para dirección
}
// Debug: Mostrar tema actual
std::string theme_text;
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
theme_text = std::string("THEME ") + dynamic_themes_[current_dynamic_theme_index_].name_en;
} else {
theme_text = std::string("THEME ") + themes_[static_cast<int>(current_theme_)].name_en;
}
// Debug: Mostrar tema actual (delegado a ThemeManager)
std::string theme_text = std::string("THEME ") + theme_manager_->getCurrentThemeNameEN();
dbg_print(8, 64, theme_text.c_str(), 255, 255, 128); // Amarillo claro para tema
// Debug: Mostrar modo de simulación actual
@@ -872,20 +932,10 @@ void Engine::initBalls(int value) {
const float VX = (((rand() % 20) + 10) * 0.1f) * SIGN; // Velocidad en X
const float VY = ((rand() % 60) - 30) * 0.1f; // Velocidad en Y
// Seleccionar color de la paleta del tema actual (estático o dinámico)
Color COLOR;
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
// Tema dinámico: usar colores del keyframe actual
const DynamicTheme& dyn_theme = dynamic_themes_[current_dynamic_theme_index_];
const DynamicThemeKeyframe& current_kf = dyn_theme.keyframes[current_keyframe_index_];
int color_index = rand() % current_kf.ball_colors.size();
COLOR = current_kf.ball_colors[color_index];
} else {
// Tema estático
ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
int color_index = rand() % theme.ball_colors.size();
COLOR = theme.ball_colors[color_index];
}
// Seleccionar color de la paleta del tema actual (delegado a ThemeManager)
int random_index = rand();
Color COLOR = theme_manager_->getInitialBallColor(random_index);
// Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada)
float mass_factor = GRAVITY_MASS_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN);
balls_.emplace_back(std::make_unique<Ball>(X, VX, VY, COLOR, texture_, current_screen_width_, current_screen_height_, current_ball_size_, current_gravity_, mass_factor));
@@ -1114,77 +1164,6 @@ std::string Engine::gravityDirectionToString(GravityDirection direction) const {
}
}
void Engine::renderGradientBackground() {
// Crear quad de pantalla completa con degradado
SDL_Vertex bg_vertices[4];
// Obtener colores (con LERP si estamos en transición)
float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b;
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
// Tema dinámico activo: interpolar entre keyframes
DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];
const DynamicThemeKeyframe& current_kf = theme.keyframes[current_keyframe_index_];
const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];
top_r = lerp(current_kf.bg_top_r, target_kf.bg_top_r, dynamic_transition_progress_);
top_g = lerp(current_kf.bg_top_g, target_kf.bg_top_g, dynamic_transition_progress_);
top_b = lerp(current_kf.bg_top_b, target_kf.bg_top_b, dynamic_transition_progress_);
bottom_r = lerp(current_kf.bg_bottom_r, target_kf.bg_bottom_r, dynamic_transition_progress_);
bottom_g = lerp(current_kf.bg_bottom_g, target_kf.bg_bottom_g, dynamic_transition_progress_);
bottom_b = lerp(current_kf.bg_bottom_b, target_kf.bg_bottom_b, dynamic_transition_progress_);
} else if (transitioning_) {
// Transición estática: interpolar entre tema actual y tema destino
ThemeColors& current = themes_[static_cast<int>(current_theme_)];
ThemeColors& target = themes_[static_cast<int>(target_theme_)];
top_r = lerp(current.bg_top_r, target.bg_top_r, transition_progress_);
top_g = lerp(current.bg_top_g, target.bg_top_g, transition_progress_);
top_b = lerp(current.bg_top_b, target.bg_top_b, transition_progress_);
bottom_r = lerp(current.bg_bottom_r, target.bg_bottom_r, transition_progress_);
bottom_g = lerp(current.bg_bottom_g, target.bg_bottom_g, transition_progress_);
bottom_b = lerp(current.bg_bottom_b, target.bg_bottom_b, transition_progress_);
} else {
// Sin transición: usar tema estático actual directamente
ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
top_r = theme.bg_top_r;
top_g = theme.bg_top_g;
top_b = theme.bg_top_b;
bottom_r = theme.bg_bottom_r;
bottom_g = theme.bg_bottom_g;
bottom_b = theme.bg_bottom_b;
}
// Vértice superior izquierdo
bg_vertices[0].position = {0, 0};
bg_vertices[0].tex_coord = {0.0f, 0.0f};
bg_vertices[0].color = {top_r, top_g, top_b, 1.0f};
// Vértice superior derecho
bg_vertices[1].position = {static_cast<float>(current_screen_width_), 0};
bg_vertices[1].tex_coord = {1.0f, 0.0f};
bg_vertices[1].color = {top_r, top_g, top_b, 1.0f};
// Vértice inferior derecho
bg_vertices[2].position = {static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_)};
bg_vertices[2].tex_coord = {1.0f, 1.0f};
bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f};
// Vértice inferior izquierdo
bg_vertices[3].position = {0, static_cast<float>(current_screen_height_)};
bg_vertices[3].tex_coord = {0.0f, 1.0f};
bg_vertices[3].color = {bottom_r, bottom_g, bottom_b, 1.0f};
// Índices para 2 triángulos
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
// Renderizar sin textura (nullptr)
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
}
void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
int vertex_index = static_cast<int>(batch_vertices_.size());
@@ -1312,473 +1291,6 @@ void Engine::zoomOut() {
setWindowZoom(current_window_zoom_ - 1);
}
void Engine::initializeThemes() {
// SUNSET: Naranjas, rojos, amarillos, rosas (8 colores)
themes_[0] = {
"SUNSET",
"ATARDECER", // Nombres (inglés, español)
255,
140,
60, // Color texto: naranja cálido
180.0f / 255.0f,
140.0f / 255.0f,
100.0f / 255.0f, // Fondo superior (naranja suave)
40.0f / 255.0f,
20.0f / 255.0f,
60.0f / 255.0f, // Fondo inferior (púrpura oscuro)
{{255, 140, 0}, {255, 69, 0}, {255, 215, 0}, {255, 20, 147}, {255, 99, 71}, {255, 165, 0}, {255, 192, 203}, {220, 20, 60}}};
// OCEAN: Azules, turquesas, blancos (8 colores)
themes_[1] = {
"OCEAN",
"OCEANO", // Nombres (inglés, español)
80,
200,
255, // Color texto: azul océano
100.0f / 255.0f,
150.0f / 255.0f,
200.0f / 255.0f, // Fondo superior (azul cielo)
20.0f / 255.0f,
40.0f / 255.0f,
80.0f / 255.0f, // Fondo inferior (azul marino)
{{0, 191, 255}, {0, 255, 255}, {32, 178, 170}, {176, 224, 230}, {70, 130, 180}, {0, 206, 209}, {240, 248, 255}, {64, 224, 208}}};
// NEON: Cian, magenta, verde lima, amarillo vibrante (8 colores)
themes_[2] = {
"NEON",
"NEON", // Nombres (inglés, español)
255,
60,
255, // Color texto: magenta brillante
20.0f / 255.0f,
20.0f / 255.0f,
40.0f / 255.0f, // Fondo superior (negro azulado)
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior (negro)
{{0, 255, 255}, {255, 0, 255}, {50, 205, 50}, {255, 255, 0}, {255, 20, 147}, {0, 255, 127}, {138, 43, 226}, {255, 69, 0}}};
// FOREST: Verdes, marrones, amarillos otoño (8 colores)
themes_[3] = {
"FOREST",
"BOSQUE", // Nombres (inglés, español)
100,
255,
100, // Color texto: verde natural
144.0f / 255.0f,
238.0f / 255.0f,
144.0f / 255.0f, // Fondo superior (verde claro)
101.0f / 255.0f,
67.0f / 255.0f,
33.0f / 255.0f, // Fondo inferior (marrón tierra)
{{34, 139, 34}, {107, 142, 35}, {154, 205, 50}, {255, 215, 0}, {210, 180, 140}, {160, 82, 45}, {218, 165, 32}, {50, 205, 50}}};
// RGB: Círculo cromático con 24 puntos (cada 15°) - Ultra precisión matemática
themes_[4] = {
"RGB",
"RGB", // Nombres (inglés, español)
100,
100,
100, // Color texto: gris oscuro (contraste con fondo blanco)
1.0f,
1.0f,
1.0f, // Fondo superior (blanco puro)
1.0f,
1.0f,
1.0f, // Fondo inferior (blanco puro) - sin degradado
{
{255, 0, 0}, // 0° - Rojo puro
{255, 64, 0}, // 15° - Rojo-Naranja
{255, 128, 0}, // 30° - Naranja
{255, 191, 0}, // 45° - Naranja-Amarillo
{255, 255, 0}, // 60° - Amarillo puro
{191, 255, 0}, // 75° - Amarillo-Verde claro
{128, 255, 0}, // 90° - Verde-Amarillo
{64, 255, 0}, // 105° - Verde claro-Amarillo
{0, 255, 0}, // 120° - Verde puro
{0, 255, 64}, // 135° - Verde-Cian claro
{0, 255, 128}, // 150° - Verde-Cian
{0, 255, 191}, // 165° - Verde claro-Cian
{0, 255, 255}, // 180° - Cian puro
{0, 191, 255}, // 195° - Cian-Azul claro
{0, 128, 255}, // 210° - Azul-Cian
{0, 64, 255}, // 225° - Azul claro-Cian
{0, 0, 255}, // 240° - Azul puro
{64, 0, 255}, // 255° - Azul-Magenta claro
{128, 0, 255}, // 270° - Azul-Magenta
{191, 0, 255}, // 285° - Azul claro-Magenta
{255, 0, 255}, // 300° - Magenta puro
{255, 0, 191}, // 315° - Magenta-Rojo claro
{255, 0, 128}, // 330° - Magenta-Rojo
{255, 0, 64} // 345° - Magenta claro-Rojo
}};
// MONOCHROME: Fondo negro degradado, sprites blancos monocromáticos (8 tonos grises)
themes_[5] = {
"MONOCHROME",
"MONOCROMO", // Nombres (inglés, español)
200,
200,
200, // Color texto: gris claro
20.0f / 255.0f,
20.0f / 255.0f,
20.0f / 255.0f, // Fondo superior (gris muy oscuro)
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior (negro)
{
{255, 255, 255}, // Blanco puro - todas las pelotas del mismo color
{255, 255, 255},
{255, 255, 255},
{255, 255, 255},
{255, 255, 255},
{255, 255, 255},
{255, 255, 255},
{255, 255, 255}}};
// LAVENDER: Degradado violeta oscuro → azul medianoche, pelotas amarillo dorado monocromático
themes_[6] = {
"LAVENDER",
"LAVANDA", // Nombres (inglés, español)
255,
200,
100, // Color texto: amarillo cálido
120.0f / 255.0f,
80.0f / 255.0f,
140.0f / 255.0f, // Fondo superior (violeta oscuro)
25.0f / 255.0f,
30.0f / 255.0f,
60.0f / 255.0f, // Fondo inferior (azul medianoche)
{
{255, 215, 0}, // Amarillo dorado - todas las pelotas del mismo color
{255, 215, 0},
{255, 215, 0},
{255, 215, 0},
{255, 215, 0},
{255, 215, 0},
{255, 215, 0},
{255, 215, 0}}};
}
void Engine::initializeDynamicThemes() {
// ========================================================================
// DYNAMIC_1: "SUNRISE" (Amanecer) - Noche → Alba → Día → Loop
// ========================================================================
dynamic_themes_[0] = {
"SUNRISE",
"AMANECER",
255,
200,
100, // Color texto: amarillo cálido
{
// Keyframe 0: Noche oscura (estado inicial)
{
20.0f / 255.0f,
25.0f / 255.0f,
60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f,
10.0f / 255.0f,
30.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160}, {95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}}, // Pelotas azules tenues
0.0f // Sin transición (estado inicial)
},
// Keyframe 1: Alba naranja-rosa
{
180.0f / 255.0f,
100.0f / 255.0f,
120.0f / 255.0f, // Fondo superior: naranja-rosa
255.0f / 255.0f,
140.0f / 255.0f,
100.0f / 255.0f, // Fondo inferior: naranja cálido
{{255, 180, 100}, {255, 160, 80}, {255, 200, 120}, {255, 150, 90}, {255, 190, 110}, {255, 170, 95}, {255, 185, 105}, {255, 165, 88}}, // Pelotas naranjas
4.0f // 4 segundos para llegar aquí
},
// Keyframe 2: Día brillante amarillo
{
255.0f / 255.0f,
240.0f / 255.0f,
180.0f / 255.0f, // Fondo superior: amarillo claro
255.0f / 255.0f,
255.0f / 255.0f,
220.0f / 255.0f, // Fondo inferior: amarillo muy claro
{{255, 255, 200}, {255, 255, 180}, {255, 255, 220}, {255, 255, 190}, {255, 255, 210}, {255, 255, 185}, {255, 255, 205}, {255, 255, 195}}, // Pelotas amarillas brillantes
3.0f // 3 segundos para llegar aquí
},
// Keyframe 3: Vuelta a noche (para loop)
{
20.0f / 255.0f,
25.0f / 255.0f,
60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f,
10.0f / 255.0f,
30.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160}, {95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}}, // Pelotas azules tenues
5.0f // 5 segundos para volver a noche
}},
true // Loop = true
};
// ========================================================================
// DYNAMIC_2: "OCEAN WAVES" (Olas Oceánicas) - Azul oscuro ↔ Turquesa
// ========================================================================
dynamic_themes_[1] = {
"OCEAN WAVES",
"OLAS OCEANICAS",
100,
220,
255, // Color texto: cian claro
{
// Keyframe 0: Profundidad oceánica (azul oscuro)
{
20.0f / 255.0f,
50.0f / 255.0f,
100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f,
30.0f / 255.0f,
60.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175}, {65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}}, // Pelotas azul oscuro
0.0f // Estado inicial
},
// Keyframe 1: Aguas poco profundas (turquesa brillante)
{
100.0f / 255.0f,
200.0f / 255.0f,
230.0f / 255.0f, // Fondo superior: turquesa claro
50.0f / 255.0f,
150.0f / 255.0f,
200.0f / 255.0f, // Fondo inferior: turquesa medio
{{100, 220, 255}, {90, 210, 245}, {110, 230, 255}, {95, 215, 250}, {105, 225, 255}, {98, 218, 248}, {102, 222, 252}, {92, 212, 242}}, // Pelotas turquesa brillante
4.0f // 4 segundos para llegar
},
// Keyframe 2: Vuelta a profundidad (para loop)
{
20.0f / 255.0f,
50.0f / 255.0f,
100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f,
30.0f / 255.0f,
60.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175}, {65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}}, // Pelotas azul oscuro
4.0f // 4 segundos para volver
}},
true // Loop = true
};
// ========================================================================
// DYNAMIC_3: "NEON PULSE" (Pulso Neón) - Negro → Neón brillante (rápido)
// ========================================================================
dynamic_themes_[2] = {
"NEON PULSE",
"PULSO NEON",
255,
60,
255, // Color texto: magenta brillante
{
// Keyframe 0: Apagado (negro)
{
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior: negro
{{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48}, {42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}}, // Pelotas grises muy oscuras
0.0f // Estado inicial
},
// Keyframe 1: Encendido (neón cian-magenta)
{
20.0f / 255.0f,
20.0f / 255.0f,
40.0f / 255.0f, // Fondo superior: azul oscuro
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior: negro
{{0, 255, 255}, {255, 0, 255}, {0, 255, 200}, {255, 50, 255}, {50, 255, 255}, {255, 0, 200}, {0, 255, 230}, {255, 80, 255}}, // Pelotas neón vibrante
1.5f // 1.5 segundos para encender (rápido)
},
// Keyframe 2: Vuelta a apagado (para loop ping-pong)
{
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior: negro
{{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48}, {42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}}, // Pelotas grises muy oscuras
1.5f // 1.5 segundos para apagar
}},
true // Loop = true
};
}
void Engine::startThemeTransition(ColorTheme new_theme) {
if (new_theme == current_theme_) return; // Ya estamos en ese tema
// Si venimos de tema dinámico, hacer transición instantánea (sin LERP)
// porque current_theme_ apunta a DYNAMIC_X (índice >= 7) y no existe en themes_[]
if (dynamic_theme_active_) {
dynamic_theme_active_ = false;
current_dynamic_theme_index_ = -1;
// Cambio instantáneo: sin transición LERP
current_theme_ = new_theme;
transitioning_ = false;
// Actualizar colores de pelotas al tema final
const ThemeColors& theme = themes_[static_cast<int>(new_theme)];
for (size_t i = 0; i < balls_.size(); i++) {
size_t color_index = i % theme.ball_colors.size();
balls_[i]->setColor(theme.ball_colors[color_index]);
}
// Mostrar nombre del tema
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme.name_es;
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
return;
}
// Transición normal entre temas estáticos
target_theme_ = new_theme;
transitioning_ = true;
transition_progress_ = 0.0f;
// Mostrar nombre del tema (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
ThemeColors& theme = themes_[static_cast<int>(new_theme)];
text_ = theme.name_es;
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
}
void Engine::updateDynamicTheme() {
if (!dynamic_theme_active_ || current_dynamic_theme_index_ < 0) return;
if (dynamic_theme_paused_) return; // Pausado con Shift+D
DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];
// Obtener keyframe destino para calcular duración
const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];
// Avanzar progreso de transición
dynamic_transition_progress_ += delta_time_ / target_kf.duration;
if (dynamic_transition_progress_ >= 1.0f) {
// Transición completa: avanzar al siguiente keyframe
dynamic_transition_progress_ = 0.0f;
current_keyframe_index_ = target_keyframe_index_;
// Calcular siguiente keyframe destino
target_keyframe_index_++;
if (target_keyframe_index_ >= theme.keyframes.size()) {
if (theme.loop) {
target_keyframe_index_ = 0; // Volver al inicio
} else {
target_keyframe_index_ = theme.keyframes.size() - 1; // Quedarse en el último
dynamic_theme_active_ = false; // Detener animación
}
}
// NOTA: No se actualiza Ball::color_ aquí porque getInterpolatedColor()
// calcula el color directamente desde los keyframes cada frame.
// Cuando progress=1.0, getInterpolatedColor() devuelve exactamente el color destino.
}
}
void Engine::activateDynamicTheme(int index) {
if (index < 0 || index >= 3) return; // Validar índice
// Desactivar transición estática si estaba activa
if (transitioning_) {
transitioning_ = false;
current_theme_ = target_theme_;
}
// Activar tema dinámico
dynamic_theme_active_ = true;
current_dynamic_theme_index_ = index;
current_keyframe_index_ = 0;
target_keyframe_index_ = 1;
dynamic_transition_progress_ = 0.0f;
dynamic_theme_paused_ = false;
// NOTA: No actualizamos current_theme_ porque cuando dynamic_theme_active_=true,
// todo el código usa current_dynamic_theme_index_ en su lugar
// Establecer colores iniciales del keyframe 0
DynamicTheme& theme = dynamic_themes_[index];
const DynamicThemeKeyframe& initial_kf = theme.keyframes[0];
for (size_t i = 0; i < balls_.size(); i++) {
size_t color_index = i % initial_kf.ball_colors.size();
balls_[i]->setColor(initial_kf.ball_colors[color_index]);
}
// Mostrar nombre del tema (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme.name_es;
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
}
void Engine::pauseDynamicTheme() {
if (!dynamic_theme_active_) return; // Solo funciona si hay tema dinámico activo
dynamic_theme_paused_ = !dynamic_theme_paused_;
// Mostrar estado de pausa (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
text_ = dynamic_theme_paused_ ? "PAUSADO" : "REANUDADO";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
}
Color Engine::getInterpolatedColor(size_t ball_index) const {
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
// Tema dinámico activo: interpolar entre keyframes
const DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];
const DynamicThemeKeyframe& current_kf = theme.keyframes[current_keyframe_index_];
const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];
// Obtener colores desde keyframes (NO desde Ball::color_)
size_t color_index = ball_index % current_kf.ball_colors.size();
Color current_color = current_kf.ball_colors[color_index]; // Color del keyframe actual
Color target_color = target_kf.ball_colors[color_index]; // Color del keyframe destino
// Interpolar RGB entre keyframes
return {
static_cast<int>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), dynamic_transition_progress_)),
static_cast<int>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), dynamic_transition_progress_)),
static_cast<int>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), dynamic_transition_progress_))};
} else if (transitioning_) {
// Transición estática: interpolar entre tema actual y tema destino
Color current_color = balls_[ball_index]->getColor();
// Obtener el color destino (mismo índice de color en el tema destino)
const ThemeColors& target_theme = themes_[static_cast<int>(target_theme_)];
size_t color_index = ball_index % target_theme.ball_colors.size();
Color target_color = target_theme.ball_colors[color_index];
// Interpolar RGB
return {
static_cast<int>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), transition_progress_)),
static_cast<int>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), transition_progress_)),
static_cast<int>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), transition_progress_))};
} else {
// Sin transición: devolver color actual
return balls_[ball_index]->getColor();
}
}
// ============================================================================
// Sistema de gestión de estados (MANUAL/DEMO/DEMO_LITE/LOGO)
// ============================================================================
@@ -1998,7 +1510,7 @@ void Engine::performDemoAction(bool is_lite) {
if (is_lite) {
// DEMO LITE: Verificar condiciones para salto a Logo Mode
if (static_cast<int>(balls_.size()) >= LOGO_MODE_MIN_BALLS &&
current_theme_ == ColorTheme::MONOCHROME) {
theme_manager_->getCurrentThemeIndex() == 5) { // MONOCHROME
// 10% probabilidad de saltar a Logo Mode
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) {
enterLogoMode(true); // Entrar desde DEMO
@@ -2115,8 +1627,9 @@ void Engine::performDemoAction(bool is_lite) {
// Cambiar tema (15%)
accumulated_weight += DEMO_WEIGHT_THEME;
if (random_value < accumulated_weight) {
ColorTheme new_theme = static_cast<ColorTheme>(rand() % 6);
startThemeTransition(new_theme);
// Elegir entre TODOS los 10 temas (estáticos + dinámicos)
int random_theme_index = rand() % 10;
theme_manager_->switchToTheme(random_theme_index);
return;
}
@@ -2204,9 +1717,9 @@ void Engine::randomizeOnDemoStart(bool is_lite) {
scenario_ = valid_scenarios[rand() % 5];
initBalls(scenario_);
// 2. Tema
ColorTheme new_theme = static_cast<ColorTheme>(rand() % 6);
startThemeTransition(new_theme);
// 2. Tema (elegir entre TODOS los 10 temas)
int random_theme_index = rand() % 10;
theme_manager_->switchToTheme(random_theme_index);
// 3. Sprite
if (rand() % 2 == 0) {
@@ -2272,7 +1785,7 @@ void Engine::enterLogoMode(bool from_demo) {
}
// Guardar estado previo (para restaurar al salir)
logo_previous_theme_ = current_theme_;
logo_previous_theme_ = theme_manager_->getCurrentThemeIndex();
logo_previous_texture_index_ = current_texture_index_;
logo_previous_shape_scale_ = shape_scale_factor_;
@@ -2300,7 +1813,7 @@ void Engine::enterLogoMode(bool from_demo) {
}
// Cambiar a tema MONOCHROME
startThemeTransition(ColorTheme::MONOCHROME);
theme_manager_->switchToTheme(5); // MONOCHROME
// Establecer escala a 120%
shape_scale_factor_ = LOGO_MODE_SHAPE_SCALE;
@@ -2333,7 +1846,7 @@ void Engine::exitLogoMode(bool return_to_demo) {
if (current_app_mode_ != AppMode::LOGO) return;
// Restaurar estado previo
startThemeTransition(logo_previous_theme_);
theme_manager_->switchToTheme(logo_previous_theme_);
if (logo_previous_texture_index_ != current_texture_index_ &&
logo_previous_texture_index_ < textures_.size()) {