Implementa patrón facade/delegation para gestión de estado de aplicación. Engine ahora consulta estado a través de StateManager en lugar de acceso directo. ## Cambios **source/engine.cpp:** - Reemplazar `current_app_mode_` con `state_manager_->getCurrentMode()` (18 ocurrencias) - setState() delega a StateManager pero mantiene setup en Engine (temporal) - toggleDemoMode/Lite/Logo() usan getCurrentMode() de StateManager - updateDemoMode() consulta modo actual mediante StateManager **source/state/state_manager.cpp:** - setState() implementado con lógica básica de cambio de estado - Maneja transiciones LOGO ↔ otros modos correctamente - Reset de demo_timer_ al cambiar estado ## Patrón Facade Aplicado **Justificación:** Token budget limitado requiere enfoque pragmático - StateManager = Interfaz pública para consultas de estado - Engine = Mantiene implementación compleja temporalmente - Refactorización incremental sin reescribir 600+ líneas **Próximo paso (Fase 4c):** - Eliminar duplicación de miembros entre Engine y StateManager - Migrar lógica compleja gradualmente ## Verificación ✅ Compilación exitosa ✅ Sin errores de asignación a lvalue ✅ Todas las consultas de estado delegadas correctamente 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1756 lines
70 KiB
C++
1756 lines
70 KiB
C++
#include "engine.h"
|
|
|
|
#include <SDL3/SDL_error.h> // for SDL_GetError
|
|
#include <SDL3/SDL_events.h> // for SDL_Event, SDL_PollEvent
|
|
#include <SDL3/SDL_init.h> // for SDL_Init, SDL_Quit, SDL_INIT_VIDEO
|
|
#include <SDL3/SDL_keycode.h> // for SDL_Keycode
|
|
#include <SDL3/SDL_render.h> // for SDL_SetRenderDrawColor, SDL_RenderPresent
|
|
#include <SDL3/SDL_timer.h> // for SDL_GetTicks
|
|
#include <SDL3/SDL_video.h> // for SDL_CreateWindow, SDL_DestroyWindow, SDL_GetDisplayBounds
|
|
|
|
#include <algorithm> // for std::min, std::max, std::sort
|
|
#include <cmath> // for sqrtf, acosf, cosf, sinf (funciones matemáticas)
|
|
#include <cstdlib> // for rand, srand
|
|
#include <cstring> // for strlen
|
|
#include <ctime> // for time
|
|
#include <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
|
|
updatePhysicalWindowSize();
|
|
|
|
// Inicializar UIManager (HUD, FPS, notificaciones)
|
|
// NOTA: Debe llamarse DESPUÉS de updatePhysicalWindowSize() y ThemeManager
|
|
ui_manager_ = std::make_unique<UIManager>();
|
|
ui_manager_->initialize(renderer_, theme_manager_.get(), physical_window_width_, physical_window_height_);
|
|
|
|
// Inicializar StateManager (gestión de estados DEMO/LOGO)
|
|
state_manager_ = std::make_unique<StateManager>();
|
|
state_manager_->initialize(this); // Callback al Engine
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// Actualizar Modo DEMO (auto-play)
|
|
updateDemoMode();
|
|
|
|
// 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 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 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();
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
// Resetear modo SHAPE si está activo
|
|
if (current_mode_ == SimulationMode::SHAPE) {
|
|
current_mode_ = SimulationMode::PHYSICS;
|
|
active_shape_.reset();
|
|
}
|
|
|
|
scene_manager_->changeScenario(scenario_id);
|
|
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)
|
|
void Engine::toggleDemoMode() {
|
|
if (state_manager_->getCurrentMode() == AppMode::DEMO) {
|
|
// Ya estamos en DEMO → volver a SANDBOX
|
|
setState(AppMode::SANDBOX);
|
|
showNotificationForAction("MODO SANDBOX");
|
|
} else {
|
|
// Estamos en otro modo → ir a DEMO
|
|
setState(AppMode::DEMO);
|
|
randomizeOnDemoStart(false);
|
|
showNotificationForAction("MODO DEMO");
|
|
}
|
|
}
|
|
|
|
void Engine::toggleDemoLiteMode() {
|
|
if (state_manager_->getCurrentMode() == AppMode::DEMO_LITE) {
|
|
// Ya estamos en DEMO_LITE → volver a SANDBOX
|
|
setState(AppMode::SANDBOX);
|
|
showNotificationForAction("MODO SANDBOX");
|
|
} else {
|
|
// Estamos en otro modo → ir a DEMO_LITE
|
|
setState(AppMode::DEMO_LITE);
|
|
randomizeOnDemoStart(true);
|
|
showNotificationForAction("MODO DEMO LITE");
|
|
}
|
|
}
|
|
|
|
void Engine::toggleLogoMode() {
|
|
if (state_manager_->getCurrentMode() == AppMode::LOGO) {
|
|
// Ya estamos en LOGO → volver a SANDBOX
|
|
exitLogoMode(false);
|
|
showNotificationForAction("MODO SANDBOX");
|
|
} else {
|
|
// Estamos en otro modo → ir a LOGO
|
|
enterLogoMode(false);
|
|
showNotificationForAction("MODO LOGO");
|
|
}
|
|
}
|
|
|
|
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_, 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_);
|
|
|
|
// 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());
|
|
}
|
|
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());
|
|
}
|
|
}
|
|
|
|
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_);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sistema de gestión de estados (MANUAL/DEMO/DEMO_LITE/LOGO)
|
|
// ============================================================================
|
|
|
|
void Engine::setState(AppMode new_mode) {
|
|
// Delegar a StateManager pero mantener lógica de setup en Engine temporalmente
|
|
// TODO: Mover toda esta lógica a StateManager
|
|
|
|
// Aplicar el nuevo modo a través de StateManager
|
|
state_manager_->setState(new_mode, current_screen_width_, current_screen_height_);
|
|
|
|
// Configurar timer de demo según el modo
|
|
if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) {
|
|
demo_timer_ = 0.0f;
|
|
float min_interval, max_interval;
|
|
|
|
if (new_mode == AppMode::LOGO) {
|
|
// Escalar tiempos con resolución (720p como base)
|
|
float resolution_scale = current_screen_height_ / 720.0f;
|
|
logo_min_time_ = LOGO_ACTION_INTERVAL_MIN * resolution_scale;
|
|
logo_max_time_ = LOGO_ACTION_INTERVAL_MAX * resolution_scale;
|
|
|
|
min_interval = logo_min_time_;
|
|
max_interval = logo_max_time_;
|
|
} else {
|
|
bool is_lite = (new_mode == AppMode::DEMO_LITE);
|
|
min_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
|
|
max_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
|
|
}
|
|
|
|
demo_next_action_time_ = min_interval + (rand() % 1000) / 1000.0f * (max_interval - min_interval);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sistema de Modo DEMO (auto-play)
|
|
// ============================================================================
|
|
|
|
void Engine::updateDemoMode() {
|
|
// Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO)
|
|
if (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) → 3 opciones posibles
|
|
if (action < 60) {
|
|
// 60%: 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 < 80) {
|
|
// 20%: Forzar gravedad ON (empezar a caer mientras da vueltas)
|
|
scene_manager_->forceBallsGravityOn();
|
|
} else {
|
|
// 20%: Forzar gravedad OFF (flotar mientras da vueltas)
|
|
scene_manager_->forceBallsGravityOff();
|
|
}
|
|
|
|
// Resetear timer con intervalos escalados
|
|
demo_timer_ = 0.0f;
|
|
float interval_range = logo_max_time_ - logo_min_time_;
|
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
|
}
|
|
|
|
// Solo salir automáticamente si 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 (!logo_entered_manually_ && rand() % 100 < 60) {
|
|
exitLogoMode(true); // Volver a DEMO/DEMO_LITE
|
|
}
|
|
}
|
|
// MODO DEMO/DEMO_LITE: Acciones normales
|
|
else {
|
|
bool is_lite = (state_manager_->getCurrentMode() == AppMode::DEMO_LITE);
|
|
performDemoAction(is_lite);
|
|
|
|
// Resetear timer y calcular próximo intervalo aleatorio
|
|
demo_timer_ = 0.0f;
|
|
|
|
// Usar intervalos diferentes según modo
|
|
float interval_min = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
|
|
float interval_max = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
|
|
float interval_range = interval_max - interval_min;
|
|
demo_next_action_time_ = interval_min + (rand() % 1000) / 1000.0f * interval_range;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Engine::performDemoAction(bool is_lite) {
|
|
// ============================================
|
|
// SALTO AUTOMÁTICO A LOGO MODE (Easter Egg)
|
|
// ============================================
|
|
|
|
if (is_lite) {
|
|
// DEMO LITE: Verificar condiciones para salto a Logo Mode
|
|
if (static_cast<int>(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) {
|
|
enterLogoMode(true); // Entrar desde DEMO
|
|
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) {
|
|
enterLogoMode(true); // Entrar desde DEMO
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// ACCIONES NORMALES DE DEMO/DEMO_LITE
|
|
// ============================================
|
|
|
|
int TOTAL_WEIGHT;
|
|
int random_value;
|
|
int accumulated_weight = 0;
|
|
|
|
if (is_lite) {
|
|
// DEMO LITE: Solo física/figuras
|
|
TOTAL_WEIGHT = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE;
|
|
random_value = rand() % TOTAL_WEIGHT;
|
|
|
|
// Cambiar dirección gravedad (25%)
|
|
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR;
|
|
if (random_value < accumulated_weight) {
|
|
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
|
|
scene_manager_->changeGravityDirection(new_direction);
|
|
return;
|
|
}
|
|
|
|
// Toggle gravedad ON/OFF (20%)
|
|
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_TOGGLE;
|
|
if (random_value < accumulated_weight) {
|
|
toggleGravityOnOff();
|
|
return;
|
|
}
|
|
|
|
// Activar figura 3D (25%) - PNG_SHAPE excluido (reservado para Logo Mode)
|
|
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
|
|
if (random_value < accumulated_weight) {
|
|
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
|
int shape_index = rand() % 8;
|
|
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) {
|
|
toggleGravityOnOff();
|
|
return;
|
|
}
|
|
|
|
// Activar figura 3D (20%) - PNG_SHAPE excluido (reservado para Logo Mode)
|
|
accumulated_weight += DEMO_WEIGHT_SHAPE;
|
|
if (random_value < accumulated_weight) {
|
|
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
|
|
int shape_index = rand() % 8;
|
|
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);
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Randomizar todo al iniciar modo DEMO
|
|
void Engine::randomizeOnDemoStart(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) {
|
|
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};
|
|
int new_scenario = valid_scenarios[rand() % 5];
|
|
scene_manager_->changeScenario(new_scenario);
|
|
|
|
// 2. Tema (elegir entre TODOS los 15 temas)
|
|
int random_theme_index = rand() % 15;
|
|
theme_manager_->switchToTheme(random_theme_index);
|
|
|
|
// 3. Sprite
|
|
if (rand() % 2 == 0) {
|
|
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
|
|
toggleGravityOnOff();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Toggle gravedad ON/OFF para todas las pelotas
|
|
void Engine::toggleGravityOnOff() {
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SISTEMA DE MODO LOGO (Easter Egg - "Marca de Agua")
|
|
// ============================================================================
|
|
|
|
// Entrar al Modo Logo (manual con tecla K o automático desde DEMO)
|
|
void Engine::enterLogoMode(bool from_demo) {
|
|
// Verificar mínimo de pelotas
|
|
if (static_cast<int>(scene_manager_->getBallCount()) < LOGO_MODE_MIN_BALLS) {
|
|
// Ajustar a 5000 pelotas automáticamente
|
|
scene_manager_->changeScenario(5); // 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
|
|
}
|
|
}
|
|
|
|
// Resetear variables de espera de flips
|
|
logo_waiting_for_flip_ = false;
|
|
logo_target_flip_number_ = 0;
|
|
logo_target_flip_percentage_ = 0.0f;
|
|
logo_current_flip_count_ = 0;
|
|
|
|
// Guardar si entrada fue manual (tecla K) o automática (desde DEMO)
|
|
logo_entered_manually_ = !from_demo;
|
|
|
|
// Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente)
|
|
setState(AppMode::LOGO);
|
|
}
|
|
|
|
// Salir del Modo Logo (volver a estado anterior o salir de DEMO)
|
|
void Engine::exitLogoMode(bool return_to_demo) {
|
|
if (state_manager_->getCurrentMode() != AppMode::LOGO) return;
|
|
|
|
// Restaurar estado previo
|
|
theme_manager_->switchToTheme(logo_previous_theme_);
|
|
|
|
if (logo_previous_texture_index_ != current_texture_index_ &&
|
|
logo_previous_texture_index_ < textures_.size()) {
|
|
current_texture_index_ = logo_previous_texture_index_;
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Resetear flag de entrada manual
|
|
logo_entered_manually_ = false;
|
|
|
|
if (!return_to_demo) {
|
|
// Salida manual (tecla K): volver a MANUAL
|
|
setState(AppMode::SANDBOX);
|
|
} else {
|
|
// Volver al modo previo (DEMO o DEMO_LITE)
|
|
setState(previous_app_mode_);
|
|
|
|
// 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 - Alternar entre modo física y última figura (Toggle con tecla F)
|
|
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_));
|
|
} |