Files
vibe3_physics/source/engine.cpp
Sergio Valor c4ca49b006 Añadir parámetros de línea de comandos para resolución
Características:
- Parámetros CLI: -w/--width, -h/--height, -f/--fullscreen
- Help: --help muestra uso y ejemplos
- Validación: mínimo 640x480, con mensajes de error
- Defaults: 1280x720 ventana si no se especifica
- Fullscreen opcional con flag -f

Ejemplos de uso:
  ./vibe3_physics                    # Default 1280x720
  ./vibe3_physics -w 1920 -h 1080    # Personalizado
  ./vibe3_physics -w 1920 -h 1080 -f # Fullscreen
  ./vibe3_physics --help             # Ayuda

Archivos modificados:
- source/main.cpp: Parser de argumentos + printHelp()
- source/engine.h: initialize() acepta parámetros
- source/engine.cpp: Lógica de ventana configurable
- ROADMAP.md: Marcar tarea completada

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 13:28:44 +02:00

1806 lines
70 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 <iostream> // for cout
#include <string> // for string
#include <filesystem> // for path operations
#ifdef _WIN32
#include <windows.h> // for GetModuleFileName
#endif
#include "ball.h" // for Ball
#include "external/dbgtxt.h" // for dbg_init, dbg_print
#include "external/texture.h" // for Texture
#include "shapes/sphere_shape.h" // for SphereShape
#include "shapes/cube_shape.h" // for CubeShape
#include "shapes/helix_shape.h" // for HelixShape
#include "shapes/wave_grid_shape.h" // for WaveGridShape
#include "shapes/torus_shape.h" // for TorusShape
#include "shapes/cylinder_shape.h" // for CylinderShape
#include "shapes/icosahedron_shape.h" // for IcosahedronShape
#include "shapes/atom_shape.h" // for AtomShape
#include "shapes/png_shape.h" // for PNGShape
// Función auxiliar para obtener la ruta del directorio del ejecutable
std::string getExecutableDirectory() {
#ifdef _WIN32
char buffer[MAX_PATH];
GetModuleFileNameA(NULL, buffer, MAX_PATH);
std::filesystem::path exe_path(buffer);
return exe_path.parent_path().string();
#else
// Para Linux/macOS se podría usar readlink("/proc/self/exe") o dladdr
return "."; // Fallback para otros sistemas
#endif
}
// Implementación de métodos públicos
bool Engine::initialize(int width, int height, bool fullscreen) {
bool success = true;
// Usar parámetros o valores por defecto
int window_width = (width > 0) ? width : SCREEN_WIDTH * WINDOW_ZOOM;
int window_height = (height > 0) ? height : SCREEN_HEIGHT * WINDOW_ZOOM;
int logical_width = (width > 0) ? width : SCREEN_WIDTH;
int logical_height = (height > 0) ? height : SCREEN_HEIGHT;
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl;
success = false;
} else {
// 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 exe_dir = getExecutableDirectory();
std::string balls_dir = exe_dir + "/data/balls";
// Buscar todas las texturas PNG en data/balls/
namespace fs = std::filesystem;
if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
std::vector<std::pair<std::string, std::string>> texture_files; // (nombre, path)
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(); // Sin extensión
std::string fullpath = entry.path().string();
texture_files.push_back({filename, fullpath});
}
}
// Ordenar alfabéticamente (normal.png será primero)
std::sort(texture_files.begin(), texture_files.end());
// Cargar texturas en orden (con normal.png primero si existe)
int normal_index = -1;
for (size_t i = 0; i < texture_files.size(); i++) {
if (texture_files[i].first == "normal") {
normal_index = static_cast<int>(i);
break;
}
}
// Poner normal.png primero
if (normal_index > 0) {
std::swap(texture_files[0], texture_files[normal_index]);
}
// Cargar todas las texturas
for (const auto& [name, path] : texture_files) {
textures_.push_back(std::make_shared<Texture>(renderer_, path));
texture_names_.push_back(name);
}
}
// Fallback si no hay texturas (no debería pasar)
if (textures_.empty()) {
std::cerr << "ERROR: No se encontraron texturas en data/balls/" << std::endl;
success = false;
}
// Establecer textura inicial (índice 0 = normal.png)
current_texture_index_ = 0;
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();
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() {
// 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]);
}
}
}
}
void Engine::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&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;
// 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)
case SDLK_D:
demo_mode_enabled_ = !demo_mode_enabled_;
if (demo_mode_enabled_) {
// Desactivar demo lite si estaba activo (mutuamente excluyentes)
demo_lite_enabled_ = false;
// Randomizar TODO al activar
randomizeOnDemoStart(false);
// Inicializar timer con primer intervalo aleatorio
demo_timer_ = 0.0f;
float interval_range = DEMO_ACTION_INTERVAL_MAX - DEMO_ACTION_INTERVAL_MIN;
demo_next_action_time_ = DEMO_ACTION_INTERVAL_MIN + (rand() % 1000) / 1000.0f * interval_range;
// Mostrar texto de activación (usa color del tema)
text_ = "DEMO MODE ON";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
} else {
// Al desactivar: mostrar texto
text_ = "DEMO MODE OFF";
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:
demo_lite_enabled_ = !demo_lite_enabled_;
if (demo_lite_enabled_) {
// Desactivar demo completo si estaba activo (mutuamente excluyentes)
demo_mode_enabled_ = false;
// Randomizar solo física/figura (mantiene escenario y tema)
randomizeOnDemoStart(true);
// Inicializar timer con primer intervalo aleatorio (más rápido)
demo_timer_ = 0.0f;
float interval_range = DEMO_LITE_ACTION_INTERVAL_MAX - DEMO_LITE_ACTION_INTERVAL_MIN;
demo_next_action_time_ = DEMO_LITE_ACTION_INTERVAL_MIN + (rand() % 1000) / 1000.0f * interval_range;
// Mostrar texto de activación (usa color del tema)
text_ = "DEMO LITE ON";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
} else {
// Al desactivar: mostrar texto
text_ = "DEMO LITE 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 tema actual
int theme_idx = static_cast<int>(current_theme_);
const ThemeColors& current = themes_[theme_idx];
// Texto del número de pelotas con color del tema
dbg_print(text_pos_, 8, text_.c_str(), current.text_color_r, current.text_color_g, current.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 actual o destino, para evitar duplicación durante LERP)
const ThemeColors& target = themes_[static_cast<int>(target_theme_)];
if (text_ != current.name_es && text_ != target.name_es) {
int theme_text_width = static_cast<int>(strlen(current.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, current.name_es, current.text_color_r, current.text_color_g, current.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 = 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
}
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 < test_.at(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
ThemeColors &theme = themes_[static_cast<int>(current_theme_)];
int color_index = rand() % theme.ball_colors.size(); // Cantidad variable de colores por tema
const Color 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 (demo_mode_enabled_ || demo_lite_enabled_) return;
int num_balls = test_.at(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() {
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_);
// Ocultar/mostrar cursor según modo fullscreen
if (fullscreen_enabled_) {
SDL_HideCursor();
} else {
SDL_ShowCursor();
}
}
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);
}
// Ocultar cursor en real fullscreen
SDL_HideCursor();
} else {
// Volver a resolución original
current_screen_width_ = SCREEN_WIDTH;
current_screen_height_ = SCREEN_HEIGHT;
// Restaurar ventana normal
SDL_SetWindowFullscreen(window_, false);
SDL_SetWindowSize(window_, SCREEN_WIDTH * WINDOW_ZOOM, SCREEN_HEIGHT * WINDOW_ZOOM);
// Restaurar presentación lógica original
SDL_SetRenderLogicalPresentation(renderer_, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Reinicar la escena con resolución original
initBalls(scenario_);
// Mostrar cursor al salir de real fullscreen
SDL_ShowCursor();
}
}
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_, SCREEN_WIDTH, 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 (transitioning_) {
// 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 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 / SCREEN_WIDTH, (dm->h - WINDOW_DECORATION_HEIGHT) / 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 + (SCREEN_WIDTH * current_window_zoom_) / 2;
int current_center_y = current_y + (SCREEN_HEIGHT * current_window_zoom_) / 2;
// Calcular nuevo tamaño
int new_width = SCREEN_WIDTH * new_zoom;
int new_height = 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}
}
};
}
void Engine::startThemeTransition(ColorTheme new_theme) {
if (new_theme == current_theme_) return; // Ya estamos en ese tema
target_theme_ = new_theme;
transitioning_ = true;
transition_progress_ = 0.0f;
// Mostrar nombre del tema (solo si NO estamos en modo demo)
if (!demo_mode_enabled_ && !demo_lite_enabled_) {
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();
}
}
Color Engine::getInterpolatedColor(size_t ball_index) const {
if (!transitioning_) {
// Sin transición: devolver color actual
return balls_[ball_index]->getColor();
}
// En transición: interpolar entre color actual y color 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<Uint8>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), transition_progress_)),
static_cast<Uint8>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), transition_progress_)),
static_cast<Uint8>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), transition_progress_))
};
}
// Sistema de Modo DEMO (auto-play)
void Engine::updateDemoMode() {
// Verificar si algún modo está activo
bool is_demo_active = demo_mode_enabled_ || demo_lite_enabled_;
if (!is_demo_active) return;
// Actualizar timer
demo_timer_ += delta_time_;
// Si es hora de ejecutar acción
if (demo_timer_ >= demo_next_action_time_) {
// Ejecutar acción según modo activo
bool is_lite = demo_lite_enabled_;
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) {
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%)
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, ShapeType::PNG_SHAPE};
int shape_index = rand() % 9;
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%)
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, ShapeType::PNG_SHAPE};
int shape_index = rand() % 9;
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 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 (!demo_mode_enabled_ && !demo_lite_enabled_) {
// 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_);
} 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)
if (!demo_mode_enabled_ && !demo_lite_enabled_) {
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)
if (active_shape_ && !demo_mode_enabled_ && !demo_lite_enabled_) {
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);
}
}
// 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_));
}