Files
vibe3_physics/source/engine.cpp
Sergio Valor 10a4234d49 Refactor: Sistema de modos y notificaciones mejorado
Cambios principales:
- Renombrado AppMode::MANUAL → AppMode::SANDBOX (nomenclatura más clara)
- Notificaciones ahora funcionan en TODAS las transiciones de modo
- Lógica de teclas D/L/K simplificada: toggle exclusivo modo ↔ SANDBOX
- Mensajes simplificados: "MODO DEMO", "MODO SANDBOX", etc. (sin ON/OFF)
- Eliminado check restrictivo en showNotificationForAction()

Comportamiento nuevo:
- Tecla D: Toggle DEMO ↔ SANDBOX
- Tecla L: Toggle DEMO_LITE ↔ SANDBOX
- Tecla K: Toggle LOGO ↔ SANDBOX
- Cada tecla activa su modo o vuelve a SANDBOX si ya está activo
- Notificaciones visibles tanto al activar como desactivar modos

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 07:44:57 +02:00

2348 lines
95 KiB
C++

#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/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/lissajous_shape.h" // for LissajousShape
#include "shapes/png_shape.h" // for PNGShape
#include "shapes/sphere_shape.h" // for SphereShape
#include "shapes/torus_shape.h" // for TorusShape
// 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)
// NOTA: SDL_WINDOW_HIGH_PIXEL_DENSITY removido por incompatibilidad con STRETCH mode (F4)
// El DPI se detectará manualmente con SDL_GetWindowSizeInPixels()
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)));
// Calcular tamaño físico de ventana y tamaño de fuente absoluto
updatePhysicalWindowSize();
// Inicializar ThemeManager
theme_manager_ = std::make_unique<ThemeManager>();
theme_manager_->initialize();
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 (OBSOLETO: sistema antiguo, se mantiene por compatibilidad temporal)
if (show_text_) {
show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION);
}
// Actualizar sistema de notificaciones
notifier_.update(current_time);
// Actualizar Modo DEMO (auto-play)
updateDemoMode();
// Actualizar transiciones de temas (delegado a ThemeManager)
theme_manager_->update(delta_time_);
}
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
showNotificationForAction("Gravity Off");
} else {
switchBallsGravity(); // Toggle normal en modo física
// Determinar estado actual de gravedad (gravity_force_ != 0.0f significa ON)
bool gravity_on = balls_.empty() ? true : (balls_[0]->getGravityForce() != 0.0f);
showNotificationForAction(gravity_on ? "Gravity On" : "Gravity Off");
}
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);
showNotificationForAction("Gravity 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);
showNotificationForAction("Gravity 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);
showNotificationForAction("Gravity 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);
showNotificationForAction("Gravity 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();
// Mostrar notificación según el modo actual después del toggle
if (current_mode_ == SimulationMode::PHYSICS) {
showNotificationForAction("Physics Mode");
} else {
// Mostrar nombre de la figura actual
const char* shape_names[] = {"Sphere", "Lissajous", "Helix", "Torus", "Cube", "Cylinder", "Icosahedron", "Atom", "PNG Shape"};
showNotificationForAction(shape_names[static_cast<int>(current_shape_type_)]);
}
break;
// Selección directa de figuras 3D
case SDLK_Q:
activateShape(ShapeType::SPHERE);
showNotificationForAction("Sphere");
break;
case SDLK_W:
activateShape(ShapeType::LISSAJOUS);
showNotificationForAction("Lissajous");
break;
case SDLK_E:
activateShape(ShapeType::HELIX);
showNotificationForAction("Helix");
break;
case SDLK_R:
activateShape(ShapeType::TORUS);
showNotificationForAction("Torus");
break;
case SDLK_T:
activateShape(ShapeType::CUBE);
showNotificationForAction("Cube");
break;
case SDLK_Y:
activateShape(ShapeType::CYLINDER);
showNotificationForAction("Cylinder");
break;
case SDLK_U:
activateShape(ShapeType::ICOSAHEDRON);
showNotificationForAction("Icosahedron");
break;
case SDLK_I:
activateShape(ShapeType::ATOM);
showNotificationForAction("Atom");
break;
case SDLK_O:
activateShape(ShapeType::PNG_SHAPE);
showNotificationForAction("PNG Shape");
break;
// Ciclar temas de color (movido de T a B)
case SDLK_B:
{
// Detectar si Shift está presionado
SDL_Keymod modstate = SDL_GetModState();
if (modstate & SDL_KMOD_SHIFT) {
// Shift+B: Ciclar hacia atrás (tema anterior)
theme_manager_->cyclePrevTheme();
} else {
// B solo: Ciclar hacia adelante (tema siguiente)
theme_manager_->cycleTheme();
}
// Mostrar notificación con el nombre del tema
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
// Temas de colores con teclado numérico (con transición suave)
case SDLK_KP_1:
// Página 0: SUNSET (0), Página 1: OCEAN_WAVES (10)
{
int theme_index = (theme_page_ == 0) ? 0 : 10;
theme_manager_->switchToTheme(theme_index);
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_2:
// Página 0: OCEAN (1), Página 1: NEON_PULSE (11)
{
int theme_index = (theme_page_ == 0) ? 1 : 11;
theme_manager_->switchToTheme(theme_index);
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_3:
// Página 0: NEON (2), Página 1: FIRE (12)
{
int theme_index = (theme_page_ == 0) ? 2 : 12;
theme_manager_->switchToTheme(theme_index);
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_4:
// Página 0: FOREST (3), Página 1: AURORA (13)
{
int theme_index = (theme_page_ == 0) ? 3 : 13;
theme_manager_->switchToTheme(theme_index);
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_5:
// Página 0: RGB (4), Página 1: VOLCANIC (14)
{
int theme_index = (theme_page_ == 0) ? 4 : 14;
theme_manager_->switchToTheme(theme_index);
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_6:
// Solo página 0: MONOCHROME (5)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(5); // MONOCHROME
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_7:
// Solo página 0: LAVENDER (6)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(6); // LAVENDER
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_8:
// Solo página 0: CRIMSON (7)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(7); // CRIMSON
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_9:
// Solo página 0: EMERALD (8)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(8); // EMERALD
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
case SDLK_KP_0:
// Solo página 0: SUNRISE (9)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(9); // SUNRISE
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
// Toggle de página de temas (Numpad Enter)
case SDLK_KP_ENTER:
// Alternar entre página 0 y página 1
theme_page_ = (theme_page_ == 0) ? 1 : 0;
showNotificationForAction((theme_page_ == 0) ? "Página 1" : "Página 2");
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();
showNotificationForAction("Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%");
}
break;
case SDLK_KP_MINUS:
if (current_mode_ == SimulationMode::SHAPE) {
shape_scale_factor_ -= SHAPE_SCALE_STEP;
clampShapeScale();
showNotificationForAction("Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%");
}
break;
case SDLK_KP_MULTIPLY:
if (current_mode_ == SimulationMode::SHAPE) {
shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
showNotificationForAction("Escala 100%");
}
break;
case SDLK_KP_DIVIDE:
if (current_mode_ == SimulationMode::SHAPE) {
depth_zoom_enabled_ = !depth_zoom_enabled_;
showNotificationForAction(depth_zoom_enabled_ ? "Depth Zoom On" : "Depth Zoom Off");
}
break;
case SDLK_1:
scenario_ = 0;
initBalls(scenario_);
showNotificationForAction("10 Pelotas");
break;
case SDLK_2:
scenario_ = 1;
initBalls(scenario_);
showNotificationForAction("50 Pelotas");
break;
case SDLK_3:
scenario_ = 2;
initBalls(scenario_);
showNotificationForAction("100 Pelotas");
break;
case SDLK_4:
scenario_ = 3;
initBalls(scenario_);
showNotificationForAction("500 Pelotas");
break;
case SDLK_5:
scenario_ = 4;
initBalls(scenario_);
showNotificationForAction("1,000 Pelotas");
break;
case SDLK_6:
scenario_ = 5;
initBalls(scenario_);
showNotificationForAction("5,000 Pelotas");
break;
case SDLK_7:
scenario_ = 6;
initBalls(scenario_);
showNotificationForAction("10,000 Pelotas");
break;
case SDLK_8:
scenario_ = 7;
initBalls(scenario_);
showNotificationForAction("50,000 Pelotas");
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) {
theme_manager_->pauseDynamic();
} else {
// D sin Shift = Toggle DEMO ↔ SANDBOX
if (current_app_mode_ == AppMode::DEMO) {
// Ya estamos en DEMO → volver a SANDBOX
setState(AppMode::SANDBOX);
showNotificationForAction("MODO SANDBOX");
} else {
// Estamos en otro modo → ir a DEMO
setState(AppMode::DEMO);
randomizeOnDemoStart(false);
showNotificationForAction("MODO DEMO");
}
}
break;
// Toggle Modo DEMO LITE (solo física/figuras)
case SDLK_L:
if (current_app_mode_ == AppMode::DEMO_LITE) {
// Ya estamos en DEMO_LITE → volver a SANDBOX
setState(AppMode::SANDBOX);
showNotificationForAction("MODO SANDBOX");
} else {
// Estamos en otro modo → ir a DEMO_LITE
setState(AppMode::DEMO_LITE);
randomizeOnDemoStart(true);
showNotificationForAction("MODO DEMO LITE");
}
break;
// Toggle Modo LOGO (easter egg - marca de agua)
case SDLK_K:
if (current_app_mode_ == AppMode::LOGO) {
// Ya estamos en LOGO → volver a SANDBOX
exitLogoMode(false);
showNotificationForAction("MODO SANDBOX");
} else {
// Estamos en otro modo → ir a LOGO
enterLogoMode(false);
showNotificationForAction("MODO LOGO");
}
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 (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();
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 = theme_manager_->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 = 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++;
}
}
// 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()));
}
// Calcular factores de escala lógica → física para texto absoluto
float text_scale_x = static_cast<float>(physical_window_width_) / static_cast<float>(current_screen_width_);
float text_scale_y = static_cast<float>(physical_window_height_) / static_cast<float>(current_screen_height_);
// SISTEMA DE TEXTO ANTIGUO DESHABILITADO
// Reemplazado completamente por el sistema de notificaciones (Notifier)
// El doble renderizado causaba que aparecieran textos duplicados detrás de las notificaciones
/*
if (show_text_) {
// Obtener datos del tema actual (delegado a ThemeManager)
int text_color_r, text_color_g, text_color_b;
theme_manager_->getCurrentThemeTextColor(text_color_r, text_color_g, text_color_b);
const char* theme_name_es = theme_manager_->getCurrentThemeNameES();
// Calcular espaciado dinámico
int line_height = text_renderer_.getTextHeight();
int margin = 8;
// Texto del número de pelotas con color del tema
text_renderer_.printPhysical(text_pos_, margin, text_.c_str(), text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
// 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 = text_renderer_.getTextWidth(theme_name_es);
int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente
int theme_y = margin + line_height; // Espaciado dinámico
// Texto del nombre del tema con el mismo color
text_renderer_.printPhysical(theme_x, theme_y, theme_name_es, text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
}
}
*/
// Debug display (solo si está activado con tecla H)
if (show_debug_) {
// Obtener altura de línea para espaciado dinámico (usando fuente debug)
int line_height = text_renderer_debug_.getTextHeight();
int margin = 8; // Margen constante
int current_y = margin; // Y inicial
// Mostrar contador de FPS en esquina superior derecha
int fps_text_width = text_renderer_debug_.getTextWidth(fps_text_.c_str());
int fps_x = current_screen_width_ - fps_text_width - margin;
text_renderer_debug_.printPhysical(fps_x, current_y, fps_text_.c_str(), 255, 255, 0, text_scale_x, text_scale_y); // Amarillo
// Mostrar estado V-Sync en esquina superior izquierda
text_renderer_debug_.printPhysical(margin, current_y, vsync_text_.c_str(), 0, 255, 255, text_scale_x, text_scale_y); // Cian
current_y += line_height;
// Debug: Mostrar valores de la primera pelota (si existe)
if (!balls_.empty()) {
// Línea 1: Gravedad
int grav_int = static_cast<int>(balls_[0]->getGravityForce());
std::string grav_text = "Gravedad: " + std::to_string(grav_int);
text_renderer_debug_.printPhysical(margin, current_y, grav_text.c_str(), 255, 0, 255, text_scale_x, text_scale_y); // Magenta
current_y += line_height;
// Línea 2: Velocidad Y
int vy_int = static_cast<int>(balls_[0]->getVelocityY());
std::string vy_text = "Velocidad Y: " + std::to_string(vy_int);
text_renderer_debug_.printPhysical(margin, current_y, vy_text.c_str(), 255, 0, 255, text_scale_x, text_scale_y); // Magenta
current_y += line_height;
// Línea 3: Estado superficie
std::string surface_text = balls_[0]->isOnSurface() ? "Superficie: Sí" : "Superficie: No";
text_renderer_debug_.printPhysical(margin, current_y, surface_text.c_str(), 255, 0, 255, text_scale_x, text_scale_y); // Magenta
current_y += line_height;
// Línea 4: Coeficiente de rebote (loss)
float loss_val = balls_[0]->getLossCoefficient();
std::string loss_text = "Rebote: " + std::to_string(loss_val).substr(0, 4);
text_renderer_debug_.printPhysical(margin, current_y, loss_text.c_str(), 255, 0, 255, text_scale_x, text_scale_y); // Magenta
current_y += line_height;
// Línea 5: Dirección de gravedad
std::string gravity_dir_text = "Dirección: " + gravityDirectionToString(current_gravity_);
text_renderer_debug_.printPhysical(margin, current_y, gravity_dir_text.c_str(), 255, 255, 0, text_scale_x, text_scale_y); // Amarillo
current_y += line_height;
}
// Debug: Mostrar tema actual (delegado a ThemeManager)
std::string theme_text = std::string("Tema: ") + theme_manager_->getCurrentThemeNameEN();
text_renderer_debug_.printPhysical(margin, current_y, theme_text.c_str(), 255, 255, 128, text_scale_x, text_scale_y); // Amarillo claro
current_y += line_height;
// Debug: Mostrar modo de simulación actual
std::string mode_text;
if (current_mode_ == SimulationMode::PHYSICS) {
mode_text = "Modo: Física";
} else if (active_shape_) {
mode_text = std::string("Modo: ") + active_shape_->getName();
} else {
mode_text = "Modo: Forma";
}
text_renderer_debug_.printPhysical(margin, current_y, mode_text.c_str(), 0, 255, 128, text_scale_x, text_scale_y); // Verde claro
current_y += line_height;
// Debug: Mostrar convergencia en modo LOGO (solo cuando está activo)
if (current_app_mode_ == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
int convergence_percent = static_cast<int>(shape_convergence_ * 100.0f);
std::string convergence_text = "Convergencia: " + std::to_string(convergence_percent) + "%";
text_renderer_debug_.printPhysical(margin, current_y, convergence_text.c_str(), 255, 128, 0, text_scale_x, text_scale_y); // Naranja
current_y += line_height;
}
// Debug: Mostrar modo DEMO/LOGO activo (siempre visible cuando debug está ON)
// FIJO en tercera fila (no se mueve con otros elementos del HUD)
int fixed_y = margin + (line_height * 2); // Tercera fila fija
if (current_app_mode_ == AppMode::LOGO) {
const char* logo_text = "Modo Logo";
int logo_text_width = text_renderer_debug_.getTextWidth(logo_text);
int logo_x = (current_screen_width_ - logo_text_width) / 2;
text_renderer_debug_.printPhysical(logo_x, fixed_y, logo_text, 255, 128, 0, text_scale_x, text_scale_y); // Naranja
} else if (current_app_mode_ == AppMode::DEMO) {
const char* demo_text = "Modo Demo";
int demo_text_width = text_renderer_debug_.getTextWidth(demo_text);
int demo_x = (current_screen_width_ - demo_text_width) / 2;
text_renderer_debug_.printPhysical(demo_x, fixed_y, demo_text, 255, 165, 0, text_scale_x, text_scale_y); // Naranja
} else if (current_app_mode_ == AppMode::DEMO_LITE) {
const char* lite_text = "Modo Demo Lite";
int lite_text_width = text_renderer_debug_.getTextWidth(lite_text);
int lite_x = (current_screen_width_ - lite_text_width) / 2;
text_renderer_debug_.printPhysical(lite_x, fixed_y, lite_text, 255, 200, 0, text_scale_x, text_scale_y); // Amarillo-naranja
}
}
// Renderizar notificaciones (siempre al final, sobre todo lo demás)
notifier_.render();
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 (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));
}
// NOTA: setText() removido - las notificaciones ahora se llaman manualmente desde cada tecla
}
void Engine::setText() {
// Suprimir textos durante modos demo
if (current_app_mode_ != AppMode::SANDBOX) return;
// Generar texto de número de pelotas
int num_balls = BALL_COUNT_SCENARIOS[scenario_];
std::string notification_text;
if (num_balls == 1) {
notification_text = "1 Pelota";
} else if (num_balls < 1000) {
notification_text = std::to_string(num_balls) + " Pelotas";
} else {
// Formato con separador de miles para números grandes
notification_text = std::to_string(num_balls / 1000) + "," +
(num_balls % 1000 < 100 ? "0" : "") +
(num_balls % 1000 < 10 ? "0" : "") +
std::to_string(num_balls % 1000) + " Pelotas";
}
// Obtener color del tema actual para la notificación
int text_r, text_g, text_b;
theme_manager_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color notification_color = {
static_cast<Uint8>(text_r),
static_cast<Uint8>(text_g),
static_cast<Uint8>(text_b),
255
};
// Obtener color de fondo de la notificación desde el tema
int bg_r, bg_g, bg_b;
theme_manager_->getCurrentNotificationBackgroundColor(bg_r, bg_g, bg_b);
SDL_Color notification_bg_color = {
static_cast<Uint8>(bg_r),
static_cast<Uint8>(bg_g),
static_cast<Uint8>(bg_b),
255
};
// Mostrar notificación
notifier_.show(notification_text, NOTIFICATION_DURATION, notification_color, notification_bg_color);
// Sistema antiguo (mantener temporalmente para compatibilidad)
text_ = notification_text;
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
void Engine::showNotificationForAction(const std::string& text) {
// IMPORTANTE: Esta función solo se llama desde handlers de teclado (acciones manuales)
// NUNCA se llama desde código automático (DEMO/LOGO), por lo tanto siempre mostramos notificación
// Obtener color del tema actual para el texto
int text_r, text_g, text_b;
theme_manager_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color notification_color = {
static_cast<Uint8>(text_r),
static_cast<Uint8>(text_g),
static_cast<Uint8>(text_b),
255
};
// Obtener color de fondo de la notificación desde el tema
int bg_r, bg_g, bg_b;
theme_manager_->getCurrentNotificationBackgroundColor(bg_r, bg_g, bg_b);
SDL_Color notification_bg_color = {
static_cast<Uint8>(bg_r),
static_cast<Uint8>(bg_g),
static_cast<Uint8>(bg_b),
255
};
// Mostrar notificación
notifier_.show(text, NOTIFICATION_DURATION, notification_color, notification_bg_color);
}
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_ ? "V-Sync: On" : "V-Sync: 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_);
// Actualizar dimensiones físicas después del cambio
updatePhysicalWindowSize();
}
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);
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
// 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);
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
// 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_ = "Escalado: ";
text_ += mode_name;
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 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::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;
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
}
void Engine::zoomIn() {
setWindowZoom(current_window_zoom_ + 1);
}
void Engine::zoomOut() {
setWindowZoom(current_window_zoom_ - 1);
}
void Engine::updatePhysicalWindowSize() {
if (real_fullscreen_enabled_) {
// En fullscreen real (F4), usar resolución del display
physical_window_width_ = current_screen_width_;
physical_window_height_ = current_screen_height_;
} else if (fullscreen_enabled_) {
// En fullscreen F3, obtener tamaño REAL del display (no del framebuffer lógico)
// SDL_GetRenderOutputSize() falla en F3 (devuelve tamaño lógico 960x720)
// Necesitamos el tamaño FÍSICO real de la pantalla
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) {
physical_window_width_ = dm->w;
physical_window_height_ = dm->h;
}
SDL_free(displays);
}
} else {
// En modo ventana, obtener tamaño FÍSICO real del framebuffer
int window_w = 0, window_h = 0;
SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
physical_window_width_ = window_w;
physical_window_height_ = window_h;
}
// Recalcular tamaño de fuente basado en altura física
// Referencia: 8px a 1440p (monitor del usuario)
int font_size = (physical_window_height_ * TEXT_BASE_SIZE) / 1440;
if (font_size < 6) font_size = 6; // Tamaño mínimo legible
// Reinicializar TextRenderers con nuevo tamaño de fuente
text_renderer_.cleanup();
text_renderer_debug_.cleanup();
text_renderer_notifier_.cleanup();
text_renderer_.init(renderer_, TEXT_FONT_PATH, font_size, TEXT_ANTIALIASING);
text_renderer_debug_.init(renderer_, TEXT_FONT_PATH, font_size, TEXT_ANTIALIASING);
// TextRenderer para notificaciones: Detectar DPI y ajustar tamaño
// En pantallas Retina/HiDPI, el texto necesita ser más grande para ser legible
int logical_w = 0, logical_h = 0;
SDL_GetWindowSize(window_, &logical_w, &logical_h);
// Usar physical_window_width_ que ya contiene el tamaño real del framebuffer
// (calculado arriba con SDL_GetRenderOutputSize o current_screen_width_)
int pixels_w = physical_window_width_;
// Calcular escala DPI (1.0 normal, 2.0 Retina, 3.0 en algunos displays)
float dpi_scale = (logical_w > 0) ? static_cast<float>(pixels_w) / static_cast<float>(logical_w) : 1.0f;
// Ajustar tamaño de fuente base (16px) por escala DPI
// Retina macOS: 16px * 2.0 = 32px (legible)
// Normal: 16px * 1.0 = 16px
int notification_font_size = static_cast<int>(TEXT_ABSOLUTE_SIZE * dpi_scale);
if (notification_font_size < 12) notification_font_size = 12; // Mínimo legible
text_renderer_notifier_.init(renderer_, TEXT_FONT_PATH, notification_font_size, TEXT_ANTIALIASING);
// Inicializar/actualizar Notifier con nuevas dimensiones
// NOTA: init() es seguro de llamar múltiples veces, solo actualiza punteros y dimensiones
// Esto asegura que el notifier tenga las referencias correctas tras resize/fullscreen
notifier_.init(renderer_, &text_renderer_notifier_, physical_window_width_, physical_window_height_);
}
// ============================================================================
// 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::SANDBOX) 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: Dos caminos posibles
if (logo_waiting_for_flip_) {
// CAMINO B: Esperando a que ocurran flips
// Obtener referencia a PNGShape si está activa
PNGShape* png_shape = nullptr;
if (active_shape_ && current_mode_ == SimulationMode::SHAPE) {
png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
}
if (png_shape) {
int current_flip_count = png_shape->getFlipCount();
// Detectar nuevo flip completado
if (current_flip_count > logo_current_flip_count_) {
logo_current_flip_count_ = current_flip_count;
}
// Si estamos EN o DESPUÉS del flip objetivo
// +1 porque queremos actuar DURANTE el flip N, no después de completarlo
if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
// Monitorear progreso del flip actual
if (png_shape->isFlipping()) {
float flip_progress = png_shape->getFlipProgress();
if (flip_progress >= logo_target_flip_percentage_) {
should_trigger = true; // ¡Trigger durante el flip!
}
}
}
}
} else {
// CAMINO A: Esperar convergencia + tiempo (comportamiento original)
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) → Decidir camino a seguir
// DECISIÓN BIFURCADA: ¿Cambio inmediato o esperar flips?
if (logo_waiting_for_flip_) {
// Ya estábamos esperando flips, y se disparó el trigger
// → Hacer el cambio SHAPE → PHYSICS ahora (durante el flip)
if (action < 50) {
toggleShapeMode(true); // Con gravedad ON
} else {
toggleShapeMode(false); // Con gravedad OFF
}
// Resetear variables de espera de flips
logo_waiting_for_flip_ = false;
logo_current_flip_count_ = 0;
// Resetear timer
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;
} else if (rand() % 100 < LOGO_FLIP_WAIT_PROBABILITY) {
// CAMINO B (50%): Esperar a que ocurran 1-3 flips
logo_waiting_for_flip_ = true;
logo_target_flip_number_ = LOGO_FLIP_WAIT_MIN + rand() % (LOGO_FLIP_WAIT_MAX - LOGO_FLIP_WAIT_MIN + 1);
logo_target_flip_percentage_ = LOGO_FLIP_TRIGGER_MIN + (rand() % 1000) / 1000.0f * (LOGO_FLIP_TRIGGER_MAX - LOGO_FLIP_TRIGGER_MIN);
logo_current_flip_count_ = 0;
// Resetear contador de flips en PNGShape
if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape) {
png_shape->resetFlipCount();
}
}
// NO hacer nada más este frame - esperar a que ocurran los flips
// El trigger se ejecutará en futuras iteraciones cuando se cumplan las condiciones
} else {
// CAMINO A (50%): Cambio inmediato
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);
}
// Resetear variables de espera de flips al cambiar a PHYSICS
logo_waiting_for_flip_ = false;
logo_current_flip_count_ = 0;
// Resetear timer con intervalos escalados
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;
}
} else {
// Logo animado (PHYSICS) → 3 opciones posibles
if (action < 60) {
// 60%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones)
toggleShapeMode(false);
// Resetear variables de espera de flips al volver a SHAPE
logo_waiting_for_flip_ = false;
logo_current_flip_count_ = 0;
} 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
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::SANDBOX && 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 &&
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
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::LISSAJOUS, 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::LISSAJOUS, 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) {
// Elegir entre TODOS los 15 temas (9 estáticos + 6 dinámicos)
int random_theme_index = rand() % 15;
theme_manager_->switchToTheme(random_theme_index);
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(false); // Suprimir notificación en modo automático
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::LISSAJOUS, 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 (elegir entre TODOS los 15 temas)
int random_theme_index = rand() % 15;
theme_manager_->switchToTheme(random_theme_index);
// 3. Sprite
if (rand() % 2 == 0) {
switchTexture(false); // Suprimir notificación al activar modo DEMO
}
// 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::LISSAJOUS, 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_ = theme_manager_->getCurrentThemeIndex();
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
theme_manager_->switchToTheme(5); // 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);
png_shape->resetFlipCount(); // Resetear contador de flips
}
}
// Resetear variables de espera de flips
logo_waiting_for_flip_ = false;
logo_target_flip_number_ = 0;
logo_target_flip_percentage_ = 0.0f;
logo_current_flip_count_ = 0;
// 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
theme_manager_->switchToTheme(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::SANDBOX);
} 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->setShapeScreenPosition(pos.x, pos.y);
}
}
}
void Engine::switchTexture(bool show_notification) {
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 notificación con el nombre de la textura (solo si se solicita)
if (show_notification) {
std::string texture_name = texture_names_[current_texture_index_];
std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper);
showNotificationForAction("Sprite: " + texture_name);
}
}
// 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->enableShapeAttraction(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::SANDBOX) {
text_ = "Modo Física";
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 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::LISSAJOUS:
active_shape_ = std::make_unique<LissajousShape>();
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->enableShapeAttraction(true);
}
// Mostrar texto informativo con nombre de figura (solo si NO estamos en modo demo o logo)
if (active_shape_ && current_app_mode_ == AppMode::SANDBOX) {
text_ = std::string("Modo ") + active_shape_->getName();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 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;
// Actualizar target de la pelota para cálculo de convergencia
balls_[i]->setShapeTarget2D(target_x, target_y);
// 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]->applyShapeForce(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();
// Notificar a la figura sobre el porcentaje de convergencia
// Esto permite que PNGShape decida cuándo empezar a contar para flips
active_shape_->setConvergence(shape_convergence_);
}
}
// 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_));
}