From b8d3c60e5899074baf98dcc33ee758b26d174c7b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 10 Oct 2025 11:39:59 +0200 Subject: [PATCH] Refactor fase 1: Extraer InputHandler de Engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aplicación del Principio de Responsabilidad Única (SRP) - Fase 1/6 ## Cambios realizados ### Nuevos archivos - source/input/input_handler.h - Declaración clase InputHandler - source/input/input_handler.cpp - Procesamiento eventos SDL (~180 líneas) - REFACTOR_PLAN.md - Documento de seguimiento del refactor ### Modificaciones en Engine - **engine.h**: Agregados 24 métodos públicos para InputHandler - **engine.cpp**: - Eliminado handleEvents() (420 líneas) - Implementados métodos públicos wrapper (~180 líneas) - Renombrados métodos internos con sufijo `Internal`: * toggleShapeMode → toggleShapeModeInternal * activateShape → activateShapeInternal * switchTexture → switchTextureInternal - Bucle run() simplificado (5 → 12 líneas) ### Actualización build - CMakeLists.txt: Agregado source/input/*.cpp a archivos fuente ## Impacto - **Líneas extraídas**: ~430 del switch gigante de handleEvents() - **Compilación**: ✅ Exitosa sin errores - **Funcionalidad**: ✅ 100% preservada ## Beneficios - ✅ Engine desacoplado de eventos SDL - ✅ InputHandler stateless (fácilmente testeable) - ✅ Clara separación detección input vs ejecución lógica - ✅ Preparado para testing unitario de inputs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CMakeLists.txt | 2 +- REFACTOR_PLAN.md | 218 +++++++++++ source/engine.cpp | 645 +++++++++++---------------------- source/engine.h | 79 +++- source/input/input_handler.cpp | 266 ++++++++++++++ source/input/input_handler.h | 32 ++ 6 files changed, 778 insertions(+), 464 deletions(-) create mode 100644 REFACTOR_PLAN.md create mode 100644 source/input/input_handler.cpp create mode 100644 source/input/input_handler.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 271d7aa..1d03f8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ if (NOT SDL3_ttf_FOUND) endif() # Archivos fuente (excluir main_old.cpp) -file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/shapes/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp) +file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/input/*.cpp source/shapes/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp) list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp") # Comprobar si se encontraron archivos fuente diff --git a/REFACTOR_PLAN.md b/REFACTOR_PLAN.md new file mode 100644 index 0000000..0c34717 --- /dev/null +++ b/REFACTOR_PLAN.md @@ -0,0 +1,218 @@ +# Plan de Refactorización - ViBe3 Physics Engine + +## Objetivo +Aplicar el **Principio de Responsabilidad Única (SRP)** al motor Engine para: +- Mejorar mantenibilidad del código +- Facilitar extensión de funcionalidades +- Reducir acoplamiento entre sistemas +- Hacer el código más testeable + +## Métricas Iniciales (Pre-refactorización) +- **engine.cpp**: 2341 líneas +- **engine.h**: 196 líneas con 40+ miembros privados +- **Responsabilidades mezcladas**: 7 subsistemas en una sola clase + +## Progreso de Refactorización + +### ✅ FASE 1: InputHandler (COMPLETADA) +**Fecha**: 10/01/2025 +**Commit**: (pendiente) + +**Impacto**: ~430 líneas extraídas del `handleEvents()` + +**Archivos creados**: +- `source/input/input_handler.h` +- `source/input/input_handler.cpp` + +**Métodos públicos agregados a Engine (24 total)**: +```cpp +// Gravedad y física +void pushBallsAwayFromGravity(); +void handleGravityToggle(); +void handleGravityDirectionChange(GravityDirection, const char*); + +// Display y depuración +void toggleVSync(); +void toggleDebug(); + +// Figuras 3D +void toggleShapeMode(); +void activateShape(ShapeType, const char*); +void handleShapeScaleChange(bool); +void resetShapeScale(); +void toggleDepthZoom(); + +// Temas de colores +void cycleTheme(bool); +void switchThemeByNumpad(int); +void toggleThemePage(); +void pauseDynamicTheme(); + +// Sprites/Texturas +void switchTexture(); + +// Escenarios +void changeScenario(int, const char*); + +// Zoom y fullscreen +void handleZoomIn(); +void handleZoomOut(); +void toggleFullscreen(); +void toggleRealFullscreen(); +void toggleIntegerScaling(); + +// Modos de aplicación +void toggleDemoMode(); +void toggleDemoLiteMode(); +void toggleLogoMode(); +``` + +**Cambios internos**: +- Métodos internos renombrados con sufijo `Internal`: + - `toggleShapeMode()` → `toggleShapeModeInternal()` + - `activateShape()` → `activateShapeInternal()` + - `switchTexture()` → `switchTextureInternal()` +- Eliminado método `handleEvents()` (420 líneas) +- Bucle `run()` simplificado a 12 líneas + +**Beneficios**: +- ✅ Engine desacoplado de eventos SDL +- ✅ InputHandler stateless (fácilmente testeable) +- ✅ Clara separación entre detección de input y ejecución de lógica +- ✅ Compilación exitosa sin errores + +--- + +### 🔄 FASE 2: SceneManager (PENDIENTE) +**Impacto estimado**: ~500 líneas + `std::vector` movido + +**Responsabilidad**: Crear, actualizar y gestionar todas las `Ball` + +**Miembros a mover**: +- `std::vector> balls_` +- `GravityDirection current_gravity_` +- `int scenario_` + +**Métodos a mover**: +- `initBalls()` +- `pushBallsAwayFromGravity()` +- `switchBallsGravity()` +- `enableBallsGravityIfDisabled()` +- `forceBallsGravityOn() / Off()` +- `changeGravityDirection()` +- `updateBallSizes()` + +--- + +### 🔄 FASE 3: UIManager (PENDIENTE) +**Impacto estimado**: ~300 líneas + rendering de texto movido + +**Responsabilidad**: Renderizar y actualizar interfaz de usuario + +**Miembros a mover**: +- `Notifier notifier_` +- `TextRenderer text_renderer_debug_` +- `bool show_debug_` +- Variables FPS (`fps_frame_count_`, `fps_current_`, `fps_text_`, `vsync_text_`) + +**Métodos a mover**: +- `showNotificationForAction()` +- Renderizado de FPS, debug info, gravedad, tema, modo + +--- + +### 🔄 FASE 4: StateManager (PENDIENTE) +**Impacto estimado**: ~600 líneas de lógica compleja + +**Responsabilidad**: Gestionar máquina de estados (DEMO/LOGO/SANDBOX) + +**Miembros a mover**: +- `AppMode current_app_mode_, previous_app_mode_` +- Variables DEMO (`demo_timer_`, `demo_next_action_time_`) +- Variables LOGO (todas las relacionadas con logo mode) + +**Métodos a mover**: +- `setState()` +- `updateDemoMode()` +- `performDemoAction()` +- `randomizeOnDemoStart()` +- `enterLogoMode() / exitLogoMode()` + +--- + +### 🔄 FASE 5: ShapeManager (PENDIENTE) +**Impacto estimado**: ~400 líneas + lógica de shapes + +**Responsabilidad**: Crear, actualizar y renderizar figuras 3D polimórficas + +**Miembros a mover**: +- `SimulationMode current_mode_` +- `ShapeType current_shape_type_, last_shape_type_` +- `std::unique_ptr active_shape_` +- `float shape_scale_factor_` +- `bool depth_zoom_enabled_` + +**Métodos a mover**: +- `toggleShapeModeInternal()` +- `activateShapeInternal()` +- `updateShape()` +- `generateShape()` +- `clampShapeScale()` + +--- + +### 🔄 FASE 6: Limpieza y Consolidación Final (PENDIENTE) +**Impacto esperado**: Engine reducido a ~400 líneas (coordinador) + +**Tareas**: +1. Limpiar `engine.h` / `engine.cpp` de código legacy +2. Verificar que todos los sistemas están correctamente integrados +3. Documentar interfaz pública de Engine +4. Actualizar `CLAUDE.md` con nueva arquitectura +5. Verificar compilación y funcionamiento completo + +--- + +## Arquitectura Final Esperada + +```cpp +class Engine { +private: + // SDL Core + SDL_Window* window_; + SDL_Renderer* renderer_; + + // Componentes (composición) + std::unique_ptr input_handler_; + std::unique_ptr scene_manager_; + std::unique_ptr ui_manager_; + std::unique_ptr state_manager_; + std::unique_ptr shape_manager_; + std::unique_ptr theme_manager_; + + // Estado mínimo + bool should_exit_; + float delta_time_; + +public: + void run() { + while (!should_exit_) { + calculateDeltaTime(); + input_handler_->process(*this); + update(); + render(); + } + } +}; +``` + +## Notas +- Cada fase incluye su propio **commit atómico** +- Las fases son **secuenciales** (cada una depende de la anterior) +- Se preserva **100% de funcionalidad** en cada fase +- Compilación verificada después de cada commit + +--- + +*Documento de seguimiento para refactorización ViBe3 Physics* +*Última actualización: 2025-01-10 - Fase 1 completada* diff --git a/source/engine.cpp b/source/engine.cpp index 4c37678..47d2898 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -213,6 +213,9 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) { srand(static_cast(time(nullptr))); + // Inicializar InputHandler (sin estado) + input_handler_ = std::make_unique(); + // Inicializar ThemeManager PRIMERO (requerido por Notifier) theme_manager_ = std::make_unique(); theme_manager_->initialize(); @@ -230,8 +233,13 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) { void Engine::run() { while (!should_exit_) { calculateDeltaTime(); + + // Procesar eventos de entrada (teclado, ratón, ventana) + if (input_handler_->processEvents(*this)) { + should_exit_ = true; + } + update(); - handleEvents(); render(); } } @@ -311,426 +319,185 @@ void Engine::update() { theme_manager_->update(delta_time_); } -void Engine::handleEvents() { - SDL_Event event; - while (SDL_PollEvent(&event)) { - // Procesar eventos de ratón (auto-ocultar cursor) - Mouse::handleEvent(event); +// === IMPLEMENTACIÓN DE MÉTODOS PÚBLICOS PARA INPUT HANDLER === - // Salir del bucle si se detecta una petición de cierre - if (event.type == SDL_EVENT_QUIT) { - should_exit_ = true; - break; +// 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 { + switchBallsGravity(); // Toggle normal en modo física + // Determinar estado actual de gravedad (gravity_force_ != 0.0f significa ON) + bool gravity_on = balls_.empty() ? true : (balls_[0]->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 { + enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF + } + changeGravityDirection(direction); + showNotificationForAction(notification_text); +} + +// Display y depuración +void Engine::toggleDebug() { + show_debug_ = !show_debug_; +} + +// 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(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(shape_scale_factor_ * 100.0f + 0.5f)) + "%"); + } +} - // Procesar eventos de teclado - if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) { - switch (event.key.key) { - case SDLK_ESCAPE: - should_exit_ = true; - break; +void Engine::resetShapeScale() { + if (current_mode_ == SimulationMode::SHAPE) { + shape_scale_factor_ = SHAPE_SCALE_DEFAULT; + showNotificationForAction("Escala 100%"); + } +} - case SDLK_SPACE: - pushBallsAwayFromGravity(); - break; +void Engine::toggleDepthZoom() { + if (current_mode_ == SimulationMode::SHAPE) { + depth_zoom_enabled_ = !depth_zoom_enabled_; + showNotificationForAction(depth_zoom_enabled_ ? "Profundidad On" : "Profundidad Off"); + } +} - case SDLK_G: - // Si estamos en modo figura, salir a modo física SIN GRAVEDAD - if (current_mode_ == SimulationMode::SHAPE) { - toggleShapeMode(false); // Desactivar figura sin forzar gravedad ON - showNotificationForAction("Gravedad Off"); - } else { - switchBallsGravity(); // Toggle normal en modo física - // Determinar estado actual de gravedad (gravity_force_ != 0.0f significa ON) - bool gravity_on = balls_.empty() ? true : (balls_[0]->getGravityForce() != 0.0f); - showNotificationForAction(gravity_on ? "Gravedad On" : "Gravedad Off"); - } - break; +// Temas de colores +void Engine::cycleTheme(bool forward) { + if (forward) { + theme_manager_->cycleTheme(); + } else { + theme_manager_->cyclePrevTheme(); + } + showNotificationForAction(theme_manager_->getCurrentThemeNameES()); +} - // Controles de dirección de gravedad con teclas de cursor - case SDLK_UP: - // Si estamos en modo figura, salir a modo física CON gravedad - if (current_mode_ == SimulationMode::SHAPE) { - toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente) - } else { - enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF - } - changeGravityDirection(GravityDirection::UP); - showNotificationForAction("Gravedad Arriba"); - break; +void Engine::switchThemeByNumpad(int numpad_key) { + // Mapear tecla numpad a índice de tema según página actual + int theme_index = -1; - case SDLK_DOWN: - // Si estamos en modo figura, salir a modo física CON gravedad - if (current_mode_ == SimulationMode::SHAPE) { - toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente) - } else { - enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF - } - changeGravityDirection(GravityDirection::DOWN); - showNotificationForAction("Gravedad Abajo"); - break; - - case SDLK_LEFT: - // Si estamos en modo figura, salir a modo física CON gravedad - if (current_mode_ == SimulationMode::SHAPE) { - toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente) - } else { - enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF - } - changeGravityDirection(GravityDirection::LEFT); - showNotificationForAction("Gravedad Izquierda"); - break; - - case SDLK_RIGHT: - // Si estamos en modo figura, salir a modo física CON gravedad - if (current_mode_ == SimulationMode::SHAPE) { - toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente) - } else { - enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF - } - changeGravityDirection(GravityDirection::RIGHT); - showNotificationForAction("Gravedad Derecha"); - break; - - case SDLK_V: - toggleVSync(); - break; - - case SDLK_H: - show_debug_ = !show_debug_; - break; - - // Toggle Física ↔ Última Figura (antes era C) - case SDLK_F: - toggleShapeMode(); - // 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(current_shape_type_)]); - } - break; - - // Selección directa de figuras 3D - case SDLK_Q: - activateShape(ShapeType::SPHERE); - showNotificationForAction("Esfera"); - break; - - case SDLK_W: - activateShape(ShapeType::LISSAJOUS); - showNotificationForAction("Lissajous"); - break; - - case SDLK_E: - activateShape(ShapeType::HELIX); - showNotificationForAction("Hélice"); - break; - - case SDLK_R: - activateShape(ShapeType::TORUS); - showNotificationForAction("Toroide"); - break; - - case SDLK_T: - activateShape(ShapeType::CUBE); - showNotificationForAction("Cubo"); - break; - - case SDLK_Y: - activateShape(ShapeType::CYLINDER); - showNotificationForAction("Cilindro"); - break; - - case SDLK_U: - activateShape(ShapeType::ICOSAHEDRON); - showNotificationForAction("Icosaedro"); - break; - - case SDLK_I: - activateShape(ShapeType::ATOM); - showNotificationForAction("Átomo"); - break; - - case SDLK_O: - activateShape(ShapeType::PNG_SHAPE); - showNotificationForAction("Forma PNG"); - break; - - // Ciclar temas de color (movido de T a B) - case SDLK_B: - { - // Detectar si Shift está presionado - SDL_Keymod modstate = SDL_GetModState(); - if (modstate & SDL_KMOD_SHIFT) { - // Shift+B: Ciclar hacia atrás (tema anterior) - theme_manager_->cyclePrevTheme(); - } else { - // B solo: Ciclar hacia adelante (tema siguiente) - theme_manager_->cycleTheme(); - } - - // Mostrar notificación con el nombre del tema - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - // Temas de colores con teclado numérico (con transición suave) - case SDLK_KP_1: - // Página 0: SUNSET (0), Página 1: OCEAN_WAVES (10) - { - int theme_index = (theme_page_ == 0) ? 0 : 10; - theme_manager_->switchToTheme(theme_index); - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_2: - // Página 0: OCEAN (1), Página 1: NEON_PULSE (11) - { - int theme_index = (theme_page_ == 0) ? 1 : 11; - theme_manager_->switchToTheme(theme_index); - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_3: - // Página 0: NEON (2), Página 1: FIRE (12) - { - int theme_index = (theme_page_ == 0) ? 2 : 12; - theme_manager_->switchToTheme(theme_index); - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_4: - // Página 0: FOREST (3), Página 1: AURORA (13) - { - int theme_index = (theme_page_ == 0) ? 3 : 13; - theme_manager_->switchToTheme(theme_index); - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_5: - // Página 0: RGB (4), Página 1: VOLCANIC (14) - { - int theme_index = (theme_page_ == 0) ? 4 : 14; - theme_manager_->switchToTheme(theme_index); - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_6: - // Solo página 0: MONOCHROME (5) - if (theme_page_ == 0) { - theme_manager_->switchToTheme(5); // MONOCHROME - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_7: - // Solo página 0: LAVENDER (6) - if (theme_page_ == 0) { - theme_manager_->switchToTheme(6); // LAVENDER - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_8: - // Solo página 0: CRIMSON (7) - if (theme_page_ == 0) { - theme_manager_->switchToTheme(7); // CRIMSON - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_9: - // Solo página 0: EMERALD (8) - if (theme_page_ == 0) { - theme_manager_->switchToTheme(8); // EMERALD - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - case SDLK_KP_0: - // Solo página 0: SUNRISE (9) - if (theme_page_ == 0) { - theme_manager_->switchToTheme(9); // SUNRISE - showNotificationForAction(theme_manager_->getCurrentThemeNameES()); - } - break; - - // Toggle de página de temas (Numpad Enter) - case SDLK_KP_ENTER: - // Alternar entre página 0 y página 1 - theme_page_ = (theme_page_ == 0) ? 1 : 0; - showNotificationForAction((theme_page_ == 0) ? "Página 1" : "Página 2"); - break; - - // Cambio de sprite/textura dinámico - case SDLK_N: - switchTexture(); - break; - - // Control de escala de figura (solo en modo SHAPE) - case SDLK_KP_PLUS: - if (current_mode_ == SimulationMode::SHAPE) { - shape_scale_factor_ += SHAPE_SCALE_STEP; - clampShapeScale(); - showNotificationForAction("Escala " + std::to_string(static_cast(shape_scale_factor_ * 100.0f + 0.5f)) + "%"); - } - break; - - case SDLK_KP_MINUS: - if (current_mode_ == SimulationMode::SHAPE) { - shape_scale_factor_ -= SHAPE_SCALE_STEP; - clampShapeScale(); - showNotificationForAction("Escala " + std::to_string(static_cast(shape_scale_factor_ * 100.0f + 0.5f)) + "%"); - } - break; - - case SDLK_KP_MULTIPLY: - if (current_mode_ == SimulationMode::SHAPE) { - shape_scale_factor_ = SHAPE_SCALE_DEFAULT; - showNotificationForAction("Escala 100%"); - } - break; - - case SDLK_KP_DIVIDE: - if (current_mode_ == SimulationMode::SHAPE) { - depth_zoom_enabled_ = !depth_zoom_enabled_; - showNotificationForAction(depth_zoom_enabled_ ? "Profundidad On" : "Profundidad Off"); - } - break; - - case SDLK_1: - scenario_ = 0; - initBalls(scenario_); - showNotificationForAction("10 Pelotas"); - break; - - case SDLK_2: - scenario_ = 1; - initBalls(scenario_); - showNotificationForAction("50 Pelotas"); - break; - - case SDLK_3: - scenario_ = 2; - initBalls(scenario_); - showNotificationForAction("100 Pelotas"); - break; - - case SDLK_4: - scenario_ = 3; - initBalls(scenario_); - showNotificationForAction("500 Pelotas"); - break; - - case SDLK_5: - scenario_ = 4; - initBalls(scenario_); - showNotificationForAction("1,000 Pelotas"); - break; - - case SDLK_6: - scenario_ = 5; - initBalls(scenario_); - showNotificationForAction("5,000 Pelotas"); - break; - - case SDLK_7: - scenario_ = 6; - initBalls(scenario_); - showNotificationForAction("10,000 Pelotas"); - break; - - case SDLK_8: - scenario_ = 7; - initBalls(scenario_); - showNotificationForAction("50,000 Pelotas"); - break; - - // Controles de zoom dinámico (solo si no estamos en fullscreen) - case SDLK_F1: - if (!fullscreen_enabled_ && !real_fullscreen_enabled_) { - zoomOut(); - } - break; - - case SDLK_F2: - if (!fullscreen_enabled_ && !real_fullscreen_enabled_) { - zoomIn(); - } - break; - - // Control de pantalla completa - case SDLK_F3: - toggleFullscreen(); - break; - - // Modo real fullscreen (cambia resolución interna) - case SDLK_F4: - toggleRealFullscreen(); - break; - - // Toggle escalado entero/estirado (solo en fullscreen F3) - case SDLK_F5: - toggleIntegerScaling(); - break; - - // Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D) - case SDLK_D: - // Shift+D = Pausar tema dinámico - if (event.key.mod & SDL_KMOD_SHIFT) { - theme_manager_->pauseDynamic(); - } else { - // D sin Shift = Toggle DEMO ↔ SANDBOX - if (current_app_mode_ == 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"); - } - } - break; - - // Toggle Modo DEMO LITE (solo física/figuras) - case SDLK_L: - if (current_app_mode_ == 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"); - } - break; - - // Toggle Modo LOGO (easter egg - marca de agua) - case SDLK_K: - if (current_app_mode_ == 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"); - } - break; - } + 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) { + scenario_ = scenario_id; + initBalls(scenario_); + 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 (current_app_mode_ == 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 (current_app_mode_ == 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 (current_app_mode_ == 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"); } } @@ -1556,9 +1323,9 @@ void Engine::updateDemoMode() { // Ya estábamos esperando flips, y se disparó el trigger // → Hacer el cambio SHAPE → PHYSICS ahora (durante el flip) if (action < 50) { - toggleShapeMode(true); // Con gravedad ON + toggleShapeModeInternal(true); // Con gravedad ON } else { - toggleShapeMode(false); // Con gravedad OFF + toggleShapeModeInternal(false); // Con gravedad OFF } // Resetear variables de espera de flips @@ -1590,10 +1357,10 @@ void Engine::updateDemoMode() { // CAMINO A (50%): Cambio inmediato if (action < 50) { // 50%: SHAPE → PHYSICS con gravedad ON (caída dramática) - toggleShapeMode(true); + toggleShapeModeInternal(true); } else { // 50%: SHAPE → PHYSICS con gravedad OFF (dar vueltas sin caer) - toggleShapeMode(false); + toggleShapeModeInternal(false); } // Resetear variables de espera de flips al cambiar a PHYSICS @@ -1609,7 +1376,7 @@ void Engine::updateDemoMode() { // Logo animado (PHYSICS) → 3 opciones posibles if (action < 60) { // 60%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones) - toggleShapeMode(false); + toggleShapeModeInternal(false); // Resetear variables de espera de flips al volver a SHAPE logo_waiting_for_flip_ = false; @@ -1711,14 +1478,14 @@ void Engine::performDemoAction(bool is_lite) { 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; - activateShape(shapes[shape_index]); + activateShapeInternal(shapes[shape_index]); return; } // Toggle física ↔ figura (20%) accumulated_weight += DEMO_LITE_WEIGHT_TOGGLE_PHYSICS; if (random_value < accumulated_weight) { - toggleShapeMode(false); // NO forzar gravedad al salir + toggleShapeModeInternal(false); // NO forzar gravedad al salir return; } @@ -1754,14 +1521,14 @@ void Engine::performDemoAction(bool is_lite) { 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; - activateShape(shapes[shape_index]); + activateShapeInternal(shapes[shape_index]); return; } // Toggle física ↔ figura (12%) accumulated_weight += DEMO_WEIGHT_TOGGLE_PHYSICS; if (random_value < accumulated_weight) { - toggleShapeMode(false); // NO forzar gravedad al salir + toggleShapeModeInternal(false); // NO forzar gravedad al salir return; } @@ -1830,7 +1597,7 @@ void Engine::performDemoAction(bool is_lite) { // Cambiar sprite (2%) accumulated_weight += DEMO_WEIGHT_SPRITE; if (random_value < accumulated_weight) { - switchTexture(false); // Suprimir notificación en modo automático + switchTextureInternal(false); // Suprimir notificación en modo automático return; } } @@ -1844,7 +1611,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) { ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; - activateShape(shapes[rand() % 8]); + activateShapeInternal(shapes[rand() % 8]); } if (is_lite) { @@ -1853,12 +1620,12 @@ void Engine::randomizeOnDemoStart(bool is_lite) { if (rand() % 2 == 0) { // Modo física if (current_mode_ == SimulationMode::SHAPE) { - toggleShapeMode(false); // Salir a física sin forzar gravedad + 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}; - activateShape(shapes[rand() % 8]); + activateShapeInternal(shapes[rand() % 8]); } // Randomizar gravedad: dirección + ON/OFF @@ -1882,19 +1649,19 @@ void Engine::randomizeOnDemoStart(bool is_lite) { // 3. Sprite if (rand() % 2 == 0) { - switchTexture(false); // Suprimir notificación al activar modo DEMO + 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) { - toggleShapeMode(false); + 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}; - activateShape(shapes[rand() % 8]); + activateShapeInternal(shapes[rand() % 8]); // 5. Profundidad (solo si estamos en figura) if (rand() % 2 == 0) { @@ -1981,7 +1748,7 @@ void Engine::enterLogoMode(bool from_demo) { clampShapeScale(); // Activar PNG_SHAPE (el logo) - activateShape(ShapeType::PNG_SHAPE); + activateShapeInternal(ShapeType::PNG_SHAPE); // Configurar PNG_SHAPE en modo LOGO (flip intervals más largos) if (active_shape_) { @@ -2053,20 +1820,12 @@ void Engine::exitLogoMode(bool return_to_demo) { ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; - activateShape(shapes[rand() % 8]); + activateShapeInternal(shapes[rand() % 8]); } } } // Toggle manual del Modo Logo (tecla K) -void Engine::toggleLogoMode() { - if (current_app_mode_ == AppMode::LOGO) { - exitLogoMode(false); // Salir y volver a MANUAL - } else { - enterLogoMode(false); // Entrar manualmente - } -} - // Sistema de cambio de sprites dinámico void Engine::updateBallSizes(int old_size, int new_size) { float delta_size = static_cast(new_size - old_size); @@ -2111,7 +1870,7 @@ void Engine::updateBallSizes(int old_size, int new_size) { } } -void Engine::switchTexture(bool show_notification) { +void Engine::switchTextureInternal(bool show_notification) { if (textures_.empty()) return; // Guardar tamaño antiguo @@ -2142,10 +1901,10 @@ void Engine::switchTexture(bool show_notification) { } // Sistema de Figuras 3D - Alternar entre modo física y última figura (Toggle con tecla F) -void Engine::toggleShapeMode(bool force_gravity_on_exit) { +void Engine::toggleShapeModeInternal(bool force_gravity_on_exit) { if (current_mode_ == SimulationMode::PHYSICS) { // Cambiar a modo figura (usar última figura seleccionada) - activateShape(last_shape_type_); + activateShapeInternal(last_shape_type_); // Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO if (current_app_mode_ == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) { @@ -2189,7 +1948,7 @@ void Engine::toggleShapeMode(bool force_gravity_on_exit) { } // Activar figura específica (llamado por teclas Q/W/E/R/Y/U/I o por toggleShapeMode) -void Engine::activateShape(ShapeType type) { +void Engine::activateShapeInternal(ShapeType type) { // Guardar como última figura seleccionada last_shape_type_ = type; current_shape_type_ = type; diff --git a/source/engine.h b/source/engine.h index d399a54..be57470 100644 --- a/source/engine.h +++ b/source/engine.h @@ -13,6 +13,7 @@ #include "ball.h" // for Ball #include "defines.h" // for GravityDirection, ColorTheme, ShapeType #include "external/texture.h" // for Texture +#include "input/input_handler.h" // for InputHandler #include "shapes/shape.h" // for Shape (interfaz polimórfica) #include "text/textrenderer.h" // for TextRenderer #include "theme_manager.h" // for ThemeManager @@ -28,12 +29,57 @@ enum class AppMode { class Engine { public: - // Interfaz pública + // Interfaz pública principal bool initialize(int width = 0, int height = 0, int zoom = 0, bool fullscreen = false); void run(); void shutdown(); + // === Métodos públicos para InputHandler === + + // Gravedad y física + void pushBallsAwayFromGravity(); + void handleGravityToggle(); + void handleGravityDirectionChange(GravityDirection direction, const char* notification_text); + + // Display y depuración + void toggleVSync(); + void toggleDebug(); + + // Figuras 3D + void toggleShapeMode(); + void activateShape(ShapeType type, const char* notification_text); + void handleShapeScaleChange(bool increase); + void resetShapeScale(); + void toggleDepthZoom(); + + // Temas de colores + void cycleTheme(bool forward); + void switchThemeByNumpad(int numpad_key); + void toggleThemePage(); + void pauseDynamicTheme(); + + // Sprites/Texturas + void switchTexture(); + + // Escenarios (número de pelotas) + void changeScenario(int scenario_id, const char* notification_text); + + // Zoom y fullscreen + void handleZoomIn(); + void handleZoomOut(); + void toggleFullscreen(); + void toggleRealFullscreen(); + void toggleIntegerScaling(); + + // Modos de aplicación (DEMO/LOGO) + void toggleDemoMode(); + void toggleDemoLiteMode(); + void toggleLogoMode(); + private: + // === Componentes del sistema (Composición) === + std::unique_ptr input_handler_; // Manejo de entradas SDL + // Recursos SDL SDL_Window* window_ = nullptr; SDL_Renderer* renderer_ = nullptr; @@ -140,23 +186,17 @@ class Engine { // Métodos principales del loop void calculateDeltaTime(); void update(); - void handleEvents(); void render(); - // Métodos auxiliares + // Métodos auxiliares privados (llamados por la interfaz pública) void initBalls(int value); void setText(); // DEPRECATED - usar showNotificationForAction() en su lugar void showNotificationForAction(const std::string& text); // Mostrar notificación solo en modo MANUAL - void pushBallsAwayFromGravity(); void switchBallsGravity(); void enableBallsGravityIfDisabled(); void forceBallsGravityOn(); void forceBallsGravityOff(); void changeGravityDirection(GravityDirection direction); - void toggleVSync(); - void toggleFullscreen(); - void toggleRealFullscreen(); - void toggleIntegerScaling(); std::string gravityDirectionToString(GravityDirection direction) const; // Sistema de gestión de estados (MANUAL/DEMO/DEMO_LITE/LOGO) @@ -168,16 +208,15 @@ class Engine { void randomizeOnDemoStart(bool is_lite); void toggleGravityOnOff(); - // Sistema de Modo Logo (easter egg) - void toggleLogoMode(); // Activar/desactivar modo logo manual (tecla K) + // Sistema de Modo Logo (easter egg) - Métodos privados void enterLogoMode(bool from_demo = false); // Entrar al modo logo (manual o automático) void exitLogoMode(bool return_to_demo = false); // Salir del modo logo - // Sistema de cambio de sprites dinámico - void switchTexture(bool show_notification = true); // Cambia a siguiente textura disponible - void updateBallSizes(int old_size, int new_size); // Ajusta posiciones al cambiar tamaño + // Sistema de cambio de sprites dinámico - Métodos privados + void switchTextureInternal(bool show_notification); // Implementación interna del cambio de textura + void updateBallSizes(int old_size, int new_size); // Ajusta posiciones al cambiar tamaño - // Sistema de zoom dinámico + // Sistema de zoom dinámico - Métodos privados int calculateMaxWindowZoom() const; void setWindowZoom(int new_zoom); void zoomIn(); @@ -187,10 +226,10 @@ 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 - void toggleShapeMode(bool force_gravity_on_exit = true); // Toggle PHYSICS ↔ última figura (tecla F) - void activateShape(ShapeType type); // Activar figura específica (teclas Q/W/E/R/Y/U/I) - 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 + 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 }; diff --git a/source/input/input_handler.cpp b/source/input/input_handler.cpp new file mode 100644 index 0000000..6533584 --- /dev/null +++ b/source/input/input_handler.cpp @@ -0,0 +1,266 @@ +#include "input_handler.h" + +#include // for SDL_Keycode +#include // for std::string, std::to_string + +#include "../engine.h" // for Engine +#include "../external/mouse.h" // for Mouse namespace + +bool InputHandler::processEvents(Engine& engine) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + // Procesar eventos de ratón (auto-ocultar cursor) + Mouse::handleEvent(event); + + // Salir del bucle si se detecta una petición de cierre + if (event.type == SDL_EVENT_QUIT) { + return true; // Solicitar salida + } + + // Procesar eventos de teclado + if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) { + switch (event.key.key) { + case SDLK_ESCAPE: + return true; // Solicitar salida + + case SDLK_SPACE: + engine.pushBallsAwayFromGravity(); + break; + + case SDLK_G: + engine.handleGravityToggle(); + break; + + // Controles de dirección de gravedad con teclas de cursor + case SDLK_UP: + engine.handleGravityDirectionChange(GravityDirection::UP, "Gravedad Arriba"); + break; + + case SDLK_DOWN: + engine.handleGravityDirectionChange(GravityDirection::DOWN, "Gravedad Abajo"); + break; + + case SDLK_LEFT: + engine.handleGravityDirectionChange(GravityDirection::LEFT, "Gravedad Izquierda"); + break; + + case SDLK_RIGHT: + engine.handleGravityDirectionChange(GravityDirection::RIGHT, "Gravedad Derecha"); + break; + + case SDLK_V: + engine.toggleVSync(); + break; + + case SDLK_H: + engine.toggleDebug(); + break; + + // Toggle Física ↔ Última Figura (antes era C) + case SDLK_F: + engine.toggleShapeMode(); + break; + + // Selección directa de figuras 3D + case SDLK_Q: + engine.activateShape(ShapeType::SPHERE, "Esfera"); + break; + + case SDLK_W: + engine.activateShape(ShapeType::LISSAJOUS, "Lissajous"); + break; + + case SDLK_E: + engine.activateShape(ShapeType::HELIX, "Hélice"); + break; + + case SDLK_R: + engine.activateShape(ShapeType::TORUS, "Toroide"); + break; + + case SDLK_T: + engine.activateShape(ShapeType::CUBE, "Cubo"); + break; + + case SDLK_Y: + engine.activateShape(ShapeType::CYLINDER, "Cilindro"); + break; + + case SDLK_U: + engine.activateShape(ShapeType::ICOSAHEDRON, "Icosaedro"); + break; + + case SDLK_I: + engine.activateShape(ShapeType::ATOM, "Átomo"); + break; + + case SDLK_O: + engine.activateShape(ShapeType::PNG_SHAPE, "Forma PNG"); + break; + + // Ciclar temas de color (movido de T a B) + case SDLK_B: + { + // Detectar si Shift está presionado + SDL_Keymod modstate = SDL_GetModState(); + if (modstate & SDL_KMOD_SHIFT) { + // Shift+B: Ciclar hacia atrás (tema anterior) + engine.cycleTheme(false); + } else { + // B solo: Ciclar hacia adelante (tema siguiente) + engine.cycleTheme(true); + } + } + break; + + // Temas de colores con teclado numérico (con transición suave) + case SDLK_KP_1: + engine.switchThemeByNumpad(1); + break; + + case SDLK_KP_2: + engine.switchThemeByNumpad(2); + break; + + case SDLK_KP_3: + engine.switchThemeByNumpad(3); + break; + + case SDLK_KP_4: + engine.switchThemeByNumpad(4); + break; + + case SDLK_KP_5: + engine.switchThemeByNumpad(5); + break; + + case SDLK_KP_6: + engine.switchThemeByNumpad(6); + break; + + case SDLK_KP_7: + engine.switchThemeByNumpad(7); + break; + + case SDLK_KP_8: + engine.switchThemeByNumpad(8); + break; + + case SDLK_KP_9: + engine.switchThemeByNumpad(9); + break; + + case SDLK_KP_0: + engine.switchThemeByNumpad(0); + break; + + // Toggle de página de temas (Numpad Enter) + case SDLK_KP_ENTER: + engine.toggleThemePage(); + break; + + // Cambio de sprite/textura dinámico + case SDLK_N: + engine.switchTexture(); + break; + + // Control de escala de figura (solo en modo SHAPE) + case SDLK_KP_PLUS: + engine.handleShapeScaleChange(true); // Aumentar + break; + + case SDLK_KP_MINUS: + engine.handleShapeScaleChange(false); // Disminuir + break; + + case SDLK_KP_MULTIPLY: + engine.resetShapeScale(); + break; + + case SDLK_KP_DIVIDE: + engine.toggleDepthZoom(); + break; + + // Cambio de número de pelotas (escenarios 1-8) + case SDLK_1: + engine.changeScenario(0, "10 Pelotas"); + break; + + case SDLK_2: + engine.changeScenario(1, "50 Pelotas"); + break; + + case SDLK_3: + engine.changeScenario(2, "100 Pelotas"); + break; + + case SDLK_4: + engine.changeScenario(3, "500 Pelotas"); + break; + + case SDLK_5: + engine.changeScenario(4, "1,000 Pelotas"); + break; + + case SDLK_6: + engine.changeScenario(5, "5,000 Pelotas"); + break; + + case SDLK_7: + engine.changeScenario(6, "10,000 Pelotas"); + break; + + case SDLK_8: + engine.changeScenario(7, "50,000 Pelotas"); + break; + + // Controles de zoom dinámico (solo si no estamos en fullscreen) + case SDLK_F1: + engine.handleZoomOut(); + break; + + case SDLK_F2: + engine.handleZoomIn(); + break; + + // Control de pantalla completa + case SDLK_F3: + engine.toggleFullscreen(); + break; + + // Modo real fullscreen (cambia resolución interna) + case SDLK_F4: + engine.toggleRealFullscreen(); + break; + + // Toggle escalado entero/estirado (solo en fullscreen F3) + case SDLK_F5: + engine.toggleIntegerScaling(); + break; + + // Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D) + case SDLK_D: + // Shift+D = Pausar tema dinámico + if (event.key.mod & SDL_KMOD_SHIFT) { + engine.pauseDynamicTheme(); + } else { + // D sin Shift = Toggle DEMO ↔ SANDBOX + engine.toggleDemoMode(); + } + break; + + // Toggle Modo DEMO LITE (solo física/figuras) + case SDLK_L: + engine.toggleDemoLiteMode(); + break; + + // Toggle Modo LOGO (easter egg - marca de agua) + case SDLK_K: + engine.toggleLogoMode(); + break; + } + } + } + + return false; // No se solicitó salida +} diff --git a/source/input/input_handler.h b/source/input/input_handler.h new file mode 100644 index 0000000..58623a8 --- /dev/null +++ b/source/input/input_handler.h @@ -0,0 +1,32 @@ +#pragma once + +#include // for SDL_Event + +// Forward declaration para evitar dependencia circular +class Engine; + +/** + * @class InputHandler + * @brief Procesa eventos de entrada (teclado, ratón, ventana) y los traduce a acciones del Engine + * + * Responsabilidad única: Manejo de input SDL y traducción a comandos de alto nivel + * + * Características: + * - Procesa todos los eventos SDL (teclado, ratón, quit) + * - Traduce inputs a llamadas de métodos del Engine + * - Mantiene el Engine desacoplado de la lógica de input SDL + * - Soporta todos los controles del proyecto (gravedad, figuras, temas, zoom, fullscreen) + */ +class InputHandler { + public: + /** + * @brief Procesa todos los eventos SDL pendientes + * @param engine Referencia al engine para ejecutar acciones + * @return true si se debe salir de la aplicación (ESC o cerrar ventana), false en caso contrario + */ + bool processEvents(Engine& engine); + + private: + // Sin estado interno por ahora - el InputHandler es stateless + // Todos los estados se delegan al Engine +};