diff --git a/source/engine.cpp b/source/engine.cpp index 73c7fd1..23da723 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -26,17 +26,7 @@ #include "ball.hpp" // for Ball #include "external/mouse.hpp" // for Mouse namespace #include "external/texture.hpp" // for Texture -#include "shapes/atom_shape.hpp" // for AtomShape -#include "shapes/cube_shape.hpp" // for CubeShape -#include "shapes/cylinder_shape.hpp" // for CylinderShape -#include "shapes/helix_shape.hpp" // for HelixShape -#include "shapes/icosahedron_shape.hpp" // for IcosahedronShape -#include "shapes/lissajous_shape.hpp" // for LissajousShape -#include "shapes/png_shape.hpp" // for PNGShape -#include "shapes/sphere_shape.hpp" // for SphereShape -#include "shapes/torus_shape.hpp" // for TorusShape - -// getExecutableDirectory() ya está definido en defines.h como inline +#include "shapes/png_shape.hpp" // for PNGShape (dynamic_cast en callbacks LOGO) // Implementación de métodos públicos bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMode initial_mode) { @@ -261,7 +251,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod // Inicializar StateManager (gestión de estados DEMO/LOGO) state_manager_ = std::make_unique(); - state_manager_->initialize(this); // Callback al Engine + state_manager_->initialize(this, scene_manager_.get(), theme_manager_.get(), shape_manager_.get()); // Establecer modo inicial si no es SANDBOX (default) // Usar métodos de alto nivel que ejecutan las acciones de configuración @@ -276,7 +266,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod else if (initial_mode == AppMode::LOGO) { size_t initial_ball_count = scene_manager_->getBallCount(); state_manager_->enterLogoMode(false, current_screen_width_, current_screen_height_, initial_ball_count); - // enterLogoMode() hace: setState(LOGO) + executeEnterLogoMode() (tema, PNG_SHAPE, etc.) + // enterLogoMode() hace: setState(LOGO) + configuración visual completa } // Actualizar ShapeManager con StateManager (dependencia circular - StateManager debe existir primero) @@ -384,7 +374,7 @@ void Engine::update() { } // Actualizar Modo DEMO/LOGO (delegado a StateManager) - state_manager_->update(delta_time_, shape_convergence_, active_shape_.get()); + state_manager_->update(delta_time_, shape_manager_->getConvergence(), shape_manager_->getActiveShape()); // Actualizar transiciones de temas (delegado a ThemeManager) theme_manager_->update(delta_time_); @@ -459,7 +449,7 @@ void Engine::toggleShapeMode() { // 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(current_shape_type_)]); + showNotificationForAction(shape_names[static_cast(shape_manager_->getCurrentShapeType())]); } } @@ -469,29 +459,18 @@ void Engine::activateShape(ShapeType type, const char* 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(shape_scale_factor_ * 100.0f + 0.5f)) + "%"); - } + // Delegar a ShapeManager (gestiona escala y muestra notificación en SANDBOX) + shape_manager_->handleShapeScaleChange(increase); } void Engine::resetShapeScale() { - if (current_mode_ == SimulationMode::SHAPE) { - shape_scale_factor_ = SHAPE_SCALE_DEFAULT; - showNotificationForAction("Escala 100%"); - } + // Delegar a ShapeManager (resetea escala y muestra notificación en SANDBOX) + shape_manager_->resetShapeScale(); } void Engine::toggleDepthZoom() { - if (current_mode_ == SimulationMode::SHAPE) { - depth_zoom_enabled_ = !depth_zoom_enabled_; - showNotificationForAction(depth_zoom_enabled_ ? "Profundidad On" : "Profundidad Off"); - } + // Delegar a ShapeManager (toggle depth zoom y muestra notificación en SANDBOX) + shape_manager_->toggleDepthZoom(); } // Boids (comportamiento de enjambre) @@ -504,14 +483,8 @@ void Engine::toggleBoidsMode(bool force_gravity_on) { // Entrar al modo boids (desde PHYSICS o SHAPE) if (current_mode_ == SimulationMode::SHAPE) { // Si estamos en modo shape, salir primero sin forzar gravedad + shape_manager_->toggleShapeMode(false); 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 @@ -598,12 +571,7 @@ void Engine::changeScenario(int scenario_id, const char* notification_text) { // 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); - } + scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario } // Si estamos en modo BOIDS, desactivar gravedad (modo BOIDS = gravedad OFF siempre) @@ -793,7 +761,7 @@ void Engine::render() { // 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_, + shape_manager_->getActiveShape(), shape_manager_->getConvergence(), physical_window_width_, physical_window_height_, current_screen_width_); // Renderizar AppLogo (logo periódico) - después de UI, antes de present @@ -890,12 +858,7 @@ void Engine::toggleRealFullscreen() { // Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones if (current_mode_ == SimulationMode::SHAPE) { generateShape(); // Regenerar figura con nuevas dimensiones de pantalla - - // 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); - } + scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario } } SDL_free(displays); @@ -928,12 +891,7 @@ void Engine::toggleRealFullscreen() { // Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones if (current_mode_ == SimulationMode::SHAPE) { generateShape(); // Regenerar figura con nuevas dimensiones de pantalla - - // 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); - } + scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario } } } @@ -1143,725 +1101,28 @@ void Engine::updatePhysicalWindowSize() { } // ============================================================================ -// 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(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(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(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(scene_manager_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX] && - 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(scene_manager_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX]) { - // 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(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(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%) - rango dinámico según benchmark de rendimiento - accumulated_weight += DEMO_WEIGHT_SCENARIO; - if (random_value < accumulated_weight) { - int auto_max = std::min(max_auto_scenario_, DEMO_AUTO_MAX_SCENARIO); - std::vector candidates; - for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) - candidates.push_back(i); - if (custom_scenario_enabled_ && custom_auto_available_) - candidates.push_back(CUSTOM_SCENARIO_IDX); - int new_scenario = candidates[rand() % candidates.size()]; - scene_manager_->changeScenario(new_scenario, 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); - } - } - - 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(rand() % 4); - scene_manager_->changeGravityDirection(new_direction); - if (rand() % 2 == 0) { - executeToggleGravityOnOff(); // 50% probabilidad de desactivar gravedad - } - - } else { - // DEMO COMPLETO: Randomizar TODO - - // 1. Física o Figura (decidir PRIMERO antes de cambiar escenario) - 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}; - ShapeType selected_shape = shapes[rand() % 8]; - - // Configurar figura SIN generar puntos (changeScenario lo hará después) - last_shape_type_ = selected_shape; - current_shape_type_ = selected_shape; - current_mode_ = SimulationMode::SHAPE; - - // Crear instancia de la figura sin generar puntos todavía - switch (selected_shape) { - case ShapeType::SPHERE: - active_shape_ = std::make_unique(); - break; - case ShapeType::CUBE: - active_shape_ = std::make_unique(); - break; - case ShapeType::HELIX: - active_shape_ = std::make_unique(); - break; - case ShapeType::TORUS: - active_shape_ = std::make_unique(); - break; - case ShapeType::LISSAJOUS: - active_shape_ = std::make_unique(); - break; - case ShapeType::CYLINDER: - active_shape_ = std::make_unique(); - break; - case ShapeType::ICOSAHEDRON: - active_shape_ = std::make_unique(); - break; - case ShapeType::ATOM: - active_shape_ = std::make_unique(); - break; - default: - active_shape_ = std::make_unique(); - break; - } - - // Profundidad (solo si estamos en figura) - if (rand() % 2 == 0) { - depth_zoom_enabled_ = !depth_zoom_enabled_; - } - - // Escala de figura (aleatoria entre 0.5x y 2.0x) - shape_scale_factor_ = 0.5f + (rand() % 1500) / 1000.0f; - clampShapeScale(); - - // NOTA: NO llamar a generateShape() ni activar atracción aquí - // changeScenario() creará las pelotas y luego llamará a generateShape() - } - - // 2. Escenario - rango dinámico según benchmark de rendimiento - int auto_max = std::min(max_auto_scenario_, DEMO_AUTO_MAX_SCENARIO); - std::vector candidates; - for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) - candidates.push_back(i); - if (custom_scenario_enabled_ && custom_auto_available_) - candidates.push_back(CUSTOM_SCENARIO_IDX); - int new_scenario = candidates[rand() % candidates.size()]; - scene_manager_->changeScenario(new_scenario, current_mode_); - - // Si estamos en modo SHAPE, generar la figura y activar atracción - if (current_mode_ == SimulationMode::SHAPE) { - generateShape(); - - // Activar atracción física en las bolas nuevas - auto& balls = scene_manager_->getBallsMutable(); - for (auto& ball : balls) { - ball->enableShapeAttraction(true); - } - } - - // 3. Tema (elegir entre TODOS los 15 temas) - int random_theme_index = rand() % 15; - theme_manager_->switchToTheme(random_theme_index); - - // 4. Sprite - if (rand() % 2 == 0) { - switchTextureInternal(false); // Suprimir notificación al activar modo DEMO - } - - // 5. Gravedad: dirección + ON/OFF - GravityDirection new_direction = static_cast(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(); - } -} - -// ============================================================================ -// BENCHMARK DE RENDIMIENTO +// MÉTODOS PÚBLICOS PARA STATEMANAGER (automatización sin notificación) // ============================================================================ -void Engine::runPerformanceBenchmark() { - int num_displays = 0; - SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); - float monitor_hz = 60.0f; - if (displays && num_displays > 0) { - const auto* dm = SDL_GetCurrentDisplayMode(displays[0]); - if (dm && dm->refresh_rate > 0) monitor_hz = dm->refresh_rate; - SDL_free(displays); - } - - // Ocultar ventana y desactivar V-sync para medición limpia - SDL_HideWindow(window_); - SDL_SetRenderVSync(renderer_, 0); - - const int BENCH_DURATION_MS = 600; - const int WARMUP_FRAMES = 5; - - SimulationMode original_mode = current_mode_; - - auto restore = [&]() { - SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0); - SDL_ShowWindow(window_); - current_mode_ = original_mode; - active_shape_.reset(); - scene_manager_->changeScenario(0, original_mode); - last_frame_time_ = 0; - }; - - // Test escenario custom (independiente de max_auto_scenario_) - custom_auto_available_ = false; - if (custom_scenario_enabled_) { - scene_manager_->changeScenario(CUSTOM_SCENARIO_IDX, SimulationMode::SHAPE); - activateShapeInternal(ShapeType::SPHERE); - last_frame_time_ = 0; - for (int w = 0; w < WARMUP_FRAMES; ++w) { - calculateDeltaTime(); - SDL_Event e; while (SDL_PollEvent(&e)) {} - update(); - render(); - } - int frame_count = 0; - Uint64 start = SDL_GetTicks(); - while (SDL_GetTicks() - start < static_cast(BENCH_DURATION_MS)) { - calculateDeltaTime(); - SDL_Event e; while (SDL_PollEvent(&e)) {} - update(); - render(); - ++frame_count; - } - float fps = static_cast(frame_count) / (BENCH_DURATION_MS / 1000.0f); - custom_auto_available_ = (fps >= monitor_hz); - } - - // Probar de más pesado a más ligero - for (int idx = DEMO_AUTO_MAX_SCENARIO; idx >= DEMO_AUTO_MIN_SCENARIO; --idx) { - scene_manager_->changeScenario(idx, SimulationMode::SHAPE); - activateShapeInternal(ShapeType::SPHERE); - - // Warmup: estabilizar física y pipeline GPU - last_frame_time_ = 0; - for (int w = 0; w < WARMUP_FRAMES; ++w) { - calculateDeltaTime(); - SDL_Event e; while (SDL_PollEvent(&e)) {} - update(); - render(); - } - - // Medición - int frame_count = 0; - Uint64 start = SDL_GetTicks(); - while (SDL_GetTicks() - start < static_cast(BENCH_DURATION_MS)) { - calculateDeltaTime(); - SDL_Event e; - while (SDL_PollEvent(&e)) { /* descartar */ } - update(); - render(); - ++frame_count; - } - - float measured_fps = static_cast(frame_count) / (BENCH_DURATION_MS / 1000.0f); - if (measured_fps >= monitor_hz) { - max_auto_scenario_ = idx; - restore(); - return; - } - } - // Fallback: escenario mínimo - max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO; - restore(); +void Engine::enterShapeMode(ShapeType type) { + activateShapeInternal(type); } -// ============================================================================ -// 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 (LOGO_MIN_SCENARIO_IDX = índice 4 → 1000 bolas) - if (scene_manager_->getCurrentScenario() < LOGO_MIN_SCENARIO_IDX) { - scene_manager_->changeScenario(LOGO_MIN_SCENARIO_IDX, current_mode_); - } - - // 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(active_shape_.get()); - if (png_shape) { - png_shape->setLogoMode(true); - png_shape->resetFlipCount(); // Resetear contador de flips - } - } +void Engine::exitShapeMode(bool force_gravity) { + toggleShapeModeInternal(force_gravity); } -void Engine::executeExitLogoMode() { - // Restaurar estado visual previo - theme_manager_->switchToTheme(logo_previous_theme_); +void Engine::switchTextureSilent() { + switchTextureInternal(false); +} - 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(); - - // Activar atracción física si estamos en modo SHAPE - // (crítico para que las bolas se muevan hacia la figura restaurada) - if (current_mode_ == SimulationMode::SHAPE) { - auto& balls = scene_manager_->getBallsMutable(); - for (auto& ball : balls) { - ball->enableShapeAttraction(true); - } - } - - // Desactivar modo LOGO en PNG_SHAPE (volver a flip intervals normales) - if (active_shape_) { - PNGShape* png_shape = dynamic_cast(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]); - } +void Engine::setTextureByIndex(size_t index) { + if (index >= textures_.size()) return; + current_texture_index_ = index; + texture_ = textures_[current_texture_index_]; + int new_size = texture_->getWidth(); + current_ball_size_ = new_size; + scene_manager_->updateBallTexture(texture_, new_size); } // Toggle manual del Modo Logo (tecla K) @@ -1889,208 +1150,115 @@ void Engine::switchTextureInternal(bool show_notification) { } // ============================================================================ -// Sistema de Figuras 3D - IMPLEMENTACIÓN PARA CALLBACKS DEMO/LOGO +// Sistema de Figuras 3D - THIN WRAPPERS (delegan a ShapeManager) // ============================================================================ -// 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(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"); - } - } + shape_manager_->toggleShapeMode(force_gravity_on_exit); + current_mode_ = shape_manager_->getCurrentMode(); } -// 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 + shape_manager_->activateShape(type); 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(); - break; - case ShapeType::CUBE: - active_shape_ = std::make_unique(); - break; - case ShapeType::HELIX: - active_shape_ = std::make_unique(); - break; - case ShapeType::TORUS: - active_shape_ = std::make_unique(); - break; - case ShapeType::LISSAJOUS: - active_shape_ = std::make_unique(); - break; - case ShapeType::CYLINDER: - active_shape_ = std::make_unique(); - break; - case ShapeType::ICOSAHEDRON: - active_shape_ = std::make_unique(); - break; - case ShapeType::ATOM: - active_shape_ = std::make_unique(); - break; - case ShapeType::PNG_SHAPE: - active_shape_ = std::make_unique("shapes/jailgames.png"); - break; - default: - active_shape_ = std::make_unique(); // 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(scene_manager_->getBallCount()); - active_shape_->generatePoints(num_points, static_cast(current_screen_width_), static_cast(current_screen_height_)); + shape_manager_->generateShape(); } -// Actualizar figura activa (rotación, animación, etc.) void Engine::updateShape() { - if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) return; + shape_manager_->update(delta_time_); +} - // Actualizar animación de la figura - active_shape_->update(delta_time_, static_cast(current_screen_width_), static_cast(current_screen_height_)); +// ============================================================================ +// BENCHMARK DE RENDIMIENTO +// ============================================================================ - // Obtener factor de escala para física (base de figura + escala manual) - float scale_factor = active_shape_->getScaleFactor(static_cast(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(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); +void Engine::runPerformanceBenchmark() { + int num_displays = 0; + SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); + float monitor_hz = 60.0f; + if (displays && num_displays > 0) { + const auto* dm = SDL_GetCurrentDisplayMode(displays[0]); + if (dm && dm->refresh_rate > 0) monitor_hz = dm->refresh_rate; + SDL_free(displays); } - // 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) + SDL_HideWindow(window_); + SDL_SetRenderVSync(renderer_, 0); - for (const auto& ball : balls) { - if (ball->getDistanceToTarget() < distance_threshold) { - balls_near++; - } + const int BENCH_DURATION_MS = 600; + const int WARMUP_FRAMES = 5; + + SimulationMode original_mode = current_mode_; + + auto restore = [&]() { + SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0); + SDL_ShowWindow(window_); + current_mode_ = original_mode; + if (shape_manager_->isShapeModeActive()) { + shape_manager_->toggleShapeMode(false); + } + scene_manager_->changeScenario(0, original_mode); + last_frame_time_ = 0; + }; + + custom_auto_available_ = false; + if (custom_scenario_enabled_) { + scene_manager_->changeScenario(CUSTOM_SCENARIO_IDX, SimulationMode::SHAPE); + activateShapeInternal(ShapeType::SPHERE); + last_frame_time_ = 0; + for (int w = 0; w < WARMUP_FRAMES; ++w) { + calculateDeltaTime(); + SDL_Event e; while (SDL_PollEvent(&e)) {} + update(); + render(); + } + int frame_count = 0; + Uint64 start = SDL_GetTicks(); + while (SDL_GetTicks() - start < static_cast(BENCH_DURATION_MS)) { + calculateDeltaTime(); + SDL_Event e; while (SDL_PollEvent(&e)) {} + update(); + render(); + ++frame_count; + } + float fps = static_cast(frame_count) / (BENCH_DURATION_MS / 1000.0f); + custom_auto_available_ = (fps >= monitor_hz); + } + + for (int idx = DEMO_AUTO_MAX_SCENARIO; idx >= DEMO_AUTO_MIN_SCENARIO; --idx) { + scene_manager_->changeScenario(idx, SimulationMode::SHAPE); + activateShapeInternal(ShapeType::SPHERE); + + last_frame_time_ = 0; + for (int w = 0; w < WARMUP_FRAMES; ++w) { + calculateDeltaTime(); + SDL_Event e; while (SDL_PollEvent(&e)) {} + update(); + render(); } - shape_convergence_ = static_cast(balls_near) / scene_manager_->getBallCount(); + int frame_count = 0; + Uint64 start = SDL_GetTicks(); + while (SDL_GetTicks() - start < static_cast(BENCH_DURATION_MS)) { + calculateDeltaTime(); + SDL_Event e; + while (SDL_PollEvent(&e)) {} + update(); + render(); + ++frame_count; + } - // 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_); + float measured_fps = static_cast(frame_count) / (BENCH_DURATION_MS / 1000.0f); + if (measured_fps >= monitor_hz) { + max_auto_scenario_ = idx; + restore(); + return; + } } -} -// 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_)); + max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO; + restore(); } \ No newline at end of file diff --git a/source/engine.hpp b/source/engine.hpp index 01bd665..cae8ba6 100644 --- a/source/engine.hpp +++ b/source/engine.hpp @@ -10,14 +10,13 @@ #include // for string #include // for vector -#include "app_logo.hpp" // for AppLogo +#include "ui/app_logo.hpp" // for AppLogo #include "ball.hpp" // for Ball #include "boids_mgr/boid_manager.hpp" // for BoidManager #include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType #include "external/texture.hpp" // for Texture #include "input/input_handler.hpp" // for InputHandler #include "scene/scene_manager.hpp" // for SceneManager -#include "shapes/shape.hpp" // for Shape (interfaz polimórfica) #include "shapes_mgr/shape_manager.hpp" // for ShapeManager #include "state/state_manager.hpp" // for StateManager #include "theme_manager.hpp" // for ThemeManager @@ -93,17 +92,11 @@ class Engine { void toggleDemoLiteMode(); void toggleLogoMode(); - // === Métodos públicos para StateManager (callbacks) === - // NOTA: StateManager coordina estados, Engine proporciona implementación - // Estos callbacks permiten que StateManager ejecute acciones complejas que - // requieren acceso a múltiples componentes (SceneManager, ThemeManager, ShapeManager, etc.) - // Este enfoque es pragmático y mantiene la separación de responsabilidades limpia - void performLogoAction(bool logo_waiting_for_flip); - void executeDemoAction(bool is_lite); - void executeRandomizeOnDemoStart(bool is_lite); - void executeToggleGravityOnOff(); - void executeEnterLogoMode(size_t ball_count); - void executeExitLogoMode(); + // === Métodos públicos para StateManager (automatización DEMO/LOGO sin notificación) === + void enterShapeMode(ShapeType type); // Activar figura (sin notificación) + void exitShapeMode(bool force_gravity = true); // Volver a física (sin notificación) + void switchTextureSilent(); // Cambiar textura (sin notificación) + void setTextureByIndex(size_t index); // Restaurar textura específica // === Getters públicos para UIManager (Debug HUD) === bool getVSyncEnabled() const { return vsync_enabled_; } @@ -119,6 +112,7 @@ class Engine { int getBaseScreenWidth() const { return base_screen_width_; } int getBaseScreenHeight() const { return base_screen_height_; } int getMaxAutoScenario() const { return max_auto_scenario_; } + size_t getCurrentTextureIndex() const { return current_texture_index_; } private: // === Componentes del sistema (Composición) === @@ -172,22 +166,13 @@ class Engine { std::unique_ptr theme_manager_; int theme_page_ = 0; // Página actual de temas (0 o 1) para acceso por Numpad - // Sistema de Figuras 3D (polimórfico) - // NOTA: Engine mantiene implementación de figuras usada por callbacks DEMO/LOGO - // ShapeManager tiene implementación paralela para controles manuales del usuario + // Modo de simulación actual (PHYSICS/SHAPE/BOIDS) — fuente de verdad para Engine + // El estado de figuras (active_shape_, scale, etc.) está en ShapeManager SimulationMode current_mode_ = SimulationMode::PHYSICS; - ShapeType current_shape_type_ = ShapeType::SPHERE; // Tipo de figura actual - ShapeType last_shape_type_ = ShapeType::SPHERE; // Última figura para toggle F - std::unique_ptr active_shape_; // Puntero polimórfico a figura activa - float shape_scale_factor_ = 1.0f; // Factor de escala manual (Numpad +/-) - bool depth_zoom_enabled_ = true; // Zoom por profundidad Z activado // Sistema de Modo DEMO (auto-play) y LOGO - // NOTA: Engine mantiene estado de implementación para callbacks performLogoAction() - // StateManager coordina los triggers y timers, Engine ejecuta las acciones - float demo_timer_ = 0.0f; // Contador de tiempo para próxima acción - float demo_next_action_time_ = 0.0f; // Tiempo aleatorio hasta próxima acción (segundos) - int max_auto_scenario_ = 5; // Índice máximo en modos auto (default conservador: 5000 bolas) + // Toda la lógica DEMO/LOGO y su estado vive en StateManager + int max_auto_scenario_ = 5; // Índice máximo en modos auto (resultado del benchmark) // Escenario custom (--custom-balls) int custom_scenario_balls_ = 0; @@ -195,30 +180,6 @@ class Engine { bool custom_auto_available_ = false; bool skip_benchmark_ = false; - // Sistema de convergencia para LOGO MODE (escala con resolución) - // Usado por performLogoAction() para detectar cuando las bolas forman el logo - float shape_convergence_ = 0.0f; // % de pelotas cerca del objetivo (0.0-1.0) - float logo_convergence_threshold_ = 0.90f; // Threshold aleatorio (75-100%) - float logo_min_time_ = 3.0f; // Tiempo mínimo escalado con resolución - float logo_max_time_ = 5.0f; // Tiempo máximo escalado (backup) - - // Sistema de espera de flips en LOGO MODE (camino alternativo) - // Permite que LOGO espere a que ocurran rotaciones antes de cambiar estado - bool logo_waiting_for_flip_ = false; // true si eligió el camino "esperar flip" - int logo_target_flip_number_ = 0; // En qué flip actuar (1, 2 o 3) - float logo_target_flip_percentage_ = 0.0f; // % de flip a esperar (0.2-0.8) - int logo_current_flip_count_ = 0; // Flips observados hasta ahora - - // NOTA: logo_entered_manually_ fue eliminado de Engine (duplicado) - // Ahora se obtiene de StateManager con state_manager_->getLogoEnteredManually() - // Esto evita desincronización entre Engine y StateManager - - // Estado previo antes de entrar a Logo Mode (para restaurar al salir) - // Guardado por executeEnterLogoMode(), restaurado por executeExitLogoMode() - int logo_previous_theme_ = 0; // Índice de tema (0-9) - size_t logo_previous_texture_index_ = 0; - float logo_previous_shape_scale_ = 1.0f; - // Batch rendering std::vector batch_vertices_; std::vector batch_indices_; @@ -255,12 +216,9 @@ class Engine { // Rendering void addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale = 1.0f); - // Sistema de Figuras 3D - Métodos privados - // NOTA FASE 7: Métodos DUPLICADOS con ShapeManager (Engine mantiene implementación para DEMO/LOGO) - // TODO FASE 8: Convertir en wrappers puros cuando migremos DEMO/LOGO - void toggleShapeModeInternal(bool force_gravity_on_exit = true); // Implementación interna del toggle - void activateShapeInternal(ShapeType type); // Implementación interna de activación - void updateShape(); // Actualizar figura activa - void generateShape(); // Generar puntos de figura activa - void clampShapeScale(); // Limitar escala para evitar clipping + // Sistema de Figuras 3D - Métodos privados (thin wrappers a ShapeManager) + void toggleShapeModeInternal(bool force_gravity_on_exit = true); // Delega a ShapeManager + sincroniza current_mode_ + void activateShapeInternal(ShapeType type); // Delega a ShapeManager + sets current_mode_ = SHAPE + void updateShape(); // Delega a ShapeManager::update() + void generateShape(); // Delega a ShapeManager::generateShape() }; diff --git a/source/scene/scene_manager.cpp b/source/scene/scene_manager.cpp index e579aad..52a048c 100644 --- a/source/scene/scene_manager.cpp +++ b/source/scene/scene_manager.cpp @@ -239,3 +239,15 @@ void SceneManager::updateBallSizes(int old_size, int new_size) { ball->updateSize(new_size); } } + +void SceneManager::enableShapeAttractionAll(bool enabled) { + for (auto& ball : balls_) { + ball->enableShapeAttraction(enabled); + } +} + +void SceneManager::resetDepthScalesAll() { + for (auto& ball : balls_) { + ball->setDepthScale(1.0f); + } +} diff --git a/source/scene/scene_manager.hpp b/source/scene/scene_manager.hpp index d43ea0b..881128e 100644 --- a/source/scene/scene_manager.hpp +++ b/source/scene/scene_manager.hpp @@ -109,11 +109,22 @@ class SceneManager { const std::vector>& getBalls() const { return balls_; } /** - * @brief Obtiene referencia mutable al vector de bolas (para ShapeManager) + * @brief Obtiene referencia mutable al vector de bolas (para ShapeManager/BoidManager) * NOTA: Usar con cuidado, solo para sistemas que necesitan modificar estado de bolas */ std::vector>& getBallsMutable() { return balls_; } + /** + * @brief Activa o desactiva la atracción de figura en todas las bolas + * @param enabled true para activar, false para desactivar + */ + void enableShapeAttractionAll(bool enabled); + + /** + * @brief Resetea la escala de profundidad a 1.0 en todas las bolas + */ + void resetDepthScalesAll(); + /** * @brief Obtiene número total de bolas */ diff --git a/source/shapes_mgr/shape_manager.cpp b/source/shapes_mgr/shape_manager.cpp index 7f43985..81a6048 100644 --- a/source/shapes_mgr/shape_manager.cpp +++ b/source/shapes_mgr/shape_manager.cpp @@ -74,24 +74,17 @@ void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) { } } - // Si estamos en LOGO MODE, generar threshold aleatorio de convergencia (75-100%) + // Si estamos en LOGO MODE, resetear convergencia al entrar if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO) { - /* - float logo_convergence_threshold = LOGO_CONVERGENCE_MIN + - (rand() % 1000) / 1000.0f * (LOGO_CONVERGENCE_MAX - LOGO_CONVERGENCE_MIN); - */ - shape_convergence_ = 0.0f; // Reset convergencia al entrar + shape_convergence_ = 0.0f; } } else { // Volver a modo física normal current_mode_ = SimulationMode::PHYSICS; // Desactivar atracción y resetear escala de profundidad - auto& balls = scene_mgr_->getBallsMutable(); - for (auto& ball : balls) { - ball->enableShapeAttraction(false); - ball->setDepthScale(1.0f); // Reset escala a 100% (evita "pop" visual) - } + scene_mgr_->enableShapeAttractionAll(false); + scene_mgr_->resetDepthScalesAll(); // Reset escala a 100% (evita "pop" visual) // Activar gravedad al salir (solo si se especifica) if (force_gravity_on_exit) { @@ -280,10 +273,7 @@ void ShapeManager::activateShapeInternal(ShapeType type) { generateShape(); // Activar atracción física en todas las pelotas - auto& balls = scene_mgr_->getBallsMutable(); - for (auto& ball : balls) { - ball->enableShapeAttraction(true); - } + scene_mgr_->enableShapeAttractionAll(true); // Mostrar notificación con nombre de figura (solo si NO estamos en modo demo o logo) if (active_shape_ && state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) { @@ -292,6 +282,11 @@ void ShapeManager::activateShapeInternal(ShapeType type) { } } +void ShapeManager::setShapeScaleFactor(float scale) { + shape_scale_factor_ = scale; + clampShapeScale(); +} + void ShapeManager::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 diff --git a/source/shapes_mgr/shape_manager.hpp b/source/shapes_mgr/shape_manager.hpp index 59ccdf2..0953b38 100644 --- a/source/shapes_mgr/shape_manager.hpp +++ b/source/shapes_mgr/shape_manager.hpp @@ -133,6 +133,18 @@ class ShapeManager { */ float getConvergence() const { return shape_convergence_; } + /** + * @brief Establece la escala de figura y aplica clamping + * @param scale Nuevo factor de escala (se limitará a rango válido) + */ + void setShapeScaleFactor(float scale); + + /** + * @brief Establece el estado del zoom por profundidad Z + * @param enabled true para activar, false para desactivar + */ + void setDepthZoomEnabled(bool enabled) { depth_zoom_enabled_ = enabled; } + private: // === Referencias a otros componentes === Engine* engine_; // Callback al Engine (legacy - temporal) diff --git a/source/state/state_manager.cpp b/source/state/state_manager.cpp index 1012e4f..b6a8bdb 100644 --- a/source/state/state_manager.cpp +++ b/source/state/state_manager.cpp @@ -1,13 +1,21 @@ #include "state_manager.hpp" -#include // for rand +#include // for std::min +#include // for rand +#include // for std::vector -#include "defines.hpp" // for constantes DEMO/LOGO -#include "engine.hpp" // for Engine (callbacks) -#include "shapes/png_shape.hpp" // for PNGShape flip detection +#include "defines.hpp" // for constantes DEMO/LOGO +#include "engine.hpp" // for Engine (enter/exitShapeMode, texture) +#include "scene/scene_manager.hpp" // for SceneManager +#include "shapes_mgr/shape_manager.hpp" // for ShapeManager +#include "shapes/png_shape.hpp" // for PNGShape flip detection +#include "theme_manager.hpp" // for ThemeManager StateManager::StateManager() : engine_(nullptr) + , scene_mgr_(nullptr) + , theme_mgr_(nullptr) + , shape_mgr_(nullptr) , current_app_mode_(AppMode::SANDBOX) , previous_app_mode_(AppMode::SANDBOX) , demo_timer_(0.0f) @@ -28,8 +36,11 @@ StateManager::StateManager() StateManager::~StateManager() { } -void StateManager::initialize(Engine* engine) { +void StateManager::initialize(Engine* engine, SceneManager* scene_mgr, ThemeManager* theme_mgr, ShapeManager* shape_mgr) { engine_ = engine; + scene_mgr_ = scene_mgr; + theme_mgr_ = theme_mgr; + shape_mgr_ = shape_mgr; } void StateManager::setLogoPreviousState(int theme, size_t texture_index, float shape_scale) { @@ -39,48 +50,39 @@ void StateManager::setLogoPreviousState(int theme, size_t texture_index, float s } // =========================================================================== -// ACTUALIZACIÓN DE ESTADOS - Migrado desde Engine::updateDemoMode() +// ACTUALIZACIÓN DE ESTADOS // =========================================================================== void StateManager::update(float delta_time, float shape_convergence, Shape* active_shape) { - // Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO) if (current_app_mode_ == AppMode::SANDBOX) return; - // Actualizar timer demo_timer_ += delta_time; - // Determinar si es hora de ejecutar acción (depende del modo) bool should_trigger = false; if (current_app_mode_ == AppMode::LOGO) { - // LOGO MODE: Dos caminos posibles if (logo_waiting_for_flip_) { // CAMINO B: Esperando a que ocurran flips - // Obtener referencia a PNGShape si está activa PNGShape* png_shape = dynamic_cast(active_shape); 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! + should_trigger = true; } } } } } else { - // CAMINO A: Esperar convergencia + tiempo (comportamiento original) + // CAMINO A: Esperar convergencia + tiempo 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_; @@ -88,34 +90,98 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti should_trigger = (min_time_reached && convergence_ok) || max_time_reached; } } else { - // DEMO/DEMO_LITE: Timer simple como antes should_trigger = demo_timer_ >= demo_next_action_time_; } - // Si es hora de ejecutar acción - if (should_trigger) { - // MODO LOGO: Sistema de acciones variadas con gravedad dinámica - if (current_app_mode_ == AppMode::LOGO) { - // Llamar a Engine para ejecutar acciones de LOGO - // TODO FASE 9: Mover lógica de acciones LOGO desde Engine a StateManager - if (engine_) { - engine_->performLogoAction(logo_waiting_for_flip_); + if (!should_trigger) return; + + if (current_app_mode_ == AppMode::LOGO) { + // LOGO MODE: Sistema de acciones variadas con gravedad dinámica + int action = rand() % 100; + + if (shape_mgr_->isShapeModeActive()) { + // Logo quieto (formado) → Decidir camino a seguir + if (logo_waiting_for_flip_) { + // Ya estábamos esperando flips → hacer el cambio SHAPE → PHYSICS + if (action < 50) { + engine_->exitShapeMode(true); // Con gravedad ON + } else { + engine_->exitShapeMode(false); // Con gravedad OFF + } + + logo_waiting_for_flip_ = false; + logo_current_flip_count_ = 0; + + 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 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; + + PNGShape* png_shape = dynamic_cast(shape_mgr_->getActiveShape()); + if (png_shape) { + png_shape->resetFlipCount(); + } + // No hacer nada más — esperar a que ocurran los flips + } else { + // CAMINO A (50%): Cambio inmediato + if (action < 50) { + engine_->exitShapeMode(true); // SHAPE → PHYSICS con gravedad ON + } else { + engine_->exitShapeMode(false); // SHAPE → PHYSICS con gravedad OFF + } + + logo_waiting_for_flip_ = false; + logo_current_flip_count_ = 0; + + 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 + if (action < 50) { + // 50%: PHYSICS → SHAPE (reconstruir logo) + engine_->exitShapeMode(false); // toggleShapeMode: PHYSICS → SHAPE con last_type + + logo_waiting_for_flip_ = false; + logo_current_flip_count_ = 0; + } else if (action < 68) { + // 18%: Forzar gravedad ON + scene_mgr_->forceBallsGravityOn(); + } else if (action < 84) { + // 16%: Forzar gravedad OFF + scene_mgr_->forceBallsGravityOff(); + } else { + // 16%: Cambiar dirección de gravedad + GravityDirection new_direction = static_cast(rand() % 4); + scene_mgr_->changeGravityDirection(new_direction); + scene_mgr_->forceBallsGravityOn(); } - } - // MODO DEMO/DEMO_LITE: Acciones normales - else { - bool is_lite = (current_app_mode_ == AppMode::DEMO_LITE); - performDemoAction(is_lite); - // Resetear timer y calcular próximo intervalo aleatorio demo_timer_ = 0.0f; - - // Usar intervalos diferentes según modo - float interval_min = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN; - float interval_max = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX; - float interval_range = interval_max - interval_min; - demo_next_action_time_ = interval_min + (rand() % 1000) / 1000.0f * interval_range; + float interval_range = logo_max_time_ - logo_min_time_; + demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range; } + + // Salir automáticamente si la entrada fue automática (desde DEMO) + if (!logo_entered_manually_ && rand() % 100 < 60) { + exitLogoMode(true); // Volver a DEMO/DEMO_LITE + } + } else { + // DEMO/DEMO_LITE: Acciones normales + bool is_lite = (current_app_mode_ == AppMode::DEMO_LITE); + performDemoAction(is_lite); + + demo_timer_ = 0.0f; + 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; } } @@ -132,15 +198,12 @@ void StateManager::setState(AppMode new_mode, int current_screen_width, int curr current_app_mode_ = new_mode; - // Resetear timer al cambiar modo demo_timer_ = 0.0f; - // Configurar timer de demo según el modo if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) { 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; @@ -162,7 +225,7 @@ void StateManager::toggleDemoMode(int current_screen_width, int current_screen_h setState(AppMode::SANDBOX, current_screen_width, current_screen_height); } else { setState(AppMode::DEMO, current_screen_width, current_screen_height); - randomizeOnDemoStart(false); // Randomizar estado al entrar + randomizeOnDemoStart(false); } } @@ -171,70 +234,319 @@ void StateManager::toggleDemoLiteMode(int current_screen_width, int current_scre setState(AppMode::SANDBOX, current_screen_width, current_screen_height); } else { setState(AppMode::DEMO_LITE, current_screen_width, current_screen_height); - randomizeOnDemoStart(true); // Randomizar estado al entrar + randomizeOnDemoStart(true); } } void StateManager::toggleLogoMode(int current_screen_width, int current_screen_height, size_t ball_count) { if (current_app_mode_ == AppMode::LOGO) { - exitLogoMode(false); // Salir de LOGO manualmente + exitLogoMode(false); } else { - enterLogoMode(false, current_screen_width, current_screen_height, ball_count); // Entrar manualmente + enterLogoMode(false, current_screen_width, current_screen_height, ball_count); } } // =========================================================================== -// ACCIONES DE DEMO - Migrado desde Engine::performDemoAction() +// ACCIONES DE DEMO // =========================================================================== void StateManager::performDemoAction(bool is_lite) { + if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return; + // ============================================ // SALTO AUTOMÁTICO A LOGO MODE (Easter Egg) // ============================================ - // Obtener información necesaria desde Engine via callbacks - // (En el futuro, se podría pasar como parámetros al método) - if (!engine_) return; + if (is_lite) { + if (static_cast(scene_mgr_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX] && + theme_mgr_->getCurrentThemeIndex() == 5) { // MONOCHROME + if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) { + enterLogoMode(true, 0, 0, scene_mgr_->getBallCount()); + return; + } + } + } else { + if (static_cast(scene_mgr_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX]) { + if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) { + enterLogoMode(true, 0, 0, scene_mgr_->getBallCount()); + return; + } + } + } - // TODO FASE 9: Eliminar callbacks a Engine y pasar parámetros necesarios + // ============================================ + // ACCIONES NORMALES DE DEMO/DEMO_LITE + // ============================================ - // Por ahora, delegar las acciones DEMO completas a Engine - // ya que necesitan acceso a múltiples componentes (SceneManager, ThemeManager, etc.) - engine_->executeDemoAction(is_lite); + int TOTAL_WEIGHT; + int random_value; + int accumulated_weight = 0; + + if (is_lite) { + 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(rand() % 4); + scene_mgr_->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 + 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}; + engine_->enterShapeMode(shapes[rand() % 8]); + return; + } + + // Toggle física ↔ figura (20%) + accumulated_weight += DEMO_LITE_WEIGHT_TOGGLE_PHYSICS; + if (random_value < accumulated_weight) { + engine_->exitShapeMode(false); + return; + } + + // Aplicar impulso (10%) + accumulated_weight += DEMO_LITE_WEIGHT_IMPULSE; + if (random_value < accumulated_weight) { + scene_mgr_->pushBallsAwayFromGravity(); + return; + } + + } else { + 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(rand() % 4); + scene_mgr_->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 + 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}; + engine_->enterShapeMode(shapes[rand() % 8]); + return; + } + + // Toggle física ↔ figura (12%) + accumulated_weight += DEMO_WEIGHT_TOGGLE_PHYSICS; + if (random_value < accumulated_weight) { + engine_->exitShapeMode(false); + return; + } + + // Re-generar misma figura (8%) + accumulated_weight += DEMO_WEIGHT_REGENERATE_SHAPE; + if (random_value < accumulated_weight) { + if (shape_mgr_->isShapeModeActive()) { + shape_mgr_->generateShape(); + } + return; + } + + // Cambiar tema (15%) + accumulated_weight += DEMO_WEIGHT_THEME; + if (random_value < accumulated_weight) { + theme_mgr_->switchToTheme(rand() % 15); + return; + } + + // Cambiar escenario (10%) + accumulated_weight += DEMO_WEIGHT_SCENARIO; + if (random_value < accumulated_weight) { + int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO); + std::vector candidates; + for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) + candidates.push_back(i); + if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) + candidates.push_back(CUSTOM_SCENARIO_IDX); + int new_scenario = candidates[rand() % candidates.size()]; + SimulationMode current_sim_mode = shape_mgr_->getCurrentMode(); + scene_mgr_->changeScenario(new_scenario, current_sim_mode); + + if (shape_mgr_->isShapeModeActive()) { + shape_mgr_->generateShape(); + scene_mgr_->enableShapeAttractionAll(true); + } + return; + } + + // Aplicar impulso (10%) + accumulated_weight += DEMO_WEIGHT_IMPULSE; + if (random_value < accumulated_weight) { + scene_mgr_->pushBallsAwayFromGravity(); + return; + } + + // Toggle profundidad (3%) + accumulated_weight += DEMO_WEIGHT_DEPTH_ZOOM; + if (random_value < accumulated_weight) { + if (shape_mgr_->isShapeModeActive()) { + shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled()); + } + return; + } + + // Cambiar escala de figura (2%) + accumulated_weight += DEMO_WEIGHT_SHAPE_SCALE; + if (random_value < accumulated_weight) { + if (shape_mgr_->isShapeModeActive()) { + int scale_action = rand() % 3; + if (scale_action == 0) { + shape_mgr_->setShapeScaleFactor(shape_mgr_->getShapeScaleFactor() + SHAPE_SCALE_STEP); + } else if (scale_action == 1) { + shape_mgr_->setShapeScaleFactor(shape_mgr_->getShapeScaleFactor() - SHAPE_SCALE_STEP); + } else { + shape_mgr_->setShapeScaleFactor(SHAPE_SCALE_DEFAULT); + } + shape_mgr_->generateShape(); + } + return; + } + + // Cambiar sprite (2%) + accumulated_weight += DEMO_WEIGHT_SPRITE; + if (random_value < accumulated_weight) { + engine_->switchTextureSilent(); + return; + } + } } // =========================================================================== -// RANDOMIZACIÓN AL INICIAR DEMO - Migrado desde Engine::randomizeOnDemoStart() +// RANDOMIZACIÓN AL INICIAR DEMO // =========================================================================== void StateManager::randomizeOnDemoStart(bool is_lite) { - // Delegar a Engine para randomización completa - // TODO FASE 9: Implementar lógica completa aquí - if (engine_) { - engine_->executeRandomizeOnDemoStart(is_lite); + if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return; + + // Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente + if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) { + ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, + ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, + ShapeType::ICOSAHEDRON, ShapeType::ATOM}; + engine_->enterShapeMode(shapes[rand() % 8]); + } + + if (is_lite) { + // DEMO LITE: Solo randomizar física/figura + gravedad + if (rand() % 2 == 0) { + if (shape_mgr_->isShapeModeActive()) { + engine_->exitShapeMode(false); + } + } else { + ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; + engine_->enterShapeMode(shapes[rand() % 8]); + } + + GravityDirection new_direction = static_cast(rand() % 4); + scene_mgr_->changeGravityDirection(new_direction); + if (rand() % 2 == 0) { + toggleGravityOnOff(); + } + + } else { + // DEMO COMPLETO: Randomizar TODO + + // 1. Física o Figura + if (rand() % 2 == 0) { + if (shape_mgr_->isShapeModeActive()) { + engine_->exitShapeMode(false); + } + } else { + ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; + ShapeType selected_shape = shapes[rand() % 8]; + + // Randomizar profundidad y escala ANTES de activar la figura + if (rand() % 2 == 0) { + shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled()); + } + shape_mgr_->setShapeScaleFactor(0.5f + (rand() % 1500) / 1000.0f); + + engine_->enterShapeMode(selected_shape); + } + + // 2. Escenario + int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO); + std::vector candidates; + for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) + candidates.push_back(i); + if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) + candidates.push_back(CUSTOM_SCENARIO_IDX); + int new_scenario = candidates[rand() % candidates.size()]; + SimulationMode current_sim_mode = shape_mgr_->getCurrentMode(); + scene_mgr_->changeScenario(new_scenario, current_sim_mode); + + if (shape_mgr_->isShapeModeActive()) { + shape_mgr_->generateShape(); + scene_mgr_->enableShapeAttractionAll(true); + } + + // 3. Tema + theme_mgr_->switchToTheme(rand() % 15); + + // 4. Sprite + if (rand() % 2 == 0) { + engine_->switchTextureSilent(); + } + + // 5. Gravedad + GravityDirection new_direction = static_cast(rand() % 4); + scene_mgr_->changeGravityDirection(new_direction); + if (rand() % 3 == 0) { + toggleGravityOnOff(); + } } } // =========================================================================== -// TOGGLE GRAVEDAD (para DEMO) - Migrado desde Engine::toggleGravityOnOff() +// TOGGLE GRAVEDAD (para DEMO) // =========================================================================== void StateManager::toggleGravityOnOff() { - // Delegar a Engine temporalmente - if (engine_) { - engine_->executeToggleGravityOnOff(); + if (!scene_mgr_) return; + + bool gravity_enabled = scene_mgr_->hasBalls() && + (scene_mgr_->getFirstBall()->getGravityForce() > 0.0f); + + if (gravity_enabled) { + scene_mgr_->forceBallsGravityOff(); + } else { + scene_mgr_->forceBallsGravityOn(); } } // =========================================================================== -// ENTRAR AL MODO LOGO - Migrado desde Engine::enterLogoMode() +// ENTRAR AL MODO LOGO // =========================================================================== void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int current_screen_height, size_t ball_count) { - // Guardar si entrada fue manual (tecla K) o automática (desde DEMO) + if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return; + logo_entered_manually_ = !from_demo; - // Resetear variables de espera de flips logo_waiting_for_flip_ = false; logo_target_flip_number_ = 0; logo_target_flip_percentage_ = 0.0f; @@ -243,34 +555,91 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c // Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente) setState(AppMode::LOGO, current_screen_width, current_screen_height); - // Delegar configuración visual a Engine - // TODO FASE 9: Mover configuración completa aquí - if (engine_) { - engine_->executeEnterLogoMode(ball_count); + // Verificar mínimo de pelotas + if (scene_mgr_->getCurrentScenario() < LOGO_MIN_SCENARIO_IDX) { + scene_mgr_->changeScenario(LOGO_MIN_SCENARIO_IDX, shape_mgr_->getCurrentMode()); + } + + // Guardar estado previo (para restaurar al salir) + logo_previous_theme_ = theme_mgr_->getCurrentThemeIndex(); + logo_previous_texture_index_ = engine_->getCurrentTextureIndex(); + logo_previous_shape_scale_ = shape_mgr_->getShapeScaleFactor(); + + // Aplicar textura "small" si existe — buscar por nombre iterando índices + if (engine_->getCurrentTextureName() != "small") { + size_t original_idx = logo_previous_texture_index_; + bool found = false; + for (size_t i = 0; i < 20; ++i) { + engine_->setTextureByIndex(i); + if (engine_->getCurrentTextureName() == "small") { + found = true; + break; + } + if (engine_->getCurrentTextureName().empty()) { + break; + } + } + if (!found) { + engine_->setTextureByIndex(original_idx); + } + } + + // Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA + int logo_themes[] = {5, 6, 7, 8}; + theme_mgr_->switchToTheme(logo_themes[rand() % 4]); + + // Establecer escala a 120% + shape_mgr_->setShapeScaleFactor(LOGO_MODE_SHAPE_SCALE); + + // Activar PNG_SHAPE (el logo) + engine_->enterShapeMode(ShapeType::PNG_SHAPE); + + // Configurar PNG_SHAPE en modo LOGO + PNGShape* png_shape = dynamic_cast(shape_mgr_->getActiveShape()); + if (png_shape) { + png_shape->setLogoMode(true); + png_shape->resetFlipCount(); } } // =========================================================================== -// SALIR DEL MODO LOGO - Migrado desde Engine::exitLogoMode() +// SALIR DEL MODO LOGO // =========================================================================== void StateManager::exitLogoMode(bool return_to_demo) { if (current_app_mode_ != AppMode::LOGO) return; + if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return; - // Resetear flag de entrada manual logo_entered_manually_ = false; - // Delegar restauración visual a Engine - // TODO FASE 9: Mover lógica completa aquí - if (engine_) { - engine_->executeExitLogoMode(); + // Restaurar estado visual previo + theme_mgr_->switchToTheme(logo_previous_theme_); + engine_->setTextureByIndex(logo_previous_texture_index_); + shape_mgr_->setShapeScaleFactor(logo_previous_shape_scale_); + shape_mgr_->generateShape(); + + // Activar atracción física si estamos en modo SHAPE + if (shape_mgr_->isShapeModeActive()) { + scene_mgr_->enableShapeAttractionAll(true); + } + + // Desactivar modo LOGO en PNG_SHAPE + PNGShape* png_shape = dynamic_cast(shape_mgr_->getActiveShape()); + if (png_shape) { + png_shape->setLogoMode(false); + } + + // Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria + if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) { + ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, + ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, + ShapeType::ICOSAHEDRON, ShapeType::ATOM}; + engine_->enterShapeMode(shapes[rand() % 8]); } if (!return_to_demo) { - // Salida manual (tecla K): volver a SANDBOX setState(AppMode::SANDBOX, 0, 0); } else { - // Volver al modo previo (DEMO o DEMO_LITE) current_app_mode_ = previous_app_mode_; } } diff --git a/source/state/state_manager.hpp b/source/state/state_manager.hpp index 93e747d..5e4f53f 100644 --- a/source/state/state_manager.hpp +++ b/source/state/state_manager.hpp @@ -9,6 +9,9 @@ class Engine; class Shape; class PNGShape; +class SceneManager; +class ThemeManager; +class ShapeManager; /** * @class StateManager @@ -37,10 +40,13 @@ class StateManager { ~StateManager(); /** - * @brief Inicializa el StateManager con referencia al Engine - * @param engine Puntero al Engine (para callbacks) + * @brief Inicializa el StateManager con referencias a los subsistemas necesarios + * @param engine Puntero al Engine (para cambios de modo y texturas) + * @param scene_mgr Puntero a SceneManager (para control de bolas/gravedad) + * @param theme_mgr Puntero a ThemeManager (para cambios de tema) + * @param shape_mgr Puntero a ShapeManager (para figuras) */ - void initialize(Engine* engine); + void initialize(Engine* engine, SceneManager* scene_mgr, ThemeManager* theme_mgr, ShapeManager* shape_mgr); /** * @brief Actualiza la máquina de estados (timers, triggers, acciones) @@ -145,8 +151,11 @@ class StateManager { void exitLogoMode(bool return_to_demo); private: - // === Referencia al Engine (callback) === + // === Referencias a subsistemas === Engine* engine_; + SceneManager* scene_mgr_; + ThemeManager* theme_mgr_; + ShapeManager* shape_mgr_; // === Estado de aplicación === AppMode current_app_mode_;