Files
vibe3_physics/source/engine.cpp
Sergio Valor ebeec288ee Fix: Crash al acceder a themes[] con índices de temas dinámicos
PROBLEMA:
Crash (segfault) al activar temas dinámicos OCEAN WAVES (tecla 9) y
NEON PULSE (tecla 0). Funcionamiento correcto con SUNRISE (tecla 8).

CAUSA:
Múltiples lugares del código accedían a themes_[current_theme_] sin
verificar si current_theme_ era un tema dinámico (índices 7/8/9).

El array themes_[] solo tiene 7 elementos (índices 0-6):
- SUNSET, OCEAN, NEON, FOREST, RGB, MONOCHROME, LAVENDER

Los temas dinámicos están en dynamic_themes_[] (índices 0-2):
- DYNAMIC_1=7 (SUNRISE), DYNAMIC_2=8 (OCEAN WAVES), DYNAMIC_3=9 (NEON PULSE)

Acceder a themes_[7/8/9] causaba out-of-bounds → puntero inválido
→ crash en strlen(name_es).

PUNTOS DE FALLO IDENTIFICADOS:
1. render() línea ~738: Mostrar nombre del tema en pantalla
2. render() línea ~808: Debug display "THEME XXX"
3. initBalls() línea ~864: Seleccionar colores para pelotas nuevas

SOLUCIÓN:
 Añadir verificación dynamic_theme_active_ antes de acceder a arrays
 Si tema dinámico: usar dynamic_themes_[current_dynamic_theme_index_]
 Si tema estático: usar themes_[static_cast<int>(current_theme_)]

CORRECCIONES APLICADAS:
- render() (show_text_): Obtener color y nombre desde DynamicTheme
- render() (show_debug_): Obtener name_en desde DynamicTheme
- initBalls(): Seleccionar colores desde keyframe actual de DynamicTheme

RESULTADO:
-  SUNRISE (Numpad 8) funciona correctamente
-  OCEAN WAVES (Numpad 9) funciona correctamente (antes crasheaba)
-  NEON PULSE (Numpad 0) funciona correctamente (antes crasheaba)
-  Temas estáticos (1-7) siguen funcionando normalmente

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:31:31 +02:00

2503 lines
100 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "engine.h"
#include <SDL3/SDL_error.h> // for SDL_GetError
#include <SDL3/SDL_events.h> // for SDL_Event, SDL_PollEvent
#include <SDL3/SDL_init.h> // for SDL_Init, SDL_Quit, SDL_INIT_VIDEO
#include <SDL3/SDL_keycode.h> // for SDL_Keycode
#include <SDL3/SDL_render.h> // for SDL_SetRenderDrawColor, SDL_RenderPresent
#include <SDL3/SDL_timer.h> // for SDL_GetTicks
#include <SDL3/SDL_video.h> // for SDL_CreateWindow, SDL_DestroyWindow, SDL_GetDisplayBounds
#include <algorithm> // for std::min, std::max, std::sort
#include <cmath> // for sqrtf, acosf, cosf, sinf (funciones matemáticas)
#include <cstdlib> // for rand, srand
#include <cstring> // for strlen
#include <ctime> // for time
#include <filesystem> // for path operations
#include <iostream> // for cout
#include <string> // for string
#ifdef _WIN32
#include <windows.h> // for GetModuleFileName
#endif
#include "ball.h" // for Ball
#include "external/dbgtxt.h" // for dbg_init, dbg_print
#include "external/mouse.h" // for Mouse namespace
#include "external/texture.h" // for Texture
#include "shapes/atom_shape.h" // for AtomShape
#include "shapes/cube_shape.h" // for CubeShape
#include "shapes/cylinder_shape.h" // for CylinderShape
#include "shapes/helix_shape.h" // for HelixShape
#include "shapes/icosahedron_shape.h" // for IcosahedronShape
#include "shapes/png_shape.h" // for PNGShape
#include "shapes/sphere_shape.h" // for SphereShape
#include "shapes/torus_shape.h" // for TorusShape
#include "shapes/wave_grid_shape.h" // for WaveGridShape
// getExecutableDirectory() ya está definido en defines.h como inline
// Implementación de métodos públicos
bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
bool success = true;
// Obtener resolución de pantalla para validación
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl;
return false;
}
int num_displays = 0;
SDL_DisplayID *displays = SDL_GetDisplays(&num_displays);
const auto *dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr;
int screen_w = dm ? dm->w : 1920; // Fallback si falla
int screen_h = dm ? dm->h - WINDOW_DECORATION_HEIGHT : 1080;
if (displays) SDL_free(displays);
// Usar parámetros o valores por defecto
int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH;
int logical_height = (height > 0) ? height : DEFAULT_SCREEN_HEIGHT;
int window_zoom = (zoom > 0) ? zoom : DEFAULT_WINDOW_ZOOM;
// VALIDACIÓN 1: Si resolución > pantalla → reset a default
if (logical_width > screen_w || logical_height > screen_h) {
std::cout << "Advertencia: Resolución " << logical_width << "x" << logical_height
<< " excede pantalla " << screen_w << "x" << screen_h
<< ". Usando default " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << "\n";
logical_width = DEFAULT_SCREEN_WIDTH;
logical_height = DEFAULT_SCREEN_HEIGHT;
window_zoom = DEFAULT_WINDOW_ZOOM; // Reset zoom también
}
// VALIDACIÓN 2: Calcular max_zoom y ajustar si es necesario
int max_zoom = std::min(screen_w / logical_width, screen_h / logical_height);
if (window_zoom > max_zoom) {
std::cout << "Advertencia: Zoom " << window_zoom << " excede máximo " << max_zoom
<< " para " << logical_width << "x" << logical_height << ". Ajustando a " << max_zoom << "\n";
window_zoom = max_zoom;
}
// Si se especificaron parámetros CLI y zoom no se especificó, usar zoom=1
if ((width > 0 || height > 0) && zoom == 0) {
window_zoom = 1;
}
// Calcular tamaño de ventana
int window_width = logical_width * window_zoom;
int window_height = logical_height * window_zoom;
// Guardar resolución base (configurada por CLI o default)
base_screen_width_ = logical_width;
base_screen_height_ = logical_height;
current_screen_width_ = logical_width;
current_screen_height_ = logical_height;
// SDL ya inicializado arriba para validación
{
// Crear ventana principal (fullscreen si se especifica)
Uint32 window_flags = SDL_WINDOW_OPENGL;
if (fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN;
}
window_ = SDL_CreateWindow(WINDOW_CAPTION, window_width, window_height, window_flags);
if (window_ == nullptr) {
std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << std::endl;
success = false;
} else {
// Crear renderizador
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
std::cout << "¡No se pudo crear el renderizador! Error de SDL: " << SDL_GetError() << std::endl;
success = false;
} else {
// Establecer color inicial del renderizador
SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);
// Establecer tamaño lógico para el renderizado (resolución interna)
SDL_SetRenderLogicalPresentation(renderer_, logical_width, logical_height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Configurar V-Sync inicial
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
}
}
}
// Inicializar otros componentes si SDL se inicializó correctamente
if (success) {
// Cargar todas las texturas disponibles desde data/balls/
std::string resources_dir = getResourcesDirectory();
std::string balls_dir = resources_dir + "/data/balls";
struct TextureInfo {
std::string name;
std::shared_ptr<Texture> texture;
int width;
};
std::vector<TextureInfo> texture_files;
// Buscar todas las texturas PNG en data/balls/
namespace fs = std::filesystem;
if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
// Cargar todas las texturas desde disco
for (const auto& entry : fs::directory_iterator(balls_dir)) {
if (entry.is_regular_file() && entry.path().extension() == ".png") {
std::string filename = entry.path().stem().string();
std::string fullpath = entry.path().string();
// Cargar textura y obtener dimensiones
auto texture = std::make_shared<Texture>(renderer_, fullpath);
int width = texture->getWidth();
texture_files.push_back({filename, texture, width});
}
}
} else {
// Fallback: cargar texturas desde pack usando la lista del ResourcePack
if (Texture::isPackLoaded()) {
auto pack_resources = Texture::getPackResourceList();
// Filtrar solo los recursos en balls/ con extensión .png
for (const auto& resource : pack_resources) {
if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") {
std::string tex_name = resource.substr(6); // Quitar "balls/"
std::string name = tex_name.substr(0, tex_name.find('.')); // Quitar extensión
auto texture = std::make_shared<Texture>(renderer_, resource);
int width = texture->getWidth();
texture_files.push_back({name, texture, width});
}
}
}
}
// Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4)
std::sort(texture_files.begin(), texture_files.end(),
[](const TextureInfo& a, const TextureInfo& b) {
return a.width > b.width; // Descendente por tamaño
});
// Guardar texturas ya cargadas en orden (0=big, 1=normal, 2=small, 3=tiny)
for (const auto& info : texture_files) {
textures_.push_back(info.texture);
texture_names_.push_back(info.name);
}
// Verificar que se cargaron texturas
if (textures_.empty()) {
std::cerr << "ERROR: No se pudieron cargar texturas" << std::endl;
success = false;
}
// Buscar índice de "normal" para usarlo como textura inicial (debería ser índice 1)
current_texture_index_ = 0; // Fallback
for (size_t i = 0; i < texture_names_.size(); i++) {
if (texture_names_[i] == "normal") {
current_texture_index_ = i; // Iniciar en "normal" (índice 1)
break;
}
}
texture_ = textures_[current_texture_index_];
current_ball_size_ = texture_->getWidth(); // Obtener tamaño dinámicamente
srand(static_cast<unsigned>(time(nullptr)));
dbg_init(renderer_);
initializeThemes();
initializeDynamicThemes();
initBalls(scenario_);
}
return success;
}
void Engine::run() {
while (!should_exit_) {
calculateDeltaTime();
update();
handleEvents();
render();
}
}
void Engine::shutdown() {
// Limpiar recursos SDL
if (renderer_) {
SDL_DestroyRenderer(renderer_);
renderer_ = nullptr;
}
if (window_) {
SDL_DestroyWindow(window_);
window_ = nullptr;
}
SDL_Quit();
}
// Métodos privados - esqueleto básico por ahora
void Engine::calculateDeltaTime() {
Uint64 current_time = SDL_GetTicks();
// En el primer frame, inicializar el tiempo anterior
if (last_frame_time_ == 0) {
last_frame_time_ = current_time;
delta_time_ = 1.0f / 60.0f; // Asumir 60 FPS para el primer frame
return;
}
// Calcular delta time en segundos
delta_time_ = (current_time - last_frame_time_) / 1000.0f;
last_frame_time_ = current_time;
// Limitar delta time para evitar saltos grandes (pausa larga, depuración, etc.)
if (delta_time_ > 0.05f) { // Máximo 50ms (20 FPS mínimo)
delta_time_ = 1.0f / 60.0f; // Fallback a 60 FPS
}
}
void Engine::update() {
// Actualizar visibilidad del cursor (auto-ocultar tras inactividad)
Mouse::updateCursorVisibility();
// Calcular FPS
fps_frame_count_++;
Uint64 current_time = SDL_GetTicks();
if (current_time - fps_last_time_ >= 1000) // Actualizar cada segundo
{
fps_current_ = fps_frame_count_;
fps_frame_count_ = 0;
fps_last_time_ = current_time;
fps_text_ = "FPS: " + std::to_string(fps_current_);
}
// Bifurcar actualización según modo activo
if (current_mode_ == SimulationMode::PHYSICS) {
// Modo física normal: actualizar física de cada pelota
for (auto& ball : balls_) {
ball->update(delta_time_); // Pasar delta time a cada pelota
}
} else if (current_mode_ == SimulationMode::SHAPE) {
// Modo Figura 3D: actualizar figura polimórfica
updateShape();
}
// Actualizar texto (sin cambios en la lógica)
if (show_text_) {
show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION);
}
// 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();
}
void Engine::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
// Procesar eventos de ratón (auto-ocultar cursor)
Mouse::handleEvent(event);
// Salir del bucle si se detecta una petición de cierre
if (event.type == SDL_EVENT_QUIT) {
should_exit_ = true;
break;
}
// Procesar eventos de teclado
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) {
switch (event.key.key) {
case SDLK_ESCAPE:
should_exit_ = true;
break;
case SDLK_SPACE:
pushBallsAwayFromGravity();
break;
case SDLK_G:
// Si estamos en modo figura, salir a modo física SIN GRAVEDAD
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(false); // Desactivar figura sin forzar gravedad ON
} else {
switchBallsGravity(); // Toggle normal en modo física
}
break;
// Controles de dirección de gravedad con teclas de cursor
case SDLK_UP:
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente)
} else {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
changeGravityDirection(GravityDirection::UP);
break;
case SDLK_DOWN:
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente)
} else {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
changeGravityDirection(GravityDirection::DOWN);
break;
case SDLK_LEFT:
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente)
} else {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
changeGravityDirection(GravityDirection::LEFT);
break;
case SDLK_RIGHT:
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente)
} else {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
changeGravityDirection(GravityDirection::RIGHT);
break;
case SDLK_V:
toggleVSync();
break;
case SDLK_H:
show_debug_ = !show_debug_;
break;
// Toggle Física ↔ Última Figura (antes era C)
case SDLK_F:
toggleShapeMode();
break;
// Selección directa de figuras 3D
case SDLK_Q:
activateShape(ShapeType::SPHERE);
break;
case SDLK_W:
activateShape(ShapeType::WAVE_GRID);
break;
case SDLK_E:
activateShape(ShapeType::HELIX);
break;
case SDLK_R:
activateShape(ShapeType::TORUS);
break;
case SDLK_T:
activateShape(ShapeType::CUBE);
break;
case SDLK_Y:
activateShape(ShapeType::CYLINDER);
break;
case SDLK_U:
activateShape(ShapeType::ICOSAHEDRON);
break;
case SDLK_I:
activateShape(ShapeType::ATOM);
break;
case SDLK_O:
activateShape(ShapeType::PNG_SHAPE);
break;
// 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);
}
break;
// Temas de colores con teclado numérico (con transición suave)
case SDLK_KP_1:
startThemeTransition(ColorTheme::SUNSET);
break;
case SDLK_KP_2:
startThemeTransition(ColorTheme::OCEAN);
break;
case SDLK_KP_3:
startThemeTransition(ColorTheme::NEON);
break;
case SDLK_KP_4:
startThemeTransition(ColorTheme::FOREST);
break;
case SDLK_KP_5:
startThemeTransition(ColorTheme::RGB);
break;
case SDLK_KP_6:
startThemeTransition(ColorTheme::MONOCHROME);
break;
case SDLK_KP_7:
startThemeTransition(ColorTheme::LAVENDER);
break;
// Temas dinámicos (animados) - Solo Numpad 8/9/0 (teclas normales usadas para escenarios)
case SDLK_KP_8:
activateDynamicTheme(0); // SUNRISE
break;
case SDLK_KP_9:
activateDynamicTheme(1); // OCEAN WAVES
break;
case SDLK_KP_0:
activateDynamicTheme(2); // NEON PULSE
break;
// Cambio de sprite/textura dinámico
case SDLK_N:
switchTexture();
break;
// Control de escala de figura (solo en modo SHAPE)
case SDLK_KP_PLUS:
if (current_mode_ == SimulationMode::SHAPE) {
shape_scale_factor_ += SHAPE_SCALE_STEP;
clampShapeScale();
text_ = "SCALE " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
}
break;
case SDLK_KP_MINUS:
if (current_mode_ == SimulationMode::SHAPE) {
shape_scale_factor_ -= SHAPE_SCALE_STEP;
clampShapeScale();
text_ = "SCALE " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
}
break;
case SDLK_KP_MULTIPLY:
if (current_mode_ == SimulationMode::SHAPE) {
shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
text_ = "SCALE RESET (100%)";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
}
break;
case SDLK_KP_DIVIDE:
if (current_mode_ == SimulationMode::SHAPE) {
depth_zoom_enabled_ = !depth_zoom_enabled_;
text_ = depth_zoom_enabled_ ? "DEPTH ZOOM ON" : "DEPTH ZOOM OFF";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
}
break;
case SDLK_1:
scenario_ = 0;
initBalls(scenario_);
break;
case SDLK_2:
scenario_ = 1;
initBalls(scenario_);
break;
case SDLK_3:
scenario_ = 2;
initBalls(scenario_);
break;
case SDLK_4:
scenario_ = 3;
initBalls(scenario_);
break;
case SDLK_5:
scenario_ = 4;
initBalls(scenario_);
break;
case SDLK_6:
scenario_ = 5;
initBalls(scenario_);
break;
case SDLK_7:
scenario_ = 6;
initBalls(scenario_);
break;
case SDLK_8:
scenario_ = 7;
initBalls(scenario_);
break;
// Controles de zoom dinámico (solo si no estamos en fullscreen)
case SDLK_F1:
if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
zoomOut();
}
break;
case SDLK_F2:
if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
zoomIn();
}
break;
// Control de pantalla completa
case SDLK_F3:
toggleFullscreen();
break;
// Modo real fullscreen (cambia resolución interna)
case SDLK_F4:
toggleRealFullscreen();
break;
// Toggle escalado entero/estirado (solo en fullscreen F3)
case SDLK_F5:
toggleIntegerScaling();
break;
// Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D)
case SDLK_D:
// Shift+D = Pausar tema dinámico
if (event.key.mod & SDL_KMOD_SHIFT) {
pauseDynamicTheme();
} else {
// D sin Shift = Toggle modo DEMO
if (current_app_mode_ == AppMode::DEMO) {
// Desactivar DEMO → MANUAL
setState(AppMode::MANUAL);
text_ = "DEMO MODE OFF";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
} else {
// Activar DEMO (desde cualquier otro modo)
setState(AppMode::DEMO);
randomizeOnDemoStart(false);
text_ = "DEMO MODE ON";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
}
break;
// Toggle Modo DEMO LITE (solo física/figuras)
case SDLK_L:
if (current_app_mode_ == AppMode::DEMO_LITE) {
// Desactivar DEMO_LITE → MANUAL
setState(AppMode::MANUAL);
text_ = "DEMO LITE OFF";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
} else {
// Activar DEMO_LITE (desde cualquier otro modo)
setState(AppMode::DEMO_LITE);
randomizeOnDemoStart(true);
text_ = "DEMO LITE ON";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
break;
// Toggle Modo LOGO (easter egg - marca de agua)
case SDLK_K:
toggleLogoMode();
// Mostrar texto informativo
if (current_app_mode_ == AppMode::LOGO) {
text_ = "LOGO MODE ON";
} else {
text_ = "LOGO MODE OFF";
}
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
break;
}
}
}
}
void Engine::render() {
// Limpiar framebuffer completamente (evita artefactos en barras negras al cambiar modos)
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();
// Limpiar batches del frame anterior
batch_vertices_.clear();
batch_indices_.clear();
if (current_mode_ == SimulationMode::SHAPE) {
// MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm)
// Las pelotas con menor depth_brightness (más lejos/oscuras) se renderizan primero
// Crear vector de índices para ordenamiento
std::vector<size_t> render_order;
render_order.reserve(balls_.size());
for (size_t i = 0; i < balls_.size(); i++) {
render_order.push_back(i);
}
// Ordenar índices por profundidad Z (menor primero = fondo primero)
std::sort(render_order.begin(), render_order.end(), [this](size_t a, size_t b) {
return balls_[a]->getDepthBrightness() < balls_[b]->getDepthBrightness();
});
// 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)
float brightness = balls_[idx]->getDepthBrightness();
float depth_scale = balls_[idx]->getDepthScale();
// Mapear brightness de 0-1 a rango MIN-MAX
float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
// Aplicar factor de brillo al color
int r_mod = static_cast<int>(color.r * brightness_factor);
int g_mod = static_cast<int>(color.g * brightness_factor);
int b_mod = static_cast<int>(color.b * brightness_factor);
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, r_mod, g_mod, b_mod, depth_scale);
}
} else {
// MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad)
size_t idx = 0;
for (auto& ball : balls_) {
SDL_FRect pos = ball->getPosition();
Color color = getInterpolatedColor(idx); // Usar color interpolado (LERP)
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f);
idx++;
}
}
// Renderizar todas las bolas en una sola llamada
if (!batch_vertices_.empty()) {
SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast<int>(batch_vertices_.size()), batch_indices_.data(), static_cast<int>(batch_indices_.size()));
}
if (show_text_) {
// Obtener datos del tema actual (estático o dinámico)
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;
}
// 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);
// Mostrar nombre del tema en castellano debajo del número de pelotas
// (solo si text_ NO es ya el nombre del tema, para evitar duplicación)
if (theme_name_es != nullptr && text_ != theme_name_es) {
int theme_text_width = static_cast<int>(strlen(theme_name_es) * 8); // 8 píxeles por carácter
int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente
// Texto del nombre del tema con el mismo color
dbg_print(theme_x, 24, theme_name_es, text_color_r, text_color_g, text_color_b);
}
}
// Debug display (solo si está activado con tecla H)
if (show_debug_) {
// Mostrar contador de FPS en esquina superior derecha
int fps_text_width = static_cast<int>(fps_text_.length() * 8); // 8 píxeles por carácter
int fps_x = current_screen_width_ - fps_text_width - 8; // 8 píxeles de margen
dbg_print(fps_x, 8, fps_text_.c_str(), 255, 255, 0); // Amarillo para distinguir
// Mostrar estado V-Sync en esquina superior izquierda
dbg_print(8, 8, vsync_text_.c_str(), 0, 255, 255); // Cian para distinguir
// Debug: Mostrar valores de la primera pelota (si existe)
if (!balls_.empty()) {
// Línea 1: Gravedad (solo números enteros)
int grav_int = static_cast<int>(balls_[0]->getGravityForce());
std::string grav_text = "GRAV " + std::to_string(grav_int);
dbg_print(8, 24, grav_text.c_str(), 255, 0, 255); // Magenta para debug
// Línea 2: Velocidad Y (solo números enteros)
int vy_int = static_cast<int>(balls_[0]->getVelocityY());
std::string vy_text = "VY " + std::to_string(vy_int);
dbg_print(8, 32, vy_text.c_str(), 255, 0, 255); // Magenta para debug
// Línea 3: Estado superficie
std::string surface_text = balls_[0]->isOnSurface() ? "SURFACE YES" : "SURFACE NO";
dbg_print(8, 40, surface_text.c_str(), 255, 0, 255); // Magenta para debug
// Línea 4: Coeficiente de rebote (loss)
float loss_val = balls_[0]->getLossCoefficient();
std::string loss_text = "LOSS " + std::to_string(loss_val).substr(0, 4); // Solo 2 decimales
dbg_print(8, 48, loss_text.c_str(), 255, 0, 255); // Magenta para debug
// Línea 5: Dirección de gravedad
std::string gravity_dir_text = "GRAVITY " + gravityDirectionToString(current_gravity_);
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;
}
dbg_print(8, 64, theme_text.c_str(), 255, 255, 128); // Amarillo claro para tema
// Debug: Mostrar modo de simulación actual
std::string mode_text;
if (current_mode_ == SimulationMode::PHYSICS) {
mode_text = "MODE PHYSICS";
} else if (active_shape_) {
mode_text = std::string("MODE ") + active_shape_->getName();
} else {
mode_text = "MODE SHAPE";
}
dbg_print(8, 72, mode_text.c_str(), 0, 255, 128); // Verde claro para modo
// Debug: Mostrar modo DEMO/LOGO activo (siempre visible cuando debug está ON)
if (current_app_mode_ == AppMode::LOGO) {
int logo_text_width = 9 * 8; // "LOGO MODE" = 9 caracteres × 8 píxeles
int logo_x = (current_screen_width_ - logo_text_width) / 2;
dbg_print(logo_x, 80, "LOGO MODE", 255, 128, 0); // Naranja para Logo Mode
} else if (current_app_mode_ == AppMode::DEMO) {
int demo_text_width = 9 * 8; // "DEMO MODE" = 9 caracteres × 8 píxeles
int demo_x = (current_screen_width_ - demo_text_width) / 2;
dbg_print(demo_x, 80, "DEMO MODE", 255, 165, 0); // Naranja para DEMO
} else if (current_app_mode_ == AppMode::DEMO_LITE) {
int lite_text_width = 14 * 8; // "DEMO LITE MODE" = 14 caracteres × 8 píxeles
int lite_x = (current_screen_width_ - lite_text_width) / 2;
dbg_print(lite_x, 80, "DEMO LITE MODE", 255, 200, 0); // Amarillo-naranja para DEMO LITE
}
}
SDL_RenderPresent(renderer_);
}
void Engine::initBalls(int value) {
// Si estamos en modo figura 3D, desactivarlo antes de regenerar pelotas
if (current_mode_ == SimulationMode::SHAPE) {
current_mode_ = SimulationMode::PHYSICS;
active_shape_.reset(); // Liberar figura actual
}
// Limpiar las bolas actuales
balls_.clear();
// Resetear gravedad al estado por defecto (DOWN) al cambiar escenario
changeGravityDirection(GravityDirection::DOWN);
// Crear las bolas según el escenario
for (int i = 0; i < BALL_COUNT_SCENARIOS[value]; ++i) {
const int SIGN = ((rand() % 2) * 2) - 1; // Genera un signo aleatorio (+ o -)
// Calcular spawn zone: margen a cada lado, zona central para spawn
const int margin = static_cast<int>(current_screen_width_ * BALL_SPAWN_MARGIN);
const int spawn_zone_width = current_screen_width_ - (2 * margin);
const float X = (rand() % spawn_zone_width) + margin; // Posición inicial en X
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];
}
// 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));
}
setText(); // Actualiza el texto
}
void Engine::setText() {
// Suprimir textos durante modos demo
if (current_app_mode_ != AppMode::MANUAL) return;
int num_balls = BALL_COUNT_SCENARIOS[scenario_];
if (num_balls == 1) {
text_ = "1 PELOTA";
} else {
text_ = std::to_string(num_balls) + " PELOTAS";
}
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2; // Centrar texto
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
void Engine::pushBallsAwayFromGravity() {
for (auto& ball : balls_) {
const int SIGNO = ((rand() % 2) * 2) - 1;
const float LATERAL = (((rand() % 20) + 10) * 0.1f) * SIGNO;
const float MAIN = ((rand() % 40) * 0.1f) + 5;
float vx = 0, vy = 0;
switch (current_gravity_) {
case GravityDirection::DOWN: // Impulsar ARRIBA
vx = LATERAL;
vy = -MAIN;
break;
case GravityDirection::UP: // Impulsar ABAJO
vx = LATERAL;
vy = MAIN;
break;
case GravityDirection::LEFT: // Impulsar DERECHA
vx = MAIN;
vy = LATERAL;
break;
case GravityDirection::RIGHT: // Impulsar IZQUIERDA
vx = -MAIN;
vy = LATERAL;
break;
}
ball->modVel(vx, vy); // Modifica la velocidad según dirección de gravedad
}
}
void Engine::switchBallsGravity() {
for (auto& ball : balls_) {
ball->switchGravity();
}
}
void Engine::enableBallsGravityIfDisabled() {
for (auto& ball : balls_) {
ball->enableGravityIfDisabled();
}
}
void Engine::forceBallsGravityOn() {
for (auto& ball : balls_) {
ball->forceGravityOn();
}
}
void Engine::forceBallsGravityOff() {
// Contar cuántas pelotas están en superficie (suelo/techo/pared)
int balls_on_surface = 0;
for (const auto& ball : balls_) {
if (ball->isOnSurface()) {
balls_on_surface++;
}
}
// Si la mayoría (>50%) están en superficie, aplicar impulso para que se vea el efecto
float surface_ratio = static_cast<float>(balls_on_surface) / static_cast<float>(balls_.size());
if (surface_ratio > 0.5f) {
pushBallsAwayFromGravity(); // Dar impulso contrario a gravedad
}
// Desactivar gravedad
for (auto& ball : balls_) {
ball->forceGravityOff();
}
}
void Engine::changeGravityDirection(GravityDirection direction) {
current_gravity_ = direction;
for (auto& ball : balls_) {
ball->setGravityDirection(direction);
ball->applyRandomLateralPush(); // Aplicar empuje lateral aleatorio
}
}
void Engine::toggleVSync() {
vsync_enabled_ = !vsync_enabled_;
vsync_text_ = vsync_enabled_ ? "VSYNC ON" : "VSYNC OFF";
// Aplicar el cambio de V-Sync al renderizador
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
}
void Engine::toggleFullscreen() {
// Si está en modo real fullscreen, primero salir de él
if (real_fullscreen_enabled_) {
toggleRealFullscreen(); // Esto lo desactiva
}
fullscreen_enabled_ = !fullscreen_enabled_;
SDL_SetWindowFullscreen(window_, fullscreen_enabled_);
}
void Engine::toggleRealFullscreen() {
// Si está en modo fullscreen normal, primero desactivarlo
if (fullscreen_enabled_) {
fullscreen_enabled_ = false;
SDL_SetWindowFullscreen(window_, false);
}
real_fullscreen_enabled_ = !real_fullscreen_enabled_;
if (real_fullscreen_enabled_) {
// Obtener resolución del escritorio
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays != nullptr && num_displays > 0) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm != nullptr) {
// Cambiar a resolución nativa del escritorio
current_screen_width_ = dm->w;
current_screen_height_ = dm->h;
// Recrear ventana con nueva resolución
SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_);
SDL_SetWindowFullscreen(window_, true);
// Actualizar presentación lógica del renderizador
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Reinicar la escena con nueva resolución
initBalls(scenario_);
}
SDL_free(displays);
}
} else {
// Volver a resolución base (configurada por CLI o default)
current_screen_width_ = base_screen_width_;
current_screen_height_ = base_screen_height_;
// Restaurar ventana normal
SDL_SetWindowFullscreen(window_, false);
SDL_SetWindowSize(window_, base_screen_width_ * DEFAULT_WINDOW_ZOOM, base_screen_height_ * DEFAULT_WINDOW_ZOOM);
// Restaurar presentación lógica base
SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Reinicar la escena con resolución original
initBalls(scenario_);
}
}
void Engine::toggleIntegerScaling() {
// Solo permitir cambio si estamos en modo fullscreen normal (F3)
if (!fullscreen_enabled_) {
return; // No hacer nada si no estamos en fullscreen
}
// Ciclar entre los 3 modos: INTEGER → LETTERBOX → STRETCH → INTEGER
switch (current_scaling_mode_) {
case ScalingMode::INTEGER:
current_scaling_mode_ = ScalingMode::LETTERBOX;
break;
case ScalingMode::LETTERBOX:
current_scaling_mode_ = ScalingMode::STRETCH;
break;
case ScalingMode::STRETCH:
current_scaling_mode_ = ScalingMode::INTEGER;
break;
}
// Aplicar el nuevo modo de escalado
SDL_RendererLogicalPresentation presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
const char* mode_name = "INTEGER";
switch (current_scaling_mode_) {
case ScalingMode::INTEGER:
presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
mode_name = "INTEGER";
break;
case ScalingMode::LETTERBOX:
presentation = SDL_LOGICAL_PRESENTATION_LETTERBOX;
mode_name = "LETTERBOX";
break;
case ScalingMode::STRETCH:
presentation = SDL_LOGICAL_PRESENTATION_STRETCH;
mode_name = "STRETCH";
break;
}
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, presentation);
// Mostrar texto informativo
text_ = "SCALING: ";
text_ += mode_name;
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
std::string Engine::gravityDirectionToString(GravityDirection direction) const {
switch (direction) {
case GravityDirection::DOWN:
return "DOWN";
case GravityDirection::UP:
return "UP";
case GravityDirection::LEFT:
return "LEFT";
case GravityDirection::RIGHT:
return "RIGHT";
default:
return "UNKNOWN";
}
}
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());
// Crear 4 vértices para el quad (2 triángulos)
SDL_Vertex vertices[4];
// Convertir colores de int (0-255) a float (0.0-1.0)
float rf = r / 255.0f;
float gf = g / 255.0f;
float bf = b / 255.0f;
// Aplicar escala al tamaño (centrado en el punto x, y)
float scaled_w = w * scale;
float scaled_h = h * scale;
float offset_x = (w - scaled_w) / 2.0f; // Offset para centrar
float offset_y = (h - scaled_h) / 2.0f;
// Vértice superior izquierdo
vertices[0].position = {x + offset_x, y + offset_y};
vertices[0].tex_coord = {0.0f, 0.0f};
vertices[0].color = {rf, gf, bf, 1.0f};
// Vértice superior derecho
vertices[1].position = {x + offset_x + scaled_w, y + offset_y};
vertices[1].tex_coord = {1.0f, 0.0f};
vertices[1].color = {rf, gf, bf, 1.0f};
// Vértice inferior derecho
vertices[2].position = {x + offset_x + scaled_w, y + offset_y + scaled_h};
vertices[2].tex_coord = {1.0f, 1.0f};
vertices[2].color = {rf, gf, bf, 1.0f};
// Vértice inferior izquierdo
vertices[3].position = {x + offset_x, y + offset_y + scaled_h};
vertices[3].tex_coord = {0.0f, 1.0f};
vertices[3].color = {rf, gf, bf, 1.0f};
// Añadir vértices al batch
for (int i = 0; i < 4; i++) {
batch_vertices_.push_back(vertices[i]);
}
// Añadir índices para 2 triángulos
batch_indices_.push_back(vertex_index + 0);
batch_indices_.push_back(vertex_index + 1);
batch_indices_.push_back(vertex_index + 2);
batch_indices_.push_back(vertex_index + 2);
batch_indices_.push_back(vertex_index + 3);
batch_indices_.push_back(vertex_index + 0);
}
// Sistema de zoom dinámico
int Engine::calculateMaxWindowZoom() const {
// Obtener información del display usando el método de Coffee Crisis
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays == nullptr || num_displays == 0) {
return WINDOW_ZOOM_MIN; // Fallback si no se puede obtener
}
// Obtener el modo de display actual
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm == nullptr) {
SDL_free(displays);
return WINDOW_ZOOM_MIN;
}
// Calcular zoom máximo usando la fórmula de Coffee Crisis
const int MAX_ZOOM = std::min(dm->w / base_screen_width_, (dm->h - WINDOW_DECORATION_HEIGHT) / base_screen_height_);
SDL_free(displays);
// Aplicar límites
return std::max(WINDOW_ZOOM_MIN, std::min(MAX_ZOOM, WINDOW_ZOOM_MAX));
}
void Engine::setWindowZoom(int new_zoom) {
// Validar zoom
int max_zoom = calculateMaxWindowZoom();
new_zoom = std::max(WINDOW_ZOOM_MIN, std::min(new_zoom, max_zoom));
if (new_zoom == current_window_zoom_) {
return; // No hay cambio
}
// Obtener posición actual del centro de la ventana
int current_x, current_y;
SDL_GetWindowPosition(window_, &current_x, &current_y);
int current_center_x = current_x + (base_screen_width_ * current_window_zoom_) / 2;
int current_center_y = current_y + (base_screen_height_ * current_window_zoom_) / 2;
// Calcular nuevo tamaño
int new_width = base_screen_width_ * new_zoom;
int new_height = base_screen_height_ * new_zoom;
// Calcular nueva posición (centrada en el punto actual)
int new_x = current_center_x - new_width / 2;
int new_y = current_center_y - new_height / 2;
// Obtener límites del escritorio para no salirse
SDL_Rect display_bounds;
if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &display_bounds) == 0) {
// Aplicar márgenes
int min_x = WINDOW_DESKTOP_MARGIN;
int min_y = WINDOW_DESKTOP_MARGIN;
int max_x = display_bounds.w - new_width - WINDOW_DESKTOP_MARGIN;
int max_y = display_bounds.h - new_height - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT;
// Limitar posición
new_x = std::max(min_x, std::min(new_x, max_x));
new_y = std::max(min_y, std::min(new_y, max_y));
}
// Aplicar cambios
SDL_SetWindowSize(window_, new_width, new_height);
SDL_SetWindowPosition(window_, new_x, new_y);
current_window_zoom_ = new_zoom;
}
void Engine::zoomIn() {
setWindowZoom(current_window_zoom_ + 1);
}
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
// Desactivar tema dinámico si estaba activo
if (dynamic_theme_active_) {
dynamic_theme_active_ = false;
current_dynamic_theme_index_ = -1;
}
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;
// Actualizar current_theme_ al enum correspondiente
current_theme_ = static_cast<ColorTheme>(static_cast<int>(ColorTheme::DYNAMIC_1) + index);
// 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)
// ============================================================================
void Engine::setState(AppMode new_mode) {
// Si ya estamos en ese modo, no hacer nada
if (current_app_mode_ == new_mode) return;
// Al salir de LOGO, guardar en previous_app_mode_ (para volver al modo correcto)
if (current_app_mode_ == AppMode::LOGO && new_mode != AppMode::LOGO) {
previous_app_mode_ = new_mode;
}
// Al entrar a LOGO, guardar el modo previo
if (new_mode == AppMode::LOGO) {
previous_app_mode_ = current_app_mode_;
}
// Aplicar el nuevo modo
current_app_mode_ = new_mode;
// Configurar timer de demo según el modo
if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) {
demo_timer_ = 0.0f;
float min_interval, max_interval;
if (new_mode == AppMode::LOGO) {
// Escalar tiempos con resolución (720p como base)
float resolution_scale = current_screen_height_ / 720.0f;
logo_min_time_ = LOGO_ACTION_INTERVAL_MIN * resolution_scale;
logo_max_time_ = LOGO_ACTION_INTERVAL_MAX * resolution_scale;
min_interval = logo_min_time_;
max_interval = logo_max_time_;
} else {
bool is_lite = (new_mode == AppMode::DEMO_LITE);
min_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
max_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
}
demo_next_action_time_ = min_interval + (rand() % 1000) / 1000.0f * (max_interval - min_interval);
}
}
// ============================================================================
// Sistema de Modo DEMO (auto-play)
// ============================================================================
void Engine::updateDemoMode() {
// Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO)
if (current_app_mode_ == AppMode::MANUAL) return;
// Actualizar timer
demo_timer_ += delta_time_;
// Determinar si es hora de ejecutar acción (depende del modo)
bool should_trigger = false;
if (current_app_mode_ == AppMode::LOGO) {
// LOGO MODE: Esperar convergencia + tiempo mínimo (o timeout máximo)
bool min_time_reached = demo_timer_ >= logo_min_time_;
bool max_time_reached = demo_timer_ >= logo_max_time_;
bool convergence_ok = shape_convergence_ >= logo_convergence_threshold_;
should_trigger = (min_time_reached && convergence_ok) || max_time_reached;
} else {
// DEMO/DEMO_LITE: Timer simple como antes
should_trigger = demo_timer_ >= demo_next_action_time_;
}
// Si es hora de ejecutar acción
if (should_trigger) {
// MODO LOGO: Sistema de acciones variadas con gravedad dinámica
if (current_app_mode_ == AppMode::LOGO) {
// Elegir acción aleatoria ponderada
int action = rand() % 100;
if (current_mode_ == SimulationMode::SHAPE) {
// Logo quieto (formado) → 2 opciones posibles
if (action < 50) {
// 50%: SHAPE → PHYSICS con gravedad ON (caída dramática)
toggleShapeMode(true);
} else {
// 50%: SHAPE → PHYSICS con gravedad OFF (dar vueltas sin caer)
toggleShapeMode(false);
}
} else {
// Logo animado (PHYSICS) → 3 opciones posibles
if (action < 60) {
// 60%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones)
toggleShapeMode(false);
} else if (action < 80) {
// 20%: Forzar gravedad ON (empezar a caer mientras da vueltas)
forceBallsGravityOn();
} else {
// 20%: Forzar gravedad OFF (flotar mientras da vueltas)
forceBallsGravityOff();
}
}
// Resetear timer con intervalos escalados (logo_min_time_ y logo_max_time_)
demo_timer_ = 0.0f;
float interval_range = logo_max_time_ - logo_min_time_;
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
// Solo salir automáticamente si NO llegamos desde MANUAL
// Probabilidad de salir: 60% en cada acción → sale rápido (relación DEMO:LOGO = 6:1)
if (previous_app_mode_ != AppMode::MANUAL && rand() % 100 < 60) {
exitLogoMode(true); // Volver a DEMO/DEMO_LITE
}
}
// MODO DEMO/DEMO_LITE: Acciones normales
else {
bool is_lite = (current_app_mode_ == AppMode::DEMO_LITE);
performDemoAction(is_lite);
// Resetear timer y calcular próximo intervalo aleatorio
demo_timer_ = 0.0f;
// Usar intervalos diferentes según modo
float interval_min = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
float interval_max = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
float interval_range = interval_max - interval_min;
demo_next_action_time_ = interval_min + (rand() % 1000) / 1000.0f * interval_range;
}
}
}
void Engine::performDemoAction(bool is_lite) {
// ============================================
// SALTO AUTOMÁTICO A LOGO MODE (Easter Egg)
// ============================================
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) {
// 10% probabilidad de saltar a Logo Mode
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) {
enterLogoMode(true); // Entrar desde DEMO
return;
}
}
} else {
// DEMO COMPLETO: Verificar condiciones para salto a Logo Mode
if (static_cast<int>(balls_.size()) >= LOGO_MODE_MIN_BALLS) {
// 15% probabilidad de saltar a Logo Mode
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) {
enterLogoMode(true); // Entrar desde DEMO
return;
}
}
}
// ============================================
// ACCIONES NORMALES DE DEMO/DEMO_LITE
// ============================================
int TOTAL_WEIGHT;
int random_value;
int accumulated_weight = 0;
if (is_lite) {
// DEMO LITE: Solo física/figuras
TOTAL_WEIGHT = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE;
random_value = rand() % TOTAL_WEIGHT;
// Cambiar dirección gravedad (25%)
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR;
if (random_value < accumulated_weight) {
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
changeGravityDirection(new_direction);
return;
}
// Toggle gravedad ON/OFF (20%)
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_TOGGLE;
if (random_value < accumulated_weight) {
toggleGravityOnOff();
return;
}
// Activar figura 3D (25%) - PNG_SHAPE excluido (reservado para Logo Mode)
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
if (random_value < accumulated_weight) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
int shape_index = rand() % 8;
activateShape(shapes[shape_index]);
return;
}
// Toggle física ↔ figura (20%)
accumulated_weight += DEMO_LITE_WEIGHT_TOGGLE_PHYSICS;
if (random_value < accumulated_weight) {
toggleShapeMode(false); // NO forzar gravedad al salir
return;
}
// Aplicar impulso (10%)
accumulated_weight += DEMO_LITE_WEIGHT_IMPULSE;
if (random_value < accumulated_weight) {
pushBallsAwayFromGravity();
return;
}
} else {
// DEMO COMPLETO: Todas las acciones
TOTAL_WEIGHT = DEMO_WEIGHT_GRAVITY_DIR + DEMO_WEIGHT_GRAVITY_TOGGLE + DEMO_WEIGHT_SHAPE + DEMO_WEIGHT_TOGGLE_PHYSICS + DEMO_WEIGHT_REGENERATE_SHAPE + DEMO_WEIGHT_THEME + DEMO_WEIGHT_SCENARIO + DEMO_WEIGHT_IMPULSE + DEMO_WEIGHT_DEPTH_ZOOM + DEMO_WEIGHT_SHAPE_SCALE + DEMO_WEIGHT_SPRITE;
random_value = rand() % TOTAL_WEIGHT;
// Cambiar dirección gravedad (10%)
accumulated_weight += DEMO_WEIGHT_GRAVITY_DIR;
if (random_value < accumulated_weight) {
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
changeGravityDirection(new_direction);
return;
}
// Toggle gravedad ON/OFF (8%)
accumulated_weight += DEMO_WEIGHT_GRAVITY_TOGGLE;
if (random_value < accumulated_weight) {
toggleGravityOnOff();
return;
}
// Activar figura 3D (20%) - PNG_SHAPE excluido (reservado para Logo Mode)
accumulated_weight += DEMO_WEIGHT_SHAPE;
if (random_value < accumulated_weight) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
int shape_index = rand() % 8;
activateShape(shapes[shape_index]);
return;
}
// Toggle física ↔ figura (12%)
accumulated_weight += DEMO_WEIGHT_TOGGLE_PHYSICS;
if (random_value < accumulated_weight) {
toggleShapeMode(false); // NO forzar gravedad al salir
return;
}
// Re-generar misma figura (8%)
accumulated_weight += DEMO_WEIGHT_REGENERATE_SHAPE;
if (random_value < accumulated_weight) {
if (current_mode_ == SimulationMode::SHAPE && active_shape_) {
generateShape(); // Re-generar sin cambiar tipo
}
return;
}
// Cambiar tema (15%)
accumulated_weight += DEMO_WEIGHT_THEME;
if (random_value < accumulated_weight) {
ColorTheme new_theme = static_cast<ColorTheme>(rand() % 6);
startThemeTransition(new_theme);
return;
}
// Cambiar escenario (10%) - EXCLUIR índices 0, 6, 7 (1, 50K, 100K pelotas)
accumulated_weight += DEMO_WEIGHT_SCENARIO;
if (random_value < accumulated_weight) {
// Escenarios válidos: índices 1, 2, 3, 4, 5 (10, 100, 500, 1000, 10000 pelotas)
int valid_scenarios[] = {1, 2, 3, 4, 5};
scenario_ = valid_scenarios[rand() % 5];
initBalls(scenario_);
return;
}
// Aplicar impulso (10%)
accumulated_weight += DEMO_WEIGHT_IMPULSE;
if (random_value < accumulated_weight) {
pushBallsAwayFromGravity();
return;
}
// Toggle profundidad (3%)
accumulated_weight += DEMO_WEIGHT_DEPTH_ZOOM;
if (random_value < accumulated_weight) {
if (current_mode_ == SimulationMode::SHAPE) {
depth_zoom_enabled_ = !depth_zoom_enabled_;
}
return;
}
// Cambiar escala de figura (2%)
accumulated_weight += DEMO_WEIGHT_SHAPE_SCALE;
if (random_value < accumulated_weight) {
if (current_mode_ == SimulationMode::SHAPE) {
int scale_action = rand() % 3;
if (scale_action == 0) {
shape_scale_factor_ += SHAPE_SCALE_STEP;
} else if (scale_action == 1) {
shape_scale_factor_ -= SHAPE_SCALE_STEP;
} else {
shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
}
clampShapeScale();
generateShape();
}
return;
}
// Cambiar sprite (2%)
accumulated_weight += DEMO_WEIGHT_SPRITE;
if (random_value < accumulated_weight) {
switchTexture();
return;
}
}
}
// Randomizar todo al iniciar modo DEMO
void Engine::randomizeOnDemoStart(bool is_lite) {
if (is_lite) {
// DEMO LITE: Solo randomizar física/figura + gravedad
// Elegir aleatoriamente entre modo física o figura
if (rand() % 2 == 0) {
// Modo física
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(false); // Salir a física sin forzar gravedad
}
} else {
// Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
activateShape(shapes[rand() % 8]);
}
// Randomizar gravedad: dirección + ON/OFF
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
changeGravityDirection(new_direction);
if (rand() % 2 == 0) {
toggleGravityOnOff(); // 50% probabilidad de desactivar gravedad
}
} else {
// DEMO COMPLETO: Randomizar TODO
// 1. Escenario (excluir índices 0, 6, 7)
int valid_scenarios[] = {1, 2, 3, 4, 5};
scenario_ = valid_scenarios[rand() % 5];
initBalls(scenario_);
// 2. Tema
ColorTheme new_theme = static_cast<ColorTheme>(rand() % 6);
startThemeTransition(new_theme);
// 3. Sprite
if (rand() % 2 == 0) {
switchTexture();
}
// 4. Física o Figura
if (rand() % 2 == 0) {
// Modo física
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(false);
}
} else {
// Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
activateShape(shapes[rand() % 8]);
// 5. Profundidad (solo si estamos en figura)
if (rand() % 2 == 0) {
depth_zoom_enabled_ = !depth_zoom_enabled_;
}
// 6. Escala de figura (aleatoria entre 0.5x y 2.0x)
shape_scale_factor_ = 0.5f + (rand() % 1500) / 1000.0f;
clampShapeScale();
generateShape();
}
// 7. Gravedad: dirección + ON/OFF
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
changeGravityDirection(new_direction);
if (rand() % 3 == 0) { // 33% probabilidad de desactivar gravedad
toggleGravityOnOff();
}
}
}
// Toggle gravedad ON/OFF para todas las pelotas
void Engine::toggleGravityOnOff() {
// Alternar entre activar/desactivar gravedad
bool first_ball_gravity_enabled = (balls_.empty() || balls_[0]->getGravityForce() > 0.0f);
if (first_ball_gravity_enabled) {
// Desactivar gravedad
forceBallsGravityOff();
} else {
// Activar gravedad
forceBallsGravityOn();
}
}
// ============================================================================
// SISTEMA DE MODO LOGO (Easter Egg - "Marca de Agua")
// ============================================================================
// Entrar al Modo Logo (manual con tecla K o automático desde DEMO)
void Engine::enterLogoMode(bool from_demo) {
// Verificar mínimo de pelotas
if (static_cast<int>(balls_.size()) < LOGO_MODE_MIN_BALLS) {
// Ajustar a 5000 pelotas automáticamente
scenario_ = 5; // Escenario 5000 pelotas (índice 5 en BALL_COUNT_SCENARIOS)
initBalls(scenario_);
}
// Guardar estado previo (para restaurar al salir)
logo_previous_theme_ = current_theme_;
logo_previous_texture_index_ = current_texture_index_;
logo_previous_shape_scale_ = shape_scale_factor_;
// Buscar índice de textura "tiny"
size_t tiny_index = current_texture_index_; // Por defecto mantener actual
for (size_t i = 0; i < texture_names_.size(); i++) {
if (texture_names_[i] == "tiny") {
tiny_index = i;
break;
}
}
// Aplicar configuración fija del Modo Logo
if (tiny_index != current_texture_index_) {
current_texture_index_ = tiny_index;
int old_size = current_ball_size_;
current_ball_size_ = textures_[current_texture_index_]->getWidth();
updateBallSizes(old_size, current_ball_size_);
// Actualizar textura global y en cada pelota
texture_ = textures_[current_texture_index_];
for (auto& ball : balls_) {
ball->setTexture(texture_);
}
}
// Cambiar a tema MONOCHROME
startThemeTransition(ColorTheme::MONOCHROME);
// Establecer escala a 120%
shape_scale_factor_ = LOGO_MODE_SHAPE_SCALE;
clampShapeScale();
// Activar PNG_SHAPE (el logo)
activateShape(ShapeType::PNG_SHAPE);
// Configurar PNG_SHAPE en modo LOGO (flip intervals más largos)
if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape) {
png_shape->setLogoMode(true);
}
}
// Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente)
setState(AppMode::LOGO);
}
// Salir del Modo Logo (volver a estado anterior o salir de DEMO)
void Engine::exitLogoMode(bool return_to_demo) {
if (current_app_mode_ != AppMode::LOGO) return;
// Restaurar estado previo
startThemeTransition(logo_previous_theme_);
if (logo_previous_texture_index_ != current_texture_index_ &&
logo_previous_texture_index_ < textures_.size()) {
current_texture_index_ = logo_previous_texture_index_;
int old_size = current_ball_size_;
current_ball_size_ = textures_[current_texture_index_]->getWidth();
updateBallSizes(old_size, current_ball_size_);
// Actualizar textura global y en cada pelota
texture_ = textures_[current_texture_index_];
for (auto& ball : balls_) {
ball->setTexture(texture_);
}
}
shape_scale_factor_ = logo_previous_shape_scale_;
clampShapeScale();
generateShape();
// Desactivar modo LOGO en PNG_SHAPE (volver a flip intervals normales)
if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape) {
png_shape->setLogoMode(false);
}
}
if (!return_to_demo) {
// Salida manual (tecla K): volver a MANUAL
setState(AppMode::MANUAL);
} else {
// Volver al modo previo (DEMO o DEMO_LITE)
setState(previous_app_mode_);
}
}
// Toggle manual del Modo Logo (tecla K)
void Engine::toggleLogoMode() {
if (current_app_mode_ == AppMode::LOGO) {
exitLogoMode(false); // Salir y volver a MANUAL
} else {
enterLogoMode(false); // Entrar manualmente
}
}
// Sistema de cambio de sprites dinámico
void Engine::updateBallSizes(int old_size, int new_size) {
float delta_size = static_cast<float>(new_size - old_size);
for (auto& ball : balls_) {
SDL_FRect pos = ball->getPosition();
// Solo ajustar posición si la pelota está en superficie
if (ball->isOnSurface()) {
GravityDirection grav_dir = ball->getGravityDirection();
switch (grav_dir) {
case GravityDirection::DOWN:
// Superficie inferior: ajustar Y hacia abajo si crece
pos.y += delta_size;
break;
case GravityDirection::UP:
// Superficie superior: ajustar Y hacia arriba si crece
pos.y -= delta_size;
break;
case GravityDirection::LEFT:
// Superficie izquierda: ajustar X hacia izquierda si crece
pos.x -= delta_size;
break;
case GravityDirection::RIGHT:
// Superficie derecha: ajustar X hacia derecha si crece
pos.x += delta_size;
break;
}
}
// Actualizar tamaño del hitbox
ball->updateSize(new_size);
// Si ajustamos posición, aplicarla ahora
if (ball->isOnSurface()) {
ball->setRotoBallScreenPosition(pos.x, pos.y);
}
}
}
void Engine::switchTexture() {
if (textures_.empty()) return;
// Guardar tamaño antiguo
int old_size = current_ball_size_;
// Cambiar a siguiente textura (ciclar)
current_texture_index_ = (current_texture_index_ + 1) % textures_.size();
texture_ = textures_[current_texture_index_];
// Obtener nuevo tamaño de la textura
int new_size = texture_->getWidth();
current_ball_size_ = new_size;
// Actualizar texturas y tamaños de todas las pelotas
for (auto& ball : balls_) {
ball->setTexture(texture_);
}
// Ajustar posiciones según el cambio de tamaño
updateBallSizes(old_size, new_size);
// Mostrar texto informativo (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
// Obtener nombre de textura (uppercase)
std::string texture_name = texture_names_[current_texture_index_];
std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper);
text_ = "SPRITE: " + texture_name;
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
}
// Sistema de Figuras 3D - Alternar entre modo física y última figura (Toggle con tecla F)
void Engine::toggleShapeMode(bool force_gravity_on_exit) {
if (current_mode_ == SimulationMode::PHYSICS) {
// Cambiar a modo figura (usar última figura seleccionada)
activateShape(last_shape_type_);
// Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO
if (current_app_mode_ == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape) {
png_shape->setLogoMode(true);
}
}
}
// Si estamos en LOGO MODE, generar threshold aleatorio de convergencia (75-100%)
if (current_app_mode_ == AppMode::LOGO) {
logo_convergence_threshold_ = LOGO_CONVERGENCE_MIN +
(rand() % 1000) / 1000.0f * (LOGO_CONVERGENCE_MAX - LOGO_CONVERGENCE_MIN);
shape_convergence_ = 0.0f; // Reset convergencia al entrar
}
} else {
// Volver a modo física normal
current_mode_ = SimulationMode::PHYSICS;
// Desactivar atracción y resetear escala de profundidad
for (auto& ball : balls_) {
ball->enableRotoBallAttraction(false);
ball->setDepthScale(1.0f); // Reset escala a 100% (evita "pop" visual)
}
// Activar gravedad al salir (solo si se especifica)
if (force_gravity_on_exit) {
forceBallsGravityOn();
}
// Mostrar texto informativo (solo si NO estamos en modo demo o logo)
if (current_app_mode_ == AppMode::MANUAL) {
text_ = "MODO FISICA";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
}
}
}
// Activar figura específica (llamado por teclas Q/W/E/R/Y/U/I o por toggleShapeMode)
void Engine::activateShape(ShapeType type) {
// Guardar como última figura seleccionada
last_shape_type_ = type;
current_shape_type_ = type;
// Cambiar a modo figura
current_mode_ = SimulationMode::SHAPE;
// Desactivar gravedad al entrar en modo figura
forceBallsGravityOff();
// Crear instancia polimórfica de la figura correspondiente
switch (type) {
case ShapeType::SPHERE:
active_shape_ = std::make_unique<SphereShape>();
break;
case ShapeType::CUBE:
active_shape_ = std::make_unique<CubeShape>();
break;
case ShapeType::HELIX:
active_shape_ = std::make_unique<HelixShape>();
break;
case ShapeType::TORUS:
active_shape_ = std::make_unique<TorusShape>();
break;
case ShapeType::WAVE_GRID:
active_shape_ = std::make_unique<WaveGridShape>();
break;
case ShapeType::CYLINDER:
active_shape_ = std::make_unique<CylinderShape>();
break;
case ShapeType::ICOSAHEDRON:
active_shape_ = std::make_unique<IcosahedronShape>();
break;
case ShapeType::ATOM:
active_shape_ = std::make_unique<AtomShape>();
break;
case ShapeType::PNG_SHAPE:
active_shape_ = std::make_unique<PNGShape>("data/shapes/jailgames.png");
break;
default:
active_shape_ = std::make_unique<SphereShape>(); // Fallback
break;
}
// Generar puntos de la figura
generateShape();
// Activar atracción física en todas las pelotas
for (auto& ball : balls_) {
ball->enableRotoBallAttraction(true);
}
// Mostrar texto informativo con nombre de figura (solo si NO estamos en modo demo o logo)
if (active_shape_ && current_app_mode_ == AppMode::MANUAL) {
text_ = std::string("MODO ") + active_shape_->getName();
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
}
}
// Generar puntos de la figura activa
void Engine::generateShape() {
if (!active_shape_) return;
int num_points = static_cast<int>(balls_.size());
active_shape_->generatePoints(num_points, static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_));
}
// Actualizar figura activa (rotación, animación, etc.)
void Engine::updateShape() {
if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) return;
// Actualizar animación de la figura
active_shape_->update(delta_time_, static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_));
// Obtener factor de escala para física (base de figura + escala manual)
float scale_factor = active_shape_->getScaleFactor(static_cast<float>(current_screen_height_)) * shape_scale_factor_;
// Centro de la pantalla
float center_x = current_screen_width_ / 2.0f;
float center_y = current_screen_height_ / 2.0f;
// Actualizar cada pelota con física de atracción
for (size_t i = 0; i < balls_.size(); i++) {
// Obtener posición 3D rotada del punto i
float x_3d, y_3d, z_3d;
active_shape_->getPoint3D(static_cast<int>(i), x_3d, y_3d, z_3d);
// Aplicar escala manual a las coordenadas 3D
x_3d *= shape_scale_factor_;
y_3d *= shape_scale_factor_;
z_3d *= shape_scale_factor_;
// Proyección 2D ortográfica (punto objetivo móvil)
float target_x = center_x + x_3d;
float target_y = center_y + y_3d;
// Aplicar fuerza de atracción física hacia el punto rotado
// Usar constantes SHAPE (mayor pegajosidad que ROTOBALL)
float shape_size = scale_factor * 80.0f; // 80px = radio base
balls_[i]->applyRotoBallForce(target_x, target_y, shape_size, delta_time_, SHAPE_SPRING_K, SHAPE_DAMPING_BASE, SHAPE_DAMPING_NEAR, SHAPE_NEAR_THRESHOLD, SHAPE_MAX_FORCE);
// Calcular brillo según profundidad Z para renderizado
// Normalizar Z al rango de la figura (asumiendo simetría ±shape_size)
float z_normalized = (z_3d + shape_size) / (2.0f * shape_size);
z_normalized = std::max(0.0f, std::min(1.0f, z_normalized));
balls_[i]->setDepthBrightness(z_normalized);
// Calcular escala según profundidad Z (perspectiva) - solo si está activado
// 0.0 (fondo) → 0.5x, 0.5 (medio) → 1.0x, 1.0 (frente) → 1.5x
float depth_scale = depth_zoom_enabled_ ? (0.5f + z_normalized * 1.0f) : 1.0f;
balls_[i]->setDepthScale(depth_scale);
}
// Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo)
if (current_app_mode_ == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
int balls_near = 0;
float distance_threshold = LOGO_CONVERGENCE_DISTANCE; // 20px fijo (más permisivo)
for (const auto& ball : balls_) {
if (ball->getDistanceToTarget() < distance_threshold) {
balls_near++;
}
}
shape_convergence_ = static_cast<float>(balls_near) / balls_.size();
}
}
// Limitar escala de figura para evitar que se salga de pantalla
void Engine::clampShapeScale() {
// Calcular tamaño máximo permitido según resolución actual
// La figura más grande (esfera/cubo) usa ~33% de altura por defecto
// Permitir hasta que la figura ocupe 90% de la dimensión más pequeña
float max_dimension = std::min(current_screen_width_, current_screen_height_);
float base_size_factor = 0.333f; // ROTOBALL_RADIUS_FACTOR o similar
float max_scale_for_screen = (max_dimension * 0.9f) / (max_dimension * base_size_factor);
// Limitar entre SHAPE_SCALE_MIN y el mínimo de (SHAPE_SCALE_MAX, max_scale_for_screen)
float max_allowed = std::min(SHAPE_SCALE_MAX, max_scale_for_screen);
shape_scale_factor_ = std::max(SHAPE_SCALE_MIN, std::min(max_allowed, shape_scale_factor_));
}