Problemas resueltos: - En modo F3 (letterbox), el overlay se centraba en pantalla física en lugar de en el viewport visible, quedando desplazado - Al salir de F3 a ventana, el overlay seguía roto - Padding inferior no se respetaba correctamente Cambios implementados: 1. render() ahora usa SDL_GetRenderViewport() para obtener área visible 2. Centrado calculado dentro del viewport (con offset de barras negras) 3. toggleFullscreen() restaura tamaño de ventana al salir de F3 4. Padding check movido ANTES de escribir línea (>= en lugar de >) 5. Debug logging añadido para diagnóstico de dimensiones Resultado: ✅ Overlay centrado correctamente en F3 letterbox ✅ Overlay se regenera correctamente al salir de F3 ✅ Padding inferior respetado en columna 0 Pendiente: - Columna 2 (índice 1) todavía no respeta padding inferior - Verificar que F4 (real fullscreen) siga funcionando correctamente 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1800 lines
74 KiB
C++
1800 lines
74 KiB
C++
#include "engine.h"
|
|
|
|
#include <SDL3/SDL_error.h> // for SDL_GetError
|
|
#include <SDL3/SDL_events.h> // for SDL_Event, SDL_PollEvent
|
|
#include <SDL3/SDL_init.h> // for SDL_Init, SDL_Quit, SDL_INIT_VIDEO
|
|
#include <SDL3/SDL_keycode.h> // for SDL_Keycode
|
|
#include <SDL3/SDL_render.h> // for SDL_SetRenderDrawColor, SDL_RenderPresent
|
|
#include <SDL3/SDL_timer.h> // for SDL_GetTicks
|
|
#include <SDL3/SDL_video.h> // for SDL_CreateWindow, SDL_DestroyWindow, SDL_GetDisplayBounds
|
|
|
|
#include <algorithm> // for std::min, std::max, std::sort
|
|
#include <cmath> // for sqrtf, acosf, cosf, sinf (funciones matemáticas)
|
|
#include <cstdlib> // for rand, srand
|
|
#include <cstring> // for strlen
|
|
#include <ctime> // for time
|
|
#include <filesystem> // for path operations
|
|
#include <iostream> // for cout
|
|
#include <string> // for string
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h> // for GetModuleFileName
|
|
#endif
|
|
|
|
#include "ball.h" // for Ball
|
|
#include "external/mouse.h" // for Mouse namespace
|
|
#include "external/texture.h" // for Texture
|
|
#include "shapes/atom_shape.h" // for AtomShape
|
|
#include "shapes/cube_shape.h" // for CubeShape
|
|
#include "shapes/cylinder_shape.h" // for CylinderShape
|
|
#include "shapes/helix_shape.h" // for HelixShape
|
|
#include "shapes/icosahedron_shape.h" // for IcosahedronShape
|
|
#include "shapes/lissajous_shape.h" // for LissajousShape
|
|
#include "shapes/png_shape.h" // for PNGShape
|
|
#include "shapes/sphere_shape.h" // for SphereShape
|
|
#include "shapes/torus_shape.h" // for TorusShape
|
|
|
|
// getExecutableDirectory() ya está definido en defines.h como inline
|
|
|
|
// Implementación de métodos públicos
|
|
bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
|
|
bool success = true;
|
|
|
|
// Obtener resolución de pantalla para validación
|
|
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
|
std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
int num_displays = 0;
|
|
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
|
const auto* dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr;
|
|
|
|
int screen_w = dm ? dm->w : 1920; // Fallback si falla
|
|
int screen_h = dm ? dm->h - WINDOW_DECORATION_HEIGHT : 1080;
|
|
|
|
if (displays) SDL_free(displays);
|
|
|
|
// Usar parámetros o valores por defecto
|
|
int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH;
|
|
int logical_height = (height > 0) ? height : DEFAULT_SCREEN_HEIGHT;
|
|
int window_zoom = (zoom > 0) ? zoom : DEFAULT_WINDOW_ZOOM;
|
|
|
|
// VALIDACIÓN 1: Si resolución > pantalla → reset a default
|
|
if (logical_width > screen_w || logical_height > screen_h) {
|
|
std::cout << "Advertencia: Resolución " << logical_width << "x" << logical_height
|
|
<< " excede pantalla " << screen_w << "x" << screen_h
|
|
<< ". Usando default " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << "\n";
|
|
logical_width = DEFAULT_SCREEN_WIDTH;
|
|
logical_height = DEFAULT_SCREEN_HEIGHT;
|
|
window_zoom = DEFAULT_WINDOW_ZOOM; // Reset zoom también
|
|
}
|
|
|
|
// VALIDACIÓN 2: Calcular max_zoom y ajustar si es necesario
|
|
int max_zoom = std::min(screen_w / logical_width, screen_h / logical_height);
|
|
if (window_zoom > max_zoom) {
|
|
std::cout << "Advertencia: Zoom " << window_zoom << " excede máximo " << max_zoom
|
|
<< " para " << logical_width << "x" << logical_height << ". Ajustando a " << max_zoom << "\n";
|
|
window_zoom = max_zoom;
|
|
}
|
|
|
|
// Si se especificaron parámetros CLI y zoom no se especificó, usar zoom=1
|
|
if ((width > 0 || height > 0) && zoom == 0) {
|
|
window_zoom = 1;
|
|
}
|
|
|
|
// Guardar zoom calculado ANTES de crear la ventana (para F1/F2/F3/F4)
|
|
current_window_zoom_ = window_zoom;
|
|
|
|
// Calcular tamaño de ventana
|
|
int window_width = logical_width * window_zoom;
|
|
int window_height = logical_height * window_zoom;
|
|
|
|
// Guardar resolución base (configurada por CLI o default)
|
|
base_screen_width_ = logical_width;
|
|
base_screen_height_ = logical_height;
|
|
current_screen_width_ = logical_width;
|
|
current_screen_height_ = logical_height;
|
|
|
|
// SDL ya inicializado arriba para validación
|
|
{
|
|
// Crear ventana principal (fullscreen si se especifica)
|
|
// NOTA: SDL_WINDOW_HIGH_PIXEL_DENSITY removido por incompatibilidad con STRETCH mode (F4)
|
|
// El DPI se detectará manualmente con SDL_GetWindowSizeInPixels()
|
|
Uint32 window_flags = SDL_WINDOW_OPENGL;
|
|
if (fullscreen) {
|
|
window_flags |= SDL_WINDOW_FULLSCREEN;
|
|
}
|
|
|
|
window_ = SDL_CreateWindow(WINDOW_CAPTION, window_width, window_height, window_flags);
|
|
if (window_ == nullptr) {
|
|
std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << std::endl;
|
|
success = false;
|
|
} else {
|
|
// Centrar ventana en pantalla si no está en fullscreen
|
|
if (!fullscreen) {
|
|
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
|
}
|
|
|
|
// Crear renderizador
|
|
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
|
if (renderer_ == nullptr) {
|
|
std::cout << "¡No se pudo crear el renderizador! Error de SDL: " << SDL_GetError() << std::endl;
|
|
success = false;
|
|
} else {
|
|
// Establecer color inicial del renderizador
|
|
SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);
|
|
|
|
// Establecer tamaño lógico para el renderizado (resolución interna)
|
|
SDL_SetRenderLogicalPresentation(renderer_, logical_width, logical_height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
|
|
|
|
// Configurar V-Sync inicial
|
|
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Inicializar otros componentes si SDL se inicializó correctamente
|
|
if (success) {
|
|
// Cargar todas las texturas disponibles desde data/balls/
|
|
std::string resources_dir = getResourcesDirectory();
|
|
std::string balls_dir = resources_dir + "/data/balls";
|
|
|
|
struct TextureInfo {
|
|
std::string name;
|
|
std::shared_ptr<Texture> texture;
|
|
int width;
|
|
};
|
|
std::vector<TextureInfo> texture_files;
|
|
|
|
// Buscar todas las texturas PNG en data/balls/
|
|
namespace fs = std::filesystem;
|
|
if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
|
|
// Cargar todas las texturas desde disco
|
|
for (const auto& entry : fs::directory_iterator(balls_dir)) {
|
|
if (entry.is_regular_file() && entry.path().extension() == ".png") {
|
|
std::string filename = entry.path().stem().string();
|
|
std::string fullpath = entry.path().string();
|
|
|
|
// Cargar textura y obtener dimensiones
|
|
auto texture = std::make_shared<Texture>(renderer_, fullpath);
|
|
int width = texture->getWidth();
|
|
|
|
texture_files.push_back({filename, texture, width});
|
|
}
|
|
}
|
|
} else {
|
|
// Fallback: cargar texturas desde pack usando la lista del ResourcePack
|
|
if (Texture::isPackLoaded()) {
|
|
auto pack_resources = Texture::getPackResourceList();
|
|
|
|
// Filtrar solo los recursos en balls/ con extensión .png
|
|
for (const auto& resource : pack_resources) {
|
|
if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") {
|
|
std::string tex_name = resource.substr(6); // Quitar "balls/"
|
|
std::string name = tex_name.substr(0, tex_name.find('.')); // Quitar extensión
|
|
|
|
auto texture = std::make_shared<Texture>(renderer_, resource);
|
|
int width = texture->getWidth();
|
|
|
|
texture_files.push_back({name, texture, width});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4)
|
|
std::sort(texture_files.begin(), texture_files.end(), [](const TextureInfo& a, const TextureInfo& b) {
|
|
return a.width > b.width; // Descendente por tamaño
|
|
});
|
|
|
|
// Guardar texturas ya cargadas en orden (0=big, 1=normal, 2=small, 3=tiny)
|
|
for (const auto& info : texture_files) {
|
|
textures_.push_back(info.texture);
|
|
texture_names_.push_back(info.name);
|
|
}
|
|
|
|
// Verificar que se cargaron texturas
|
|
if (textures_.empty()) {
|
|
std::cerr << "ERROR: No se pudieron cargar texturas" << std::endl;
|
|
success = false;
|
|
}
|
|
|
|
// Buscar índice de "normal" para usarlo como textura inicial (debería ser índice 1)
|
|
current_texture_index_ = 0; // Fallback
|
|
for (size_t i = 0; i < texture_names_.size(); i++) {
|
|
if (texture_names_[i] == "normal") {
|
|
current_texture_index_ = i; // Iniciar en "normal" (índice 1)
|
|
break;
|
|
}
|
|
}
|
|
texture_ = textures_[current_texture_index_];
|
|
current_ball_size_ = texture_->getWidth(); // Obtener tamaño dinámicamente
|
|
|
|
srand(static_cast<unsigned>(time(nullptr)));
|
|
|
|
// Inicializar InputHandler (sin estado)
|
|
input_handler_ = std::make_unique<InputHandler>();
|
|
|
|
// Inicializar ThemeManager PRIMERO (requerido por Notifier y SceneManager)
|
|
theme_manager_ = std::make_unique<ThemeManager>();
|
|
theme_manager_->initialize();
|
|
|
|
// Inicializar SceneManager (gestión de bolas y física)
|
|
scene_manager_ = std::make_unique<SceneManager>(current_screen_width_, current_screen_height_);
|
|
scene_manager_->initialize(0, texture_, theme_manager_.get()); // Escenario 0 (10 bolas) por defecto
|
|
|
|
// Calcular tamaño físico de ventana ANTES de inicializar UIManager
|
|
// NOTA: No llamar a updatePhysicalWindowSize() aquí porque ui_manager_ aún no existe
|
|
// Calcular manualmente para poder pasar valores al constructor de UIManager
|
|
int window_w = 0, window_h = 0;
|
|
SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
|
|
physical_window_width_ = window_w;
|
|
physical_window_height_ = window_h;
|
|
|
|
// Inicializar UIManager (HUD, FPS, notificaciones)
|
|
// NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager
|
|
ui_manager_ = std::make_unique<UIManager>();
|
|
ui_manager_->initialize(renderer_, theme_manager_.get(), physical_window_width_, physical_window_height_);
|
|
|
|
// Inicializar ShapeManager (gestión de figuras 3D)
|
|
shape_manager_ = std::make_unique<ShapeManager>();
|
|
shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), nullptr,
|
|
current_screen_width_, current_screen_height_);
|
|
|
|
// Inicializar StateManager (gestión de estados DEMO/LOGO)
|
|
state_manager_ = std::make_unique<StateManager>();
|
|
state_manager_->initialize(this); // Callback al Engine
|
|
|
|
// Actualizar ShapeManager con StateManager (dependencia circular - StateManager debe existir primero)
|
|
shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(),
|
|
current_screen_width_, current_screen_height_);
|
|
|
|
// Inicializar BoidManager (gestión de comportamiento de enjambre)
|
|
boid_manager_ = std::make_unique<BoidManager>();
|
|
boid_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(),
|
|
current_screen_width_, current_screen_height_);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void Engine::run() {
|
|
while (!should_exit_) {
|
|
calculateDeltaTime();
|
|
|
|
// Procesar eventos de entrada (teclado, ratón, ventana)
|
|
if (input_handler_->processEvents(*this)) {
|
|
should_exit_ = true;
|
|
}
|
|
|
|
update();
|
|
render();
|
|
}
|
|
}
|
|
|
|
void Engine::shutdown() {
|
|
// Limpiar recursos SDL
|
|
if (renderer_) {
|
|
SDL_DestroyRenderer(renderer_);
|
|
renderer_ = nullptr;
|
|
}
|
|
if (window_) {
|
|
SDL_DestroyWindow(window_);
|
|
window_ = nullptr;
|
|
}
|
|
SDL_Quit();
|
|
}
|
|
|
|
// Métodos privados - esqueleto básico por ahora
|
|
void Engine::calculateDeltaTime() {
|
|
Uint64 current_time = SDL_GetTicks();
|
|
|
|
// En el primer frame, inicializar el tiempo anterior
|
|
if (last_frame_time_ == 0) {
|
|
last_frame_time_ = current_time;
|
|
delta_time_ = 1.0f / 60.0f; // Asumir 60 FPS para el primer frame
|
|
return;
|
|
}
|
|
|
|
// Calcular delta time en segundos
|
|
delta_time_ = (current_time - last_frame_time_) / 1000.0f;
|
|
last_frame_time_ = current_time;
|
|
|
|
// Limitar delta time para evitar saltos grandes (pausa larga, depuración, etc.)
|
|
if (delta_time_ > 0.05f) { // Máximo 50ms (20 FPS mínimo)
|
|
delta_time_ = 1.0f / 60.0f; // Fallback a 60 FPS
|
|
}
|
|
}
|
|
|
|
void Engine::update() {
|
|
// Actualizar visibilidad del cursor (auto-ocultar tras inactividad)
|
|
Mouse::updateCursorVisibility();
|
|
|
|
// Obtener tiempo actual
|
|
Uint64 current_time = SDL_GetTicks();
|
|
|
|
// Actualizar UI (FPS, notificaciones, texto obsoleto) - delegado a UIManager
|
|
ui_manager_->update(current_time, delta_time_);
|
|
|
|
// Bifurcar actualización según modo activo
|
|
if (current_mode_ == SimulationMode::PHYSICS) {
|
|
// Modo física normal: actualizar física de cada pelota (delegado a SceneManager)
|
|
scene_manager_->update(delta_time_);
|
|
} else if (current_mode_ == SimulationMode::SHAPE) {
|
|
// Modo Figura 3D: actualizar figura polimórfica
|
|
updateShape();
|
|
} else if (current_mode_ == SimulationMode::BOIDS) {
|
|
// Modo Boids: actualizar comportamiento de enjambre (delegado a BoidManager)
|
|
boid_manager_->update(delta_time_);
|
|
}
|
|
|
|
// Actualizar Modo DEMO/LOGO (delegado a StateManager)
|
|
state_manager_->update(delta_time_, shape_convergence_, active_shape_.get());
|
|
|
|
// Actualizar transiciones de temas (delegado a ThemeManager)
|
|
theme_manager_->update(delta_time_);
|
|
}
|
|
|
|
// === IMPLEMENTACIÓN DE MÉTODOS PÚBLICOS PARA INPUT HANDLER ===
|
|
|
|
// Gravedad y física
|
|
void Engine::handleGravityToggle() {
|
|
// Si estamos en modo boids, salir a modo física CON GRAVEDAD OFF
|
|
// Según RULES.md: "BOIDS a PHYSICS: Pulsando la tecla G: Gravedad OFF"
|
|
if (current_mode_ == SimulationMode::BOIDS) {
|
|
toggleBoidsMode(); // Cambiar a PHYSICS (preserva inercia, gravedad ya está OFF desde activateBoids)
|
|
// NO llamar a forceBallsGravityOff() porque aplica impulsos que destruyen la inercia de BOIDS
|
|
// La gravedad ya está desactivada por BoidManager::activateBoids() y se mantiene al salir
|
|
showNotificationForAction("Modo Física - Gravedad Off");
|
|
return;
|
|
}
|
|
|
|
// Si estamos en modo figura, salir a modo física SIN GRAVEDAD
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
toggleShapeModeInternal(false); // Desactivar figura sin forzar gravedad ON
|
|
showNotificationForAction("Gravedad Off");
|
|
} else {
|
|
scene_manager_->switchBallsGravity(); // Toggle normal en modo física
|
|
// Determinar estado actual de gravedad (gravity_force_ != 0.0f significa ON)
|
|
const Ball* first_ball = scene_manager_->getFirstBall();
|
|
bool gravity_on = (first_ball == nullptr) ? true : (first_ball->getGravityForce() != 0.0f);
|
|
showNotificationForAction(gravity_on ? "Gravedad On" : "Gravedad Off");
|
|
}
|
|
}
|
|
|
|
void Engine::handleGravityDirectionChange(GravityDirection direction, const char* notification_text) {
|
|
// Si estamos en modo boids, salir a modo física primero
|
|
if (current_mode_ == SimulationMode::BOIDS) {
|
|
toggleBoidsMode(); // Esto cambia a PHYSICS y activa gravedad
|
|
// Continuar para aplicar la dirección de gravedad
|
|
}
|
|
|
|
// Si estamos en modo figura, salir a modo física CON gravedad
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
toggleShapeModeInternal(); // Desactivar figura (activa gravedad automáticamente)
|
|
} else {
|
|
scene_manager_->enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
|
|
}
|
|
scene_manager_->changeGravityDirection(direction);
|
|
showNotificationForAction(notification_text);
|
|
}
|
|
|
|
// Display y depuración
|
|
void Engine::toggleDebug() {
|
|
ui_manager_->toggleDebug();
|
|
}
|
|
|
|
void Engine::toggleHelp() {
|
|
ui_manager_->toggleHelp();
|
|
}
|
|
|
|
// Figuras 3D
|
|
void Engine::toggleShapeMode() {
|
|
toggleShapeModeInternal();
|
|
// Mostrar notificación según el modo actual después del toggle
|
|
if (current_mode_ == SimulationMode::PHYSICS) {
|
|
showNotificationForAction("Modo Física");
|
|
} else {
|
|
// Mostrar nombre de la figura actual (orden debe coincidir con enum ShapeType)
|
|
// Índices: 0=NONE, 1=SPHERE, 2=CUBE, 3=HELIX, 4=TORUS, 5=LISSAJOUS, 6=CYLINDER, 7=ICOSAHEDRON, 8=ATOM, 9=PNG_SHAPE
|
|
const char* shape_names[] = {"Ninguna", "Esfera", "Cubo", "Hélice", "Toroide", "Lissajous", "Cilindro", "Icosaedro", "Átomo", "Forma PNG"};
|
|
showNotificationForAction(shape_names[static_cast<int>(current_shape_type_)]);
|
|
}
|
|
}
|
|
|
|
void Engine::activateShape(ShapeType type, const char* notification_text) {
|
|
activateShapeInternal(type);
|
|
showNotificationForAction(notification_text);
|
|
}
|
|
|
|
void Engine::handleShapeScaleChange(bool increase) {
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
if (increase) {
|
|
shape_scale_factor_ += SHAPE_SCALE_STEP;
|
|
} else {
|
|
shape_scale_factor_ -= SHAPE_SCALE_STEP;
|
|
}
|
|
clampShapeScale();
|
|
showNotificationForAction("Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%");
|
|
}
|
|
}
|
|
|
|
void Engine::resetShapeScale() {
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
|
|
showNotificationForAction("Escala 100%");
|
|
}
|
|
}
|
|
|
|
void Engine::toggleDepthZoom() {
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
depth_zoom_enabled_ = !depth_zoom_enabled_;
|
|
showNotificationForAction(depth_zoom_enabled_ ? "Profundidad On" : "Profundidad Off");
|
|
}
|
|
}
|
|
|
|
// Boids (comportamiento de enjambre)
|
|
void Engine::toggleBoidsMode() {
|
|
if (current_mode_ == SimulationMode::BOIDS) {
|
|
// Salir del modo boids
|
|
current_mode_ = SimulationMode::PHYSICS;
|
|
boid_manager_->deactivateBoids();
|
|
} else {
|
|
// Entrar al modo boids (desde PHYSICS o SHAPE)
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
// Si estamos en modo shape, salir primero sin forzar gravedad
|
|
current_mode_ = SimulationMode::PHYSICS;
|
|
|
|
// Desactivar atracción de figuras
|
|
auto& balls = scene_manager_->getBallsMutable();
|
|
for (auto& ball : balls) {
|
|
ball->enableShapeAttraction(false);
|
|
ball->setDepthScale(1.0f);
|
|
}
|
|
}
|
|
|
|
// Activar modo boids
|
|
current_mode_ = SimulationMode::BOIDS;
|
|
boid_manager_->activateBoids();
|
|
}
|
|
}
|
|
|
|
// Temas de colores
|
|
void Engine::cycleTheme(bool forward) {
|
|
if (forward) {
|
|
theme_manager_->cycleTheme();
|
|
} else {
|
|
theme_manager_->cyclePrevTheme();
|
|
}
|
|
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
|
|
}
|
|
|
|
void Engine::switchThemeByNumpad(int numpad_key) {
|
|
// Mapear tecla numpad a índice de tema según página actual
|
|
int theme_index = -1;
|
|
|
|
if (theme_page_ == 0) {
|
|
// Página 0: Temas 0-9 (estáticos + SUNRISE)
|
|
if (numpad_key >= 0 && numpad_key <= 9) {
|
|
theme_index = (numpad_key == 0) ? 9 : (numpad_key - 1);
|
|
}
|
|
} else {
|
|
// Página 1: Temas 10-14 (dinámicos)
|
|
if (numpad_key >= 1 && numpad_key <= 5) {
|
|
theme_index = 9 + numpad_key;
|
|
}
|
|
}
|
|
|
|
if (theme_index != -1) {
|
|
theme_manager_->switchToTheme(theme_index);
|
|
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
|
|
}
|
|
}
|
|
|
|
void Engine::toggleThemePage() {
|
|
theme_page_ = (theme_page_ == 0) ? 1 : 0;
|
|
showNotificationForAction((theme_page_ == 0) ? "Página 1" : "Página 2");
|
|
}
|
|
|
|
void Engine::pauseDynamicTheme() {
|
|
theme_manager_->pauseDynamic();
|
|
}
|
|
|
|
// Sprites/Texturas
|
|
void Engine::switchTexture() {
|
|
switchTextureInternal(true); // Mostrar notificación en modo manual
|
|
}
|
|
|
|
// Escenarios (número de pelotas)
|
|
void Engine::changeScenario(int scenario_id, const char* notification_text) {
|
|
// Pasar el modo actual al SceneManager para inicialización correcta
|
|
scene_manager_->changeScenario(scenario_id, current_mode_);
|
|
|
|
// Si estamos en modo SHAPE, regenerar la figura con nuevo número de pelotas
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
generateShape();
|
|
|
|
// Activar atracción física en las bolas nuevas (crítico tras changeScenario)
|
|
auto& balls = scene_manager_->getBallsMutable();
|
|
for (auto& ball : balls) {
|
|
ball->enableShapeAttraction(true);
|
|
}
|
|
}
|
|
|
|
showNotificationForAction(notification_text);
|
|
}
|
|
|
|
// Zoom y fullscreen
|
|
void Engine::handleZoomIn() {
|
|
if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
|
|
zoomIn();
|
|
}
|
|
}
|
|
|
|
void Engine::handleZoomOut() {
|
|
if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
|
|
zoomOut();
|
|
}
|
|
}
|
|
|
|
// Modos de aplicación (DEMO/LOGO) - Delegados a StateManager
|
|
void Engine::toggleDemoMode() {
|
|
AppMode prev_mode = state_manager_->getCurrentMode();
|
|
state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_);
|
|
AppMode new_mode = state_manager_->getCurrentMode();
|
|
|
|
// Mostrar notificación según el modo resultante
|
|
if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
|
|
showNotificationForAction("MODO SANDBOX");
|
|
} else if (new_mode == AppMode::DEMO && prev_mode != AppMode::DEMO) {
|
|
showNotificationForAction("MODO DEMO");
|
|
}
|
|
}
|
|
|
|
void Engine::toggleDemoLiteMode() {
|
|
AppMode prev_mode = state_manager_->getCurrentMode();
|
|
state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_);
|
|
AppMode new_mode = state_manager_->getCurrentMode();
|
|
|
|
// Mostrar notificación según el modo resultante
|
|
if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
|
|
showNotificationForAction("MODO SANDBOX");
|
|
} else if (new_mode == AppMode::DEMO_LITE && prev_mode != AppMode::DEMO_LITE) {
|
|
showNotificationForAction("MODO DEMO LITE");
|
|
}
|
|
}
|
|
|
|
void Engine::toggleLogoMode() {
|
|
AppMode prev_mode = state_manager_->getCurrentMode();
|
|
state_manager_->toggleLogoMode(current_screen_width_, current_screen_height_, scene_manager_->getBallCount());
|
|
AppMode new_mode = state_manager_->getCurrentMode();
|
|
|
|
// Mostrar notificación según el modo resultante
|
|
if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
|
|
showNotificationForAction("MODO SANDBOX");
|
|
} else if (new_mode == AppMode::LOGO && prev_mode != AppMode::LOGO) {
|
|
showNotificationForAction("MODO LOGO");
|
|
}
|
|
}
|
|
|
|
void Engine::render() {
|
|
// Limpiar framebuffer completamente (evita artefactos en barras negras al cambiar modos)
|
|
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); // Negro para barras de letterbox/integer
|
|
SDL_RenderClear(renderer_);
|
|
|
|
// Renderizar fondo degradado (delegado a ThemeManager)
|
|
{
|
|
float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b;
|
|
theme_manager_->getBackgroundColors(top_r, top_g, top_b, bottom_r, bottom_g, bottom_b);
|
|
|
|
// Crear quad de pantalla completa con degradado
|
|
SDL_Vertex bg_vertices[4];
|
|
|
|
// Vértice superior izquierdo
|
|
bg_vertices[0].position = {0, 0};
|
|
bg_vertices[0].tex_coord = {0.0f, 0.0f};
|
|
bg_vertices[0].color = {top_r, top_g, top_b, 1.0f};
|
|
|
|
// Vértice superior derecho
|
|
bg_vertices[1].position = {static_cast<float>(current_screen_width_), 0};
|
|
bg_vertices[1].tex_coord = {1.0f, 0.0f};
|
|
bg_vertices[1].color = {top_r, top_g, top_b, 1.0f};
|
|
|
|
// Vértice inferior derecho
|
|
bg_vertices[2].position = {static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_)};
|
|
bg_vertices[2].tex_coord = {1.0f, 1.0f};
|
|
bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f};
|
|
|
|
// Vértice inferior izquierdo
|
|
bg_vertices[3].position = {0, static_cast<float>(current_screen_height_)};
|
|
bg_vertices[3].tex_coord = {0.0f, 1.0f};
|
|
bg_vertices[3].color = {bottom_r, bottom_g, bottom_b, 1.0f};
|
|
|
|
// Índices para 2 triángulos
|
|
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
|
|
|
|
// Renderizar sin textura (nullptr)
|
|
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
|
|
}
|
|
|
|
// Limpiar batches del frame anterior
|
|
batch_vertices_.clear();
|
|
batch_indices_.clear();
|
|
|
|
// Obtener referencia a las bolas desde SceneManager
|
|
const auto& balls = scene_manager_->getBalls();
|
|
|
|
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(), [&balls](size_t a, size_t b) {
|
|
return balls[a]->getDepthBrightness() < balls[b]->getDepthBrightness();
|
|
});
|
|
|
|
// Renderizar en orden de profundidad (fondo → frente)
|
|
for (size_t idx : render_order) {
|
|
SDL_FRect pos = balls[idx]->getPosition();
|
|
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
|
|
float brightness = balls[idx]->getDepthBrightness();
|
|
float depth_scale = balls[idx]->getDepthScale();
|
|
|
|
// Mapear brightness de 0-1 a rango MIN-MAX
|
|
float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
|
|
|
|
// Aplicar factor de brillo al color
|
|
int r_mod = static_cast<int>(color.r * brightness_factor);
|
|
int g_mod = static_cast<int>(color.g * brightness_factor);
|
|
int b_mod = static_cast<int>(color.b * brightness_factor);
|
|
|
|
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, r_mod, g_mod, b_mod, depth_scale);
|
|
}
|
|
} else {
|
|
// MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad)
|
|
const auto& balls = scene_manager_->getBalls();
|
|
size_t idx = 0;
|
|
for (auto& ball : balls) {
|
|
SDL_FRect pos = ball->getPosition();
|
|
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
|
|
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f);
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
// Renderizar todas las bolas en una sola llamada
|
|
if (!batch_vertices_.empty()) {
|
|
SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast<int>(batch_vertices_.size()), batch_indices_.data(), static_cast<int>(batch_indices_.size()));
|
|
}
|
|
|
|
// SISTEMA DE TEXTO ANTIGUO DESHABILITADO
|
|
// Reemplazado completamente por el sistema de notificaciones (Notifier)
|
|
// El doble renderizado causaba que aparecieran textos duplicados detrás de las notificaciones
|
|
/*
|
|
if (show_text_) {
|
|
// Obtener datos del tema actual (delegado a ThemeManager)
|
|
int text_color_r, text_color_g, text_color_b;
|
|
theme_manager_->getCurrentThemeTextColor(text_color_r, text_color_g, text_color_b);
|
|
const char* theme_name_es = theme_manager_->getCurrentThemeNameES();
|
|
|
|
// Calcular espaciado dinámico
|
|
int line_height = text_renderer_.getTextHeight();
|
|
int margin = 8;
|
|
|
|
// Texto del número de pelotas con color del tema
|
|
text_renderer_.printPhysical(text_pos_, margin, text_.c_str(), text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
|
|
|
|
// Mostrar nombre del tema en castellano debajo del número de pelotas
|
|
// (solo si text_ NO es ya el nombre del tema, para evitar duplicación)
|
|
if (theme_name_es != nullptr && text_ != theme_name_es) {
|
|
int theme_text_width = text_renderer_.getTextWidth(theme_name_es);
|
|
int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente
|
|
int theme_y = margin + line_height; // Espaciado dinámico
|
|
|
|
// Texto del nombre del tema con el mismo color
|
|
text_renderer_.printPhysical(theme_x, theme_y, theme_name_es, text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
|
|
}
|
|
}
|
|
*/
|
|
|
|
// Renderizar UI (debug HUD, texto obsoleto, notificaciones) - delegado a UIManager
|
|
ui_manager_->render(renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(),
|
|
active_shape_.get(), shape_convergence_,
|
|
physical_window_width_, physical_window_height_, current_screen_width_);
|
|
|
|
SDL_RenderPresent(renderer_);
|
|
}
|
|
|
|
void Engine::showNotificationForAction(const std::string& text) {
|
|
// IMPORTANTE: Esta función solo se llama desde handlers de teclado (acciones manuales)
|
|
// NUNCA se llama desde código automático (DEMO/LOGO), por lo tanto siempre mostramos notificación
|
|
|
|
// Delegar a UIManager
|
|
ui_manager_->showNotification(text, NOTIFICATION_DURATION);
|
|
}
|
|
|
|
void Engine::pushBallsAwayFromGravity() {
|
|
scene_manager_->pushBallsAwayFromGravity();
|
|
}
|
|
|
|
void Engine::toggleVSync() {
|
|
vsync_enabled_ = !vsync_enabled_;
|
|
|
|
// Actualizar texto en UIManager
|
|
ui_manager_->updateVSyncText(vsync_enabled_);
|
|
|
|
// 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_);
|
|
|
|
// Si acabamos de salir de fullscreen, restaurar tamaño de ventana
|
|
if (!fullscreen_enabled_) {
|
|
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
|
|
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
|
}
|
|
|
|
// Actualizar dimensiones físicas después del cambio
|
|
updatePhysicalWindowSize();
|
|
}
|
|
|
|
void Engine::toggleRealFullscreen() {
|
|
// Si está en modo fullscreen normal, primero desactivarlo
|
|
if (fullscreen_enabled_) {
|
|
fullscreen_enabled_ = false;
|
|
SDL_SetWindowFullscreen(window_, false);
|
|
}
|
|
|
|
real_fullscreen_enabled_ = !real_fullscreen_enabled_;
|
|
|
|
if (real_fullscreen_enabled_) {
|
|
// Obtener resolución del escritorio
|
|
int num_displays = 0;
|
|
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
|
if (displays != nullptr && num_displays > 0) {
|
|
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
|
|
if (dm != nullptr) {
|
|
// Cambiar a resolución nativa del escritorio
|
|
current_screen_width_ = dm->w;
|
|
current_screen_height_ = dm->h;
|
|
|
|
// Recrear ventana con nueva resolución
|
|
SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_);
|
|
SDL_SetWindowFullscreen(window_, true);
|
|
|
|
// Actualizar presentación lógica del renderizador
|
|
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
|
|
|
|
// Actualizar tamaño físico de ventana y fuentes
|
|
updatePhysicalWindowSize();
|
|
|
|
// Reinicar la escena con nueva resolución
|
|
scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
|
|
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
|
|
|
|
// Actualizar tamaño de pantalla para boids (wrapping boundaries)
|
|
boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
|
|
}
|
|
SDL_free(displays);
|
|
}
|
|
} else {
|
|
// Volver a resolución base (configurada por CLI o default)
|
|
current_screen_width_ = base_screen_width_;
|
|
current_screen_height_ = base_screen_height_;
|
|
|
|
// Restaurar ventana normal con el zoom actual (no hardcoded)
|
|
SDL_SetWindowFullscreen(window_, false);
|
|
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
|
|
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
|
|
|
// Restaurar presentación lógica base
|
|
SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
|
|
|
|
// Actualizar tamaño físico de ventana y fuentes
|
|
updatePhysicalWindowSize();
|
|
|
|
// Reinicar la escena con resolución original
|
|
scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
|
|
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
|
|
}
|
|
}
|
|
|
|
void Engine::toggleIntegerScaling() {
|
|
// Solo permitir cambio si estamos en modo fullscreen normal (F3)
|
|
if (!fullscreen_enabled_) {
|
|
return; // No hacer nada si no estamos en fullscreen
|
|
}
|
|
|
|
// Ciclar entre los 3 modos: INTEGER → LETTERBOX → STRETCH → INTEGER
|
|
switch (current_scaling_mode_) {
|
|
case ScalingMode::INTEGER:
|
|
current_scaling_mode_ = ScalingMode::LETTERBOX;
|
|
break;
|
|
case ScalingMode::LETTERBOX:
|
|
current_scaling_mode_ = ScalingMode::STRETCH;
|
|
break;
|
|
case ScalingMode::STRETCH:
|
|
current_scaling_mode_ = ScalingMode::INTEGER;
|
|
break;
|
|
}
|
|
|
|
// Aplicar el nuevo modo de escalado
|
|
SDL_RendererLogicalPresentation presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
|
|
const char* mode_name = "INTEGER";
|
|
|
|
switch (current_scaling_mode_) {
|
|
case ScalingMode::INTEGER:
|
|
presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
|
|
mode_name = "INTEGER";
|
|
break;
|
|
case ScalingMode::LETTERBOX:
|
|
presentation = SDL_LOGICAL_PRESENTATION_LETTERBOX;
|
|
mode_name = "LETTERBOX";
|
|
break;
|
|
case ScalingMode::STRETCH:
|
|
presentation = SDL_LOGICAL_PRESENTATION_STRETCH;
|
|
mode_name = "STRETCH";
|
|
break;
|
|
}
|
|
|
|
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, presentation);
|
|
|
|
// Mostrar notificación del cambio
|
|
std::string notification = std::string("Escalado: ") + mode_name;
|
|
ui_manager_->showNotification(notification);
|
|
}
|
|
|
|
void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
|
|
int vertex_index = static_cast<int>(batch_vertices_.size());
|
|
|
|
// Crear 4 vértices para el quad (2 triángulos)
|
|
SDL_Vertex vertices[4];
|
|
|
|
// Convertir colores de int (0-255) a float (0.0-1.0)
|
|
float rf = r / 255.0f;
|
|
float gf = g / 255.0f;
|
|
float bf = b / 255.0f;
|
|
|
|
// Aplicar escala al tamaño (centrado en el punto x, y)
|
|
float scaled_w = w * scale;
|
|
float scaled_h = h * scale;
|
|
float offset_x = (w - scaled_w) / 2.0f; // Offset para centrar
|
|
float offset_y = (h - scaled_h) / 2.0f;
|
|
|
|
// Vértice superior izquierdo
|
|
vertices[0].position = {x + offset_x, y + offset_y};
|
|
vertices[0].tex_coord = {0.0f, 0.0f};
|
|
vertices[0].color = {rf, gf, bf, 1.0f};
|
|
|
|
// Vértice superior derecho
|
|
vertices[1].position = {x + offset_x + scaled_w, y + offset_y};
|
|
vertices[1].tex_coord = {1.0f, 0.0f};
|
|
vertices[1].color = {rf, gf, bf, 1.0f};
|
|
|
|
// Vértice inferior derecho
|
|
vertices[2].position = {x + offset_x + scaled_w, y + offset_y + scaled_h};
|
|
vertices[2].tex_coord = {1.0f, 1.0f};
|
|
vertices[2].color = {rf, gf, bf, 1.0f};
|
|
|
|
// Vértice inferior izquierdo
|
|
vertices[3].position = {x + offset_x, y + offset_y + scaled_h};
|
|
vertices[3].tex_coord = {0.0f, 1.0f};
|
|
vertices[3].color = {rf, gf, bf, 1.0f};
|
|
|
|
// Añadir vértices al batch
|
|
for (int i = 0; i < 4; i++) {
|
|
batch_vertices_.push_back(vertices[i]);
|
|
}
|
|
|
|
// Añadir índices para 2 triángulos
|
|
batch_indices_.push_back(vertex_index + 0);
|
|
batch_indices_.push_back(vertex_index + 1);
|
|
batch_indices_.push_back(vertex_index + 2);
|
|
batch_indices_.push_back(vertex_index + 2);
|
|
batch_indices_.push_back(vertex_index + 3);
|
|
batch_indices_.push_back(vertex_index + 0);
|
|
}
|
|
|
|
// Sistema de zoom dinámico
|
|
int Engine::calculateMaxWindowZoom() const {
|
|
// Obtener información del display usando el método de Coffee Crisis
|
|
int num_displays = 0;
|
|
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
|
if (displays == nullptr || num_displays == 0) {
|
|
return WINDOW_ZOOM_MIN; // Fallback si no se puede obtener
|
|
}
|
|
|
|
// Obtener el modo de display actual
|
|
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
|
|
if (dm == nullptr) {
|
|
SDL_free(displays);
|
|
return WINDOW_ZOOM_MIN;
|
|
}
|
|
|
|
// Calcular zoom máximo usando la fórmula de Coffee Crisis
|
|
const int MAX_ZOOM = std::min(dm->w / base_screen_width_, (dm->h - WINDOW_DECORATION_HEIGHT) / base_screen_height_);
|
|
|
|
SDL_free(displays);
|
|
|
|
// Aplicar límites
|
|
return std::max(WINDOW_ZOOM_MIN, std::min(MAX_ZOOM, WINDOW_ZOOM_MAX));
|
|
}
|
|
|
|
void Engine::setWindowZoom(int new_zoom) {
|
|
// Validar zoom
|
|
int max_zoom = calculateMaxWindowZoom();
|
|
new_zoom = std::max(WINDOW_ZOOM_MIN, std::min(new_zoom, max_zoom));
|
|
|
|
if (new_zoom == current_window_zoom_) {
|
|
return; // No hay cambio
|
|
}
|
|
|
|
// Obtener posición actual del centro de la ventana
|
|
int current_x, current_y;
|
|
SDL_GetWindowPosition(window_, ¤t_x, ¤t_y);
|
|
int current_center_x = current_x + (base_screen_width_ * current_window_zoom_) / 2;
|
|
int current_center_y = current_y + (base_screen_height_ * current_window_zoom_) / 2;
|
|
|
|
// Calcular nuevo tamaño
|
|
int new_width = base_screen_width_ * new_zoom;
|
|
int new_height = base_screen_height_ * new_zoom;
|
|
|
|
// Calcular nueva posición (centrada en el punto actual)
|
|
int new_x = current_center_x - new_width / 2;
|
|
int new_y = current_center_y - new_height / 2;
|
|
|
|
// Obtener límites del escritorio para no salirse
|
|
SDL_Rect display_bounds;
|
|
if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &display_bounds) == 0) {
|
|
// Aplicar márgenes
|
|
int min_x = WINDOW_DESKTOP_MARGIN;
|
|
int min_y = WINDOW_DESKTOP_MARGIN;
|
|
int max_x = display_bounds.w - new_width - WINDOW_DESKTOP_MARGIN;
|
|
int max_y = display_bounds.h - new_height - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT;
|
|
|
|
// Limitar posición
|
|
new_x = std::max(min_x, std::min(new_x, max_x));
|
|
new_y = std::max(min_y, std::min(new_y, max_y));
|
|
}
|
|
|
|
// Aplicar cambios
|
|
SDL_SetWindowSize(window_, new_width, new_height);
|
|
SDL_SetWindowPosition(window_, new_x, new_y);
|
|
current_window_zoom_ = new_zoom;
|
|
|
|
// Actualizar tamaño físico de ventana y fuentes
|
|
updatePhysicalWindowSize();
|
|
}
|
|
|
|
void Engine::zoomIn() {
|
|
setWindowZoom(current_window_zoom_ + 1);
|
|
}
|
|
|
|
void Engine::zoomOut() {
|
|
setWindowZoom(current_window_zoom_ - 1);
|
|
}
|
|
|
|
void Engine::updatePhysicalWindowSize() {
|
|
if (real_fullscreen_enabled_) {
|
|
// En fullscreen real (F4), usar resolución del display
|
|
physical_window_width_ = current_screen_width_;
|
|
physical_window_height_ = current_screen_height_;
|
|
} else if (fullscreen_enabled_) {
|
|
// En fullscreen F3, obtener tamaño REAL del display (no del framebuffer lógico)
|
|
int num_displays = 0;
|
|
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
|
if (displays != nullptr && num_displays > 0) {
|
|
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
|
|
if (dm != nullptr) {
|
|
physical_window_width_ = dm->w;
|
|
physical_window_height_ = dm->h;
|
|
}
|
|
SDL_free(displays);
|
|
}
|
|
} else {
|
|
// En modo ventana, obtener tamaño FÍSICO real del framebuffer
|
|
int window_w = 0, window_h = 0;
|
|
SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
|
|
physical_window_width_ = window_w;
|
|
physical_window_height_ = window_h;
|
|
}
|
|
|
|
// Notificar a UIManager del cambio de tamaño (delegado)
|
|
ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_);
|
|
}
|
|
|
|
// ============================================================================
|
|
// CALLBACKS PARA STATEMANAGER
|
|
// ============================================================================
|
|
// StateManager coordina los estados y timers, Engine proporciona implementación
|
|
// Estos callbacks permiten que StateManager ejecute acciones complejas que
|
|
// requieren acceso a múltiples componentes (SceneManager, ThemeManager, etc.)
|
|
// Este enfoque es pragmático y mantiene la separación de responsabilidades
|
|
|
|
// Callback para ejecutar acciones de LOGO MODE (máquina de estados compleja)
|
|
void Engine::performLogoAction(bool logo_waiting_for_flip) {
|
|
// Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO)
|
|
if (state_manager_->getCurrentMode() == AppMode::SANDBOX) return;
|
|
|
|
// Actualizar timer
|
|
demo_timer_ += delta_time_;
|
|
|
|
// Determinar si es hora de ejecutar acción (depende del modo)
|
|
bool should_trigger = false;
|
|
|
|
if (state_manager_->getCurrentMode() == AppMode::LOGO) {
|
|
// LOGO MODE: Dos caminos posibles
|
|
if (logo_waiting_for_flip_) {
|
|
// CAMINO B: Esperando a que ocurran flips
|
|
// Obtener referencia a PNGShape si está activa
|
|
PNGShape* png_shape = nullptr;
|
|
if (active_shape_ && current_mode_ == SimulationMode::SHAPE) {
|
|
png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
|
|
}
|
|
|
|
if (png_shape) {
|
|
int current_flip_count = png_shape->getFlipCount();
|
|
|
|
// Detectar nuevo flip completado
|
|
if (current_flip_count > logo_current_flip_count_) {
|
|
logo_current_flip_count_ = current_flip_count;
|
|
}
|
|
|
|
// Si estamos EN o DESPUÉS del flip objetivo
|
|
// +1 porque queremos actuar DURANTE el flip N, no después de completarlo
|
|
if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
|
|
// Monitorear progreso del flip actual
|
|
if (png_shape->isFlipping()) {
|
|
float flip_progress = png_shape->getFlipProgress();
|
|
if (flip_progress >= logo_target_flip_percentage_) {
|
|
should_trigger = true; // ¡Trigger durante el flip!
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// CAMINO A: Esperar convergencia + tiempo (comportamiento original)
|
|
bool min_time_reached = demo_timer_ >= logo_min_time_;
|
|
bool max_time_reached = demo_timer_ >= logo_max_time_;
|
|
bool convergence_ok = shape_convergence_ >= logo_convergence_threshold_;
|
|
|
|
should_trigger = (min_time_reached && convergence_ok) || max_time_reached;
|
|
}
|
|
} else {
|
|
// DEMO/DEMO_LITE: Timer simple como antes
|
|
should_trigger = demo_timer_ >= demo_next_action_time_;
|
|
}
|
|
|
|
// Si es hora de ejecutar acción
|
|
if (should_trigger) {
|
|
// MODO LOGO: Sistema de acciones variadas con gravedad dinámica
|
|
if (state_manager_->getCurrentMode() == AppMode::LOGO) {
|
|
// Elegir acción aleatoria ponderada
|
|
int action = rand() % 100;
|
|
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
// Logo quieto (formado) → Decidir camino a seguir
|
|
|
|
// DECISIÓN BIFURCADA: ¿Cambio inmediato o esperar flips?
|
|
if (logo_waiting_for_flip_) {
|
|
// Ya estábamos esperando flips, y se disparó el trigger
|
|
// → Hacer el cambio SHAPE → PHYSICS ahora (durante el flip)
|
|
if (action < 50) {
|
|
toggleShapeModeInternal(true); // Con gravedad ON
|
|
} else {
|
|
toggleShapeModeInternal(false); // Con gravedad OFF
|
|
}
|
|
|
|
// Resetear variables de espera de flips
|
|
logo_waiting_for_flip_ = false;
|
|
logo_current_flip_count_ = 0;
|
|
|
|
// Resetear timer
|
|
demo_timer_ = 0.0f;
|
|
float interval_range = logo_max_time_ - logo_min_time_;
|
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
|
} else if (rand() % 100 < LOGO_FLIP_WAIT_PROBABILITY) {
|
|
// CAMINO B (50%): Esperar a que ocurran 1-3 flips
|
|
logo_waiting_for_flip_ = true;
|
|
logo_target_flip_number_ = LOGO_FLIP_WAIT_MIN + rand() % (LOGO_FLIP_WAIT_MAX - LOGO_FLIP_WAIT_MIN + 1);
|
|
logo_target_flip_percentage_ = LOGO_FLIP_TRIGGER_MIN + (rand() % 1000) / 1000.0f * (LOGO_FLIP_TRIGGER_MAX - LOGO_FLIP_TRIGGER_MIN);
|
|
logo_current_flip_count_ = 0;
|
|
|
|
// Resetear contador de flips en PNGShape
|
|
if (active_shape_) {
|
|
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
|
|
if (png_shape) {
|
|
png_shape->resetFlipCount();
|
|
}
|
|
}
|
|
|
|
// NO hacer nada más este frame - esperar a que ocurran los flips
|
|
// El trigger se ejecutará en futuras iteraciones cuando se cumplan las condiciones
|
|
} else {
|
|
// CAMINO A (50%): Cambio inmediato
|
|
if (action < 50) {
|
|
// 50%: SHAPE → PHYSICS con gravedad ON (caída dramática)
|
|
toggleShapeModeInternal(true);
|
|
} else {
|
|
// 50%: SHAPE → PHYSICS con gravedad OFF (dar vueltas sin caer)
|
|
toggleShapeModeInternal(false);
|
|
}
|
|
|
|
// Resetear variables de espera de flips al cambiar a PHYSICS
|
|
logo_waiting_for_flip_ = false;
|
|
logo_current_flip_count_ = 0;
|
|
|
|
// Resetear timer con intervalos escalados
|
|
demo_timer_ = 0.0f;
|
|
float interval_range = logo_max_time_ - logo_min_time_;
|
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
|
}
|
|
} else {
|
|
// Logo animado (PHYSICS) → 4 opciones posibles
|
|
if (action < 50) {
|
|
// 50%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones)
|
|
toggleShapeModeInternal(false);
|
|
|
|
// Resetear variables de espera de flips al volver a SHAPE
|
|
logo_waiting_for_flip_ = false;
|
|
logo_current_flip_count_ = 0;
|
|
} else if (action < 68) {
|
|
// 18%: Forzar gravedad ON (empezar a caer mientras da vueltas)
|
|
scene_manager_->forceBallsGravityOn();
|
|
} else if (action < 84) {
|
|
// 16%: Forzar gravedad OFF (flotar mientras da vueltas)
|
|
scene_manager_->forceBallsGravityOff();
|
|
} else {
|
|
// 16%: Cambiar dirección de gravedad (nueva variación)
|
|
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
|
scene_manager_->changeGravityDirection(new_direction);
|
|
// Si la gravedad está OFF, activarla para que el cambio sea visible
|
|
scene_manager_->forceBallsGravityOn();
|
|
}
|
|
|
|
// Resetear timer con intervalos escalados
|
|
demo_timer_ = 0.0f;
|
|
float interval_range = logo_max_time_ - logo_min_time_;
|
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
|
}
|
|
|
|
// Solo salir automáticamente si la entrada a LOGO fue automática (desde DEMO)
|
|
// No salir si el usuario entró manualmente con tecla K
|
|
// Probabilidad de salir: 60% en cada acción → sale rápido (relación DEMO:LOGO = 6:1)
|
|
if (!state_manager_->getLogoEnteredManually() && rand() % 100 < 60) {
|
|
state_manager_->exitLogoMode(true); // Volver a DEMO/DEMO_LITE
|
|
}
|
|
}
|
|
// MODO DEMO/DEMO_LITE: Acciones normales
|
|
else {
|
|
bool is_lite = (state_manager_->getCurrentMode() == AppMode::DEMO_LITE);
|
|
executeDemoAction(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Callback para StateManager - Ejecutar acción DEMO
|
|
void Engine::executeDemoAction(bool is_lite) {
|
|
// ============================================
|
|
// SALTO AUTOMÁTICO A LOGO MODE (Easter Egg)
|
|
// ============================================
|
|
|
|
if (is_lite) {
|
|
// DEMO LITE: Verificar condiciones para salto a Logo Mode
|
|
if (static_cast<int>(scene_manager_->getBallCount()) >= LOGO_MODE_MIN_BALLS &&
|
|
theme_manager_->getCurrentThemeIndex() == 5) { // MONOCHROME
|
|
// 10% probabilidad de saltar a Logo Mode
|
|
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) {
|
|
state_manager_->enterLogoMode(true, current_screen_width_, current_screen_height_, scene_manager_->getBallCount());
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
// DEMO COMPLETO: Verificar condiciones para salto a Logo Mode
|
|
if (static_cast<int>(scene_manager_->getBallCount()) >= LOGO_MODE_MIN_BALLS) {
|
|
// 15% probabilidad de saltar a Logo Mode
|
|
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) {
|
|
state_manager_->enterLogoMode(true, current_screen_width_, current_screen_height_, scene_manager_->getBallCount());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// ACCIONES NORMALES DE DEMO/DEMO_LITE
|
|
// ============================================
|
|
|
|
int TOTAL_WEIGHT;
|
|
int random_value;
|
|
int accumulated_weight = 0;
|
|
|
|
if (is_lite) {
|
|
// DEMO LITE: Solo física/figuras
|
|
TOTAL_WEIGHT = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE;
|
|
random_value = rand() % TOTAL_WEIGHT;
|
|
|
|
// Cambiar dirección gravedad (25%)
|
|
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR;
|
|
if (random_value < accumulated_weight) {
|
|
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
|
scene_manager_->changeGravityDirection(new_direction);
|
|
return;
|
|
}
|
|
|
|
// Toggle gravedad ON/OFF (20%)
|
|
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_TOGGLE;
|
|
if (random_value < accumulated_weight) {
|
|
executeToggleGravityOnOff();
|
|
return;
|
|
}
|
|
|
|
// Activar figura 3D (25%) - PNG_SHAPE excluido (reservado para Logo Mode)
|
|
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
|
|
if (random_value < accumulated_weight) {
|
|
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
|
int shape_index = rand() % 8;
|
|
activateShapeInternal(shapes[shape_index]);
|
|
return;
|
|
}
|
|
|
|
// Toggle física ↔ figura (20%)
|
|
accumulated_weight += DEMO_LITE_WEIGHT_TOGGLE_PHYSICS;
|
|
if (random_value < accumulated_weight) {
|
|
toggleShapeModeInternal(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);
|
|
scene_manager_->changeGravityDirection(new_direction);
|
|
return;
|
|
}
|
|
|
|
// Toggle gravedad ON/OFF (8%)
|
|
accumulated_weight += DEMO_WEIGHT_GRAVITY_TOGGLE;
|
|
if (random_value < accumulated_weight) {
|
|
executeToggleGravityOnOff();
|
|
return;
|
|
}
|
|
|
|
// Activar figura 3D (20%) - PNG_SHAPE excluido (reservado para Logo Mode)
|
|
accumulated_weight += DEMO_WEIGHT_SHAPE;
|
|
if (random_value < accumulated_weight) {
|
|
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
|
int shape_index = rand() % 8;
|
|
activateShapeInternal(shapes[shape_index]);
|
|
return;
|
|
}
|
|
|
|
// Toggle física ↔ figura (12%)
|
|
accumulated_weight += DEMO_WEIGHT_TOGGLE_PHYSICS;
|
|
if (random_value < accumulated_weight) {
|
|
toggleShapeModeInternal(false); // NO forzar gravedad al salir
|
|
return;
|
|
}
|
|
|
|
// Re-generar misma figura (8%)
|
|
accumulated_weight += DEMO_WEIGHT_REGENERATE_SHAPE;
|
|
if (random_value < accumulated_weight) {
|
|
if (current_mode_ == SimulationMode::SHAPE && active_shape_) {
|
|
generateShape(); // Re-generar sin cambiar tipo
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Cambiar tema (15%)
|
|
accumulated_weight += DEMO_WEIGHT_THEME;
|
|
if (random_value < accumulated_weight) {
|
|
// Elegir entre TODOS los 15 temas (9 estáticos + 6 dinámicos)
|
|
int random_theme_index = rand() % 15;
|
|
theme_manager_->switchToTheme(random_theme_index);
|
|
return;
|
|
}
|
|
|
|
// Cambiar escenario (10%) - EXCLUIR índices 0, 6, 7 (1, 50K, 100K pelotas)
|
|
accumulated_weight += DEMO_WEIGHT_SCENARIO;
|
|
if (random_value < accumulated_weight) {
|
|
// Escenarios válidos: índices 1, 2, 3, 4, 5 (10, 100, 500, 1000, 10000 pelotas)
|
|
int valid_scenarios[] = {1, 2, 3, 4, 5};
|
|
int new_scenario = valid_scenarios[rand() % 5];
|
|
scene_manager_->changeScenario(new_scenario, current_mode_);
|
|
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) {
|
|
switchTextureInternal(false); // Suprimir notificación en modo automático
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Callback para StateManager - Randomizar estado al iniciar DEMO
|
|
void Engine::executeRandomizeOnDemoStart(bool is_lite) {
|
|
// Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente
|
|
// PNG_SHAPE es exclusivo del modo LOGO y no debe aparecer en DEMO/DEMO_LITE
|
|
if (current_shape_type_ == ShapeType::PNG_SHAPE) {
|
|
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
|
|
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
|
|
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
|
activateShapeInternal(shapes[rand() % 8]);
|
|
}
|
|
|
|
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) {
|
|
toggleShapeModeInternal(false); // Salir a física sin forzar gravedad
|
|
}
|
|
} else {
|
|
// Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
|
|
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
|
activateShapeInternal(shapes[rand() % 8]);
|
|
}
|
|
|
|
// Randomizar gravedad: dirección + ON/OFF
|
|
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
|
scene_manager_->changeGravityDirection(new_direction);
|
|
if (rand() % 2 == 0) {
|
|
executeToggleGravityOnOff(); // 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};
|
|
int new_scenario = valid_scenarios[rand() % 5];
|
|
scene_manager_->changeScenario(new_scenario, current_mode_);
|
|
|
|
// 2. Tema (elegir entre TODOS los 15 temas)
|
|
int random_theme_index = rand() % 15;
|
|
theme_manager_->switchToTheme(random_theme_index);
|
|
|
|
// 3. Sprite
|
|
if (rand() % 2 == 0) {
|
|
switchTextureInternal(false); // Suprimir notificación al activar modo DEMO
|
|
}
|
|
|
|
// 4. Física o Figura
|
|
if (rand() % 2 == 0) {
|
|
// Modo física
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
toggleShapeModeInternal(false);
|
|
}
|
|
} else {
|
|
// Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
|
|
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
|
activateShapeInternal(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);
|
|
scene_manager_->changeGravityDirection(new_direction);
|
|
if (rand() % 3 == 0) { // 33% probabilidad de desactivar gravedad
|
|
executeToggleGravityOnOff();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Callback para StateManager - Toggle gravedad ON/OFF para todas las pelotas
|
|
void Engine::executeToggleGravityOnOff() {
|
|
// Alternar entre activar/desactivar gravedad
|
|
bool first_ball_gravity_enabled = (!scene_manager_->hasBalls() || scene_manager_->getFirstBall()->getGravityForce() > 0.0f);
|
|
|
|
if (first_ball_gravity_enabled) {
|
|
// Desactivar gravedad
|
|
scene_manager_->forceBallsGravityOff();
|
|
} else {
|
|
// Activar gravedad
|
|
scene_manager_->forceBallsGravityOn();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// CALLBACKS PARA STATEMANAGER - LOGO MODE
|
|
// ============================================================================
|
|
|
|
// Callback para StateManager - Configuración visual al entrar a LOGO MODE
|
|
void Engine::executeEnterLogoMode(size_t ball_count) {
|
|
// Verificar mínimo de pelotas
|
|
if (static_cast<int>(ball_count) < LOGO_MODE_MIN_BALLS) {
|
|
// Ajustar a 5000 pelotas automáticamente
|
|
scene_manager_->changeScenario(5, current_mode_); // Escenario 5000 pelotas (índice 5 en BALL_COUNT_SCENARIOS)
|
|
}
|
|
|
|
// Guardar estado previo (para restaurar al salir)
|
|
logo_previous_theme_ = theme_manager_->getCurrentThemeIndex();
|
|
logo_previous_texture_index_ = current_texture_index_;
|
|
logo_previous_shape_scale_ = shape_scale_factor_;
|
|
|
|
// Buscar índice de textura "small"
|
|
size_t small_index = current_texture_index_; // Por defecto mantener actual
|
|
for (size_t i = 0; i < texture_names_.size(); i++) {
|
|
if (texture_names_[i] == "small") {
|
|
small_index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Aplicar configuración fija del Modo Logo
|
|
if (small_index != current_texture_index_) {
|
|
current_texture_index_ = small_index;
|
|
texture_ = textures_[current_texture_index_];
|
|
int new_size = texture_->getWidth();
|
|
current_ball_size_ = new_size;
|
|
scene_manager_->updateBallTexture(texture_, new_size);
|
|
}
|
|
|
|
// Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
|
|
int logo_themes[] = {5, 6, 7, 8}; // MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
|
|
int random_theme = logo_themes[rand() % 4];
|
|
theme_manager_->switchToTheme(random_theme);
|
|
|
|
// Establecer escala a 120%
|
|
shape_scale_factor_ = LOGO_MODE_SHAPE_SCALE;
|
|
clampShapeScale();
|
|
|
|
// Activar PNG_SHAPE (el logo)
|
|
activateShapeInternal(ShapeType::PNG_SHAPE);
|
|
|
|
// Configurar PNG_SHAPE en modo LOGO (flip intervals más largos)
|
|
if (active_shape_) {
|
|
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
|
|
if (png_shape) {
|
|
png_shape->setLogoMode(true);
|
|
png_shape->resetFlipCount(); // Resetear contador de flips
|
|
}
|
|
}
|
|
}
|
|
|
|
void Engine::executeExitLogoMode() {
|
|
// Restaurar estado visual previo
|
|
theme_manager_->switchToTheme(logo_previous_theme_);
|
|
|
|
if (logo_previous_texture_index_ != current_texture_index_ &&
|
|
logo_previous_texture_index_ < textures_.size()) {
|
|
current_texture_index_ = logo_previous_texture_index_;
|
|
texture_ = textures_[current_texture_index_];
|
|
int new_size = texture_->getWidth();
|
|
current_ball_size_ = new_size;
|
|
scene_manager_->updateBallTexture(texture_, new_size);
|
|
}
|
|
|
|
shape_scale_factor_ = logo_previous_shape_scale_;
|
|
clampShapeScale();
|
|
generateShape();
|
|
|
|
// Desactivar modo LOGO en PNG_SHAPE (volver a flip intervals normales)
|
|
if (active_shape_) {
|
|
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
|
|
if (png_shape) {
|
|
png_shape->setLogoMode(false);
|
|
}
|
|
}
|
|
|
|
// Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria
|
|
if (current_shape_type_ == ShapeType::PNG_SHAPE) {
|
|
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
|
|
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
|
|
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
|
activateShapeInternal(shapes[rand() % 8]);
|
|
}
|
|
}
|
|
|
|
// Toggle manual del Modo Logo (tecla K)
|
|
// Sistema de cambio de sprites dinámico
|
|
void Engine::switchTextureInternal(bool show_notification) {
|
|
if (textures_.empty()) return;
|
|
|
|
// 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 (delegado a SceneManager)
|
|
scene_manager_->updateBallTexture(texture_, new_size);
|
|
|
|
// Mostrar notificación con el nombre de la textura (solo si se solicita)
|
|
if (show_notification) {
|
|
std::string texture_name = texture_names_[current_texture_index_];
|
|
std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper);
|
|
showNotificationForAction("Sprite: " + texture_name);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sistema de Figuras 3D - IMPLEMENTACIÓN PARA CALLBACKS DEMO/LOGO
|
|
// ============================================================================
|
|
// NOTA: Engine mantiene implementación de figuras usada por callbacks
|
|
// ShapeManager tiene implementación paralela para controles manuales del usuario
|
|
// Este enfoque permite que DEMO/LOGO manipulen figuras sin afectar el estado manual
|
|
|
|
// Alternar entre modo física y última figura (usado por performLogoAction)
|
|
void Engine::toggleShapeModeInternal(bool force_gravity_on_exit) {
|
|
if (current_mode_ == SimulationMode::PHYSICS) {
|
|
// Cambiar a modo figura (usar última figura seleccionada)
|
|
activateShapeInternal(last_shape_type_);
|
|
|
|
// Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO
|
|
if (state_manager_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
|
|
if (active_shape_) {
|
|
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
|
|
if (png_shape) {
|
|
png_shape->setLogoMode(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Si estamos en LOGO MODE, generar threshold aleatorio de convergencia (75-100%)
|
|
if (state_manager_->getCurrentMode() == AppMode::LOGO) {
|
|
logo_convergence_threshold_ = LOGO_CONVERGENCE_MIN +
|
|
(rand() % 1000) / 1000.0f * (LOGO_CONVERGENCE_MAX - LOGO_CONVERGENCE_MIN);
|
|
shape_convergence_ = 0.0f; // Reset convergencia al entrar
|
|
}
|
|
} else {
|
|
// Volver a modo física normal
|
|
current_mode_ = SimulationMode::PHYSICS;
|
|
|
|
// Desactivar atracción y resetear escala de profundidad
|
|
auto& balls = scene_manager_->getBallsMutable();
|
|
for (auto& ball : balls) {
|
|
ball->enableShapeAttraction(false);
|
|
ball->setDepthScale(1.0f); // Reset escala a 100% (evita "pop" visual)
|
|
}
|
|
|
|
// Activar gravedad al salir (solo si se especifica)
|
|
if (force_gravity_on_exit) {
|
|
scene_manager_->forceBallsGravityOn();
|
|
}
|
|
|
|
// Mostrar notificación (solo si NO estamos en modo demo o logo)
|
|
if (state_manager_->getCurrentMode() == AppMode::SANDBOX) {
|
|
ui_manager_->showNotification("Modo Física");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Activar figura específica (llamado por teclas Q/W/E/R/Y/U/I o por toggleShapeMode)
|
|
void Engine::activateShapeInternal(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
|
|
scene_manager_->forceBallsGravityOff();
|
|
|
|
// Crear instancia polimórfica de la figura correspondiente
|
|
switch (type) {
|
|
case ShapeType::SPHERE:
|
|
active_shape_ = std::make_unique<SphereShape>();
|
|
break;
|
|
case ShapeType::CUBE:
|
|
active_shape_ = std::make_unique<CubeShape>();
|
|
break;
|
|
case ShapeType::HELIX:
|
|
active_shape_ = std::make_unique<HelixShape>();
|
|
break;
|
|
case ShapeType::TORUS:
|
|
active_shape_ = std::make_unique<TorusShape>();
|
|
break;
|
|
case ShapeType::LISSAJOUS:
|
|
active_shape_ = std::make_unique<LissajousShape>();
|
|
break;
|
|
case ShapeType::CYLINDER:
|
|
active_shape_ = std::make_unique<CylinderShape>();
|
|
break;
|
|
case ShapeType::ICOSAHEDRON:
|
|
active_shape_ = std::make_unique<IcosahedronShape>();
|
|
break;
|
|
case ShapeType::ATOM:
|
|
active_shape_ = std::make_unique<AtomShape>();
|
|
break;
|
|
case ShapeType::PNG_SHAPE:
|
|
active_shape_ = std::make_unique<PNGShape>("data/shapes/jailgames.png");
|
|
break;
|
|
default:
|
|
active_shape_ = std::make_unique<SphereShape>(); // Fallback
|
|
break;
|
|
}
|
|
|
|
// Generar puntos de la figura
|
|
generateShape();
|
|
|
|
// Activar atracción física en todas las pelotas
|
|
auto& balls = scene_manager_->getBallsMutable();
|
|
for (auto& ball : balls) {
|
|
ball->enableShapeAttraction(true);
|
|
}
|
|
|
|
// Mostrar notificación con nombre de figura (solo si NO estamos en modo demo o logo)
|
|
if (active_shape_ && state_manager_->getCurrentMode() == AppMode::SANDBOX) {
|
|
std::string notification = std::string("Modo ") + active_shape_->getName();
|
|
ui_manager_->showNotification(notification);
|
|
}
|
|
}
|
|
|
|
// Generar puntos de la figura activa
|
|
void Engine::generateShape() {
|
|
if (!active_shape_) return;
|
|
|
|
int num_points = static_cast<int>(scene_manager_->getBallCount());
|
|
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;
|
|
|
|
// Obtener referencia mutable a las bolas desde SceneManager
|
|
auto& balls = scene_manager_->getBallsMutable();
|
|
|
|
// Actualizar cada pelota con física de atracción
|
|
for (size_t i = 0; i < balls.size(); i++) {
|
|
// Obtener posición 3D rotada del punto i
|
|
float x_3d, y_3d, z_3d;
|
|
active_shape_->getPoint3D(static_cast<int>(i), x_3d, y_3d, z_3d);
|
|
|
|
// Aplicar escala manual a las coordenadas 3D
|
|
x_3d *= shape_scale_factor_;
|
|
y_3d *= shape_scale_factor_;
|
|
z_3d *= shape_scale_factor_;
|
|
|
|
// Proyección 2D ortográfica (punto objetivo móvil)
|
|
float target_x = center_x + x_3d;
|
|
float target_y = center_y + y_3d;
|
|
|
|
// Actualizar target de la pelota para cálculo de convergencia
|
|
balls[i]->setShapeTarget2D(target_x, target_y);
|
|
|
|
// Aplicar fuerza de atracción física hacia el punto rotado
|
|
// Usar constantes SHAPE (mayor pegajosidad que ROTOBALL)
|
|
float shape_size = scale_factor * 80.0f; // 80px = radio base
|
|
balls[i]->applyShapeForce(target_x, target_y, shape_size, delta_time_, SHAPE_SPRING_K, SHAPE_DAMPING_BASE, SHAPE_DAMPING_NEAR, SHAPE_NEAR_THRESHOLD, SHAPE_MAX_FORCE);
|
|
|
|
// Calcular brillo según profundidad Z para renderizado
|
|
// Normalizar Z al rango de la figura (asumiendo simetría ±shape_size)
|
|
float z_normalized = (z_3d + shape_size) / (2.0f * shape_size);
|
|
z_normalized = std::max(0.0f, std::min(1.0f, z_normalized));
|
|
balls[i]->setDepthBrightness(z_normalized);
|
|
|
|
// Calcular escala según profundidad Z (perspectiva) - solo si está activado
|
|
// 0.0 (fondo) → 0.5x, 0.5 (medio) → 1.0x, 1.0 (frente) → 1.5x
|
|
float depth_scale = depth_zoom_enabled_ ? (0.5f + z_normalized * 1.0f) : 1.0f;
|
|
balls[i]->setDepthScale(depth_scale);
|
|
}
|
|
|
|
// Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo)
|
|
if (state_manager_->getCurrentMode() == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
|
|
int balls_near = 0;
|
|
float distance_threshold = LOGO_CONVERGENCE_DISTANCE; // 20px fijo (más permisivo)
|
|
|
|
for (const auto& ball : balls) {
|
|
if (ball->getDistanceToTarget() < distance_threshold) {
|
|
balls_near++;
|
|
}
|
|
}
|
|
|
|
shape_convergence_ = static_cast<float>(balls_near) / scene_manager_->getBallCount();
|
|
|
|
// Notificar a la figura sobre el porcentaje de convergencia
|
|
// Esto permite que PNGShape decida cuándo empezar a contar para flips
|
|
active_shape_->setConvergence(shape_convergence_);
|
|
}
|
|
}
|
|
|
|
// Limitar escala de figura para evitar que se salga de pantalla
|
|
void Engine::clampShapeScale() {
|
|
// Calcular tamaño máximo permitido según resolución actual
|
|
// La figura más grande (esfera/cubo) usa ~33% de altura por defecto
|
|
// Permitir hasta que la figura ocupe 90% de la dimensión más pequeña
|
|
float max_dimension = std::min(current_screen_width_, current_screen_height_);
|
|
float base_size_factor = 0.333f; // ROTOBALL_RADIUS_FACTOR o similar
|
|
float max_scale_for_screen = (max_dimension * 0.9f) / (max_dimension * base_size_factor);
|
|
|
|
// Limitar entre SHAPE_SCALE_MIN y el mínimo de (SHAPE_SCALE_MAX, max_scale_for_screen)
|
|
float max_allowed = std::min(SHAPE_SCALE_MAX, max_scale_for_screen);
|
|
shape_scale_factor_ = std::max(SHAPE_SCALE_MIN, std::min(max_allowed, shape_scale_factor_));
|
|
} |