Problema: - Al usar -w/-h, la ventana se creaba correcta - Pero el renderizado interno seguía usando SCREEN_WIDTH/HEIGHT (320x240) - Resultado: ventana grande con área de juego pequeña en esquina Solución: - Añadidas variables base_screen_width/height_ - Guardan resolución configurada por CLI (o default) - current_screen_* ahora se inicializa con valores base - toggleRealFullscreen() restaura a resolución base, no constantes Cambios: - engine.h: Añadir base_screen_width/height_ - engine.cpp: Inicializar con valores CLI - engine.cpp: Usar base_* al salir de fullscreen real Ahora funciona: ./vibe3_physics -w 1920 -h 1080 # Renderiza en 1920x1080 ✅ ./vibe3_physics # Renderiza en 1280x720 ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1812 lines
71 KiB
C++
1812 lines
71 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;
|
|
|
|
// Guardar resolución base (configurada por CLI)
|
|
base_screen_width_ = logical_width;
|
|
base_screen_height_ = logical_height;
|
|
current_screen_width_ = logical_width;
|
|
current_screen_height_ = logical_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 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_ * WINDOW_ZOOM, base_screen_height_ * WINDOW_ZOOM);
|
|
|
|
// Restaurar presentación lógica base
|
|
SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
|
|
|
|
// Reinicar la escena con resolución original
|
|
initBalls(scenario_);
|
|
|
|
// 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 ¤t = 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_, ¤t_x, ¤t_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_));
|
|
} |