#include "engine.hpp" #include // for SDL_GetError #include // for SDL_Event, SDL_PollEvent #include // for SDL_Init, SDL_Quit, SDL_INIT_VIDEO #include // for SDL_Keycode #include // for SDL_SetRenderDrawColor, SDL_RenderPresent #include // for SDL_GetTicks #include // for SDL_CreateWindow, SDL_DestroyWindow, SDL_GetDisplayBounds #include // for std::min, std::max, std::sort #include // for sqrtf, acosf, cosf, sinf (funciones matemáticas) #include // for rand, srand #include // for strlen #include // for time #include // for path operations #include // for cout #include // for string #include "resource_manager.hpp" // for ResourceManager #ifdef _WIN32 #include // for GetModuleFileName #endif #include "ball.hpp" // for Ball #include "external/mouse.hpp" // for Mouse namespace #include "external/texture.hpp" // for Texture #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) { bool success = true; // Obtener resolución de pantalla para validación if (!SDL_Init(SDL_INIT_VIDEO)) { std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl; return false; } int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); const auto* dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr; int screen_w = dm ? dm->w : 1920; // Fallback si falla int screen_h = dm ? dm->h - WINDOW_DECORATION_HEIGHT : 1080; if (displays) SDL_free(displays); // Usar parámetros o valores por defecto int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH; int logical_height = (height > 0) ? height : DEFAULT_SCREEN_HEIGHT; int window_zoom = (zoom > 0) ? zoom : DEFAULT_WINDOW_ZOOM; // VALIDACIÓN 1: Si resolución > pantalla → reset a default if (logical_width > screen_w || logical_height > screen_h) { std::cout << "Advertencia: Resolución " << logical_width << "x" << logical_height << " excede pantalla " << screen_w << "x" << screen_h << ". Usando default " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << "\n"; logical_width = DEFAULT_SCREEN_WIDTH; logical_height = DEFAULT_SCREEN_HEIGHT; window_zoom = DEFAULT_WINDOW_ZOOM; // Reset zoom también } // VALIDACIÓN 2: Calcular max_zoom y ajustar si es necesario int max_zoom = std::min(screen_w / logical_width, screen_h / logical_height); if (max_zoom < 1) { // Resolució lògica no cap en pantalla ni a zoom=1: escalar-la per fer-la càpida float scale = std::min(static_cast(screen_w) / logical_width, static_cast(screen_h) / logical_height); logical_width = std::max(320, static_cast(logical_width * scale)); logical_height = std::max(240, static_cast(logical_height * scale)); window_zoom = 1; std::cout << "Advertencia: Resolución no cabe en pantalla. Ajustando a " << logical_width << "x" << logical_height << "\n"; } else if (window_zoom > max_zoom) { std::cout << "Advertencia: Zoom " << window_zoom << " excede máximo " << max_zoom << " para " << logical_width << "x" << logical_height << ". Ajustando a " << max_zoom << "\n"; window_zoom = max_zoom; } // Si se especificaron parámetros CLI y zoom no se especificó, usar zoom=1 if ((width > 0 || height > 0) && zoom == 0) { window_zoom = 1; } // Guardar zoom calculado ANTES de crear la ventana (para F1/F2/F3/F4) current_window_zoom_ = window_zoom; // Calcular tamaño de ventana int window_width = logical_width * window_zoom; int window_height = logical_height * window_zoom; // Guardar resolución base (configurada por CLI o default) base_screen_width_ = logical_width; base_screen_height_ = logical_height; current_screen_width_ = logical_width; current_screen_height_ = logical_height; // SDL ya inicializado arriba para validación { // Crear ventana principal (fullscreen si se especifica) // NOTA: SDL_WINDOW_HIGH_PIXEL_DENSITY removido por incompatibilidad con STRETCH mode (F4) // El DPI se detectará manualmente con SDL_GetWindowSizeInPixels() Uint32 window_flags = SDL_WINDOW_OPENGL; if (fullscreen) { window_flags |= SDL_WINDOW_FULLSCREEN; } window_ = SDL_CreateWindow(WINDOW_CAPTION, window_width, window_height, window_flags); if (window_ == nullptr) { std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << std::endl; success = false; } else { // Centrar ventana en pantalla si no está en fullscreen if (!fullscreen) { SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); } // Crear renderizador renderer_ = SDL_CreateRenderer(window_, nullptr); if (renderer_ == nullptr) { std::cout << "¡No se pudo crear el renderizador! Error de SDL: " << SDL_GetError() << std::endl; success = false; } else { // Establecer color inicial del renderizador SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF); // Establecer tamaño lógico para el renderizado (resolución interna) SDL_SetRenderLogicalPresentation(renderer_, logical_width, logical_height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); // Configurar V-Sync inicial SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0); } } } // Inicializar otros componentes si SDL se inicializó correctamente if (success) { // Cargar todas las texturas disponibles desde data/balls/ std::string resources_dir = getResourcesDirectory(); std::string balls_dir = resources_dir + "/data/balls"; struct TextureInfo { std::string name; std::shared_ptr texture; int width; }; std::vector texture_files; // Buscar todas las texturas PNG en data/balls/ namespace fs = std::filesystem; if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) { // Cargar todas las texturas desde disco for (const auto& entry : fs::directory_iterator(balls_dir)) { if (entry.is_regular_file() && entry.path().extension() == ".png") { std::string filename = entry.path().stem().string(); std::string fullpath = entry.path().string(); // Cargar textura y obtener dimensiones auto texture = std::make_shared(renderer_, fullpath); int width = texture->getWidth(); texture_files.push_back({filename, texture, width}); } } } else { // Fallback: cargar texturas desde pack usando la lista del ResourceManager if (ResourceManager::isPackLoaded()) { auto pack_resources = ResourceManager::getResourceList(); // Filtrar solo los recursos en balls/ con extensión .png for (const auto& resource : pack_resources) { if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") { std::string tex_name = resource.substr(6); // Quitar "balls/" std::string name = tex_name.substr(0, tex_name.find('.')); // Quitar extensión auto texture = std::make_shared(renderer_, resource); int width = texture->getWidth(); texture_files.push_back({name, texture, width}); } } } } // Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4) std::sort(texture_files.begin(), texture_files.end(), [](const TextureInfo& a, const TextureInfo& b) { return a.width > b.width; // Descendente por tamaño }); // Guardar texturas ya cargadas en orden (0=big, 1=normal, 2=small, 3=tiny) for (const auto& info : texture_files) { textures_.push_back(info.texture); texture_names_.push_back(info.name); } // Verificar que se cargaron texturas if (textures_.empty()) { std::cerr << "ERROR: No se pudieron cargar texturas" << std::endl; success = false; } // Buscar índice de "normal" para usarlo como textura inicial (debería ser índice 1) current_texture_index_ = 0; // Fallback for (size_t i = 0; i < texture_names_.size(); i++) { if (texture_names_[i] == "normal") { current_texture_index_ = i; // Iniciar en "normal" (índice 1) break; } } texture_ = textures_[current_texture_index_]; current_ball_size_ = texture_->getWidth(); // Obtener tamaño dinámicamente srand(static_cast(time(nullptr))); // Inicializar InputHandler (sin estado) input_handler_ = std::make_unique(); // Inicializar ThemeManager PRIMERO (requerido por Notifier y SceneManager) theme_manager_ = std::make_unique(); theme_manager_->initialize(); // Inicializar SceneManager (gestión de bolas y física) scene_manager_ = std::make_unique(current_screen_width_, current_screen_height_); scene_manager_->initialize(0, texture_, theme_manager_.get()); // Escenario 0 (10 bolas) por defecto // Propagar configuración custom si fue establecida antes de initialize() if (custom_scenario_enabled_) scene_manager_->setCustomBallCount(custom_scenario_balls_); // Calcular tamaño físico de ventana ANTES de inicializar UIManager // NOTA: No llamar a updatePhysicalWindowSize() aquí porque ui_manager_ aún no existe // Calcular manualmente para poder pasar valores al constructor de UIManager int window_w = 0, window_h = 0; SDL_GetWindowSizeInPixels(window_, &window_w, &window_h); physical_window_width_ = window_w; physical_window_height_ = window_h; // Inicializar UIManager (HUD, FPS, notificaciones) // NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager ui_manager_ = std::make_unique(); ui_manager_->initialize(renderer_, theme_manager_.get(), physical_window_width_, physical_window_height_, current_screen_width_, current_screen_height_); // Inicializar ShapeManager (gestión de figuras 3D) shape_manager_ = std::make_unique(); shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), nullptr, current_screen_width_, current_screen_height_); // Inicializar StateManager (gestión de estados DEMO/LOGO) state_manager_ = std::make_unique(); 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 if (initial_mode == AppMode::DEMO) { state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_); // Como estamos en SANDBOX (default), toggleDemoMode() cambiará a DEMO + randomizará } else if (initial_mode == AppMode::DEMO_LITE) { state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_); // Como estamos en SANDBOX (default), toggleDemoLiteMode() cambiará a DEMO_LITE + randomizará } 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) + configuración visual completa } // Actualizar ShapeManager con StateManager (dependencia circular - StateManager debe existir primero) shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(), current_screen_width_, current_screen_height_); // Inicializar BoidManager (gestión de comportamiento de enjambre) boid_manager_ = std::make_unique(); boid_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(), current_screen_width_, current_screen_height_); // Inicializar AppLogo (logo periódico en pantalla) app_logo_ = std::make_unique(); if (!app_logo_->initialize(renderer_, current_screen_width_, current_screen_height_)) { std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << std::endl; // No es crítico, continuar sin logo app_logo_.reset(); } // Benchmark de rendimiento (determina max_auto_scenario_ para modos automáticos) if (!skip_benchmark_) runPerformanceBenchmark(); else if (custom_scenario_enabled_) custom_auto_available_ = true; // benchmark omitido: confiar en que el hardware lo soporta // Precalentar caché: shapes PNG (evitar I/O en primera activación de PNG_SHAPE) { unsigned char* tmp = nullptr; size_t tmp_size = 0; ResourceManager::loadResource("shapes/jailgames.png", tmp, tmp_size); delete[] tmp; } } return success; } void Engine::run() { while (!should_exit_) { calculateDeltaTime(); // Procesar eventos de entrada (teclado, ratón, ventana) if (input_handler_->processEvents(*this)) { should_exit_ = true; } update(); render(); } } void Engine::shutdown() { // Limpiar recursos SDL if (renderer_) { SDL_DestroyRenderer(renderer_); renderer_ = nullptr; } if (window_) { SDL_DestroyWindow(window_); window_ = nullptr; } SDL_Quit(); } // Métodos privados - esqueleto básico por ahora void Engine::calculateDeltaTime() { Uint64 current_time = SDL_GetTicks(); // En el primer frame, inicializar el tiempo anterior if (last_frame_time_ == 0) { last_frame_time_ = current_time; delta_time_ = 1.0f / 60.0f; // Asumir 60 FPS para el primer frame return; } // Calcular delta time en segundos delta_time_ = (current_time - last_frame_time_) / 1000.0f; last_frame_time_ = current_time; // Limitar delta time para evitar saltos grandes (pausa larga, depuración, etc.) if (delta_time_ > 0.05f) { // Máximo 50ms (20 FPS mínimo) delta_time_ = 1.0f / 60.0f; // Fallback a 60 FPS } } void Engine::update() { // Actualizar visibilidad del cursor (auto-ocultar tras inactividad) Mouse::updateCursorVisibility(); // Obtener tiempo actual Uint64 current_time = SDL_GetTicks(); // Actualizar UI (FPS, notificaciones, texto obsoleto) - delegado a UIManager ui_manager_->update(current_time, delta_time_); // Bifurcar actualización según modo activo if (current_mode_ == SimulationMode::PHYSICS) { // Modo física normal: actualizar física de cada pelota (delegado a SceneManager) scene_manager_->update(delta_time_); } else if (current_mode_ == SimulationMode::SHAPE) { // Modo Figura 3D: actualizar figura polimórfica updateShape(); } else if (current_mode_ == SimulationMode::BOIDS) { // Modo Boids: actualizar comportamiento de enjambre (delegado a BoidManager) boid_manager_->update(delta_time_); } // Actualizar Modo DEMO/LOGO (delegado a StateManager) state_manager_->update(delta_time_, shape_manager_->getConvergence(), shape_manager_->getActiveShape()); // Actualizar transiciones de temas (delegado a ThemeManager) theme_manager_->update(delta_time_); // Actualizar AppLogo (logo periódico) if (app_logo_) { app_logo_->update(delta_time_, state_manager_->getCurrentMode()); } } // === IMPLEMENTACIÓN DE MÉTODOS PÚBLICOS PARA INPUT HANDLER === // Gravedad y física void Engine::handleGravityToggle() { // Si estamos en modo boids, salir a modo física CON GRAVEDAD OFF // Según RULES.md: "BOIDS a PHYSICS: Pulsando la tecla G: Gravedad OFF" if (current_mode_ == SimulationMode::BOIDS) { toggleBoidsMode(false); // Cambiar a PHYSICS sin activar gravedad (preserva inercia) // NO llamar a forceBallsGravityOff() porque aplica impulsos que destruyen la inercia de BOIDS // La gravedad ya está desactivada por BoidManager::activateBoids() y se mantiene al salir showNotificationForAction("Modo Física - Gravedad Off"); return; } // Si estamos en modo figura, salir a modo física SIN GRAVEDAD if (current_mode_ == SimulationMode::SHAPE) { toggleShapeModeInternal(false); // Desactivar figura sin forzar gravedad ON showNotificationForAction("Gravedad Off"); } else { scene_manager_->switchBallsGravity(); // Toggle normal en modo física // Determinar estado actual de gravedad (gravity_force_ != 0.0f significa ON) const Ball* first_ball = scene_manager_->getFirstBall(); bool gravity_on = (first_ball == nullptr) ? true : (first_ball->getGravityForce() != 0.0f); showNotificationForAction(gravity_on ? "Gravedad On" : "Gravedad Off"); } } void Engine::handleGravityDirectionChange(GravityDirection direction, const char* notification_text) { // Si estamos en modo boids, salir a modo física primero PRESERVANDO VELOCIDAD if (current_mode_ == SimulationMode::BOIDS) { current_mode_ = SimulationMode::PHYSICS; boid_manager_->deactivateBoids(false); // NO activar gravedad aún (preservar momentum) scene_manager_->forceBallsGravityOn(); // Activar gravedad SIN impulsos (preserva velocidad) } // Si estamos en modo figura, salir a modo física CON gravedad else if (current_mode_ == SimulationMode::SHAPE) { toggleShapeModeInternal(); // Desactivar figura (activa gravedad automáticamente) } else { scene_manager_->enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF } scene_manager_->changeGravityDirection(direction); showNotificationForAction(notification_text); } // Display y depuración void Engine::toggleDebug() { ui_manager_->toggleDebug(); } void Engine::toggleHelp() { ui_manager_->toggleHelp(); } // Figuras 3D void Engine::toggleShapeMode() { toggleShapeModeInternal(); // Mostrar notificación según el modo actual después del toggle if (current_mode_ == SimulationMode::PHYSICS) { showNotificationForAction("Modo Física"); } else { // Mostrar nombre de la figura actual (orden debe coincidir con enum ShapeType) // Índices: 0=NONE, 1=SPHERE, 2=CUBE, 3=HELIX, 4=TORUS, 5=LISSAJOUS, 6=CYLINDER, 7=ICOSAHEDRON, 8=ATOM, 9=PNG_SHAPE const char* shape_names[] = {"Ninguna", "Esfera", "Cubo", "Hélice", "Toroide", "Lissajous", "Cilindro", "Icosaedro", "Átomo", "Forma PNG"}; showNotificationForAction(shape_names[static_cast(shape_manager_->getCurrentShapeType())]); } } void Engine::activateShape(ShapeType type, const char* notification_text) { activateShapeInternal(type); showNotificationForAction(notification_text); } void Engine::handleShapeScaleChange(bool increase) { // Delegar a ShapeManager (gestiona escala y muestra notificación en SANDBOX) shape_manager_->handleShapeScaleChange(increase); } void Engine::resetShapeScale() { // Delegar a ShapeManager (resetea escala y muestra notificación en SANDBOX) shape_manager_->resetShapeScale(); } void Engine::toggleDepthZoom() { // Delegar a ShapeManager (toggle depth zoom y muestra notificación en SANDBOX) shape_manager_->toggleDepthZoom(); } // Boids (comportamiento de enjambre) void Engine::toggleBoidsMode(bool force_gravity_on) { if (current_mode_ == SimulationMode::BOIDS) { // Salir del modo boids (velocidades ya son time-based, no requiere conversión) current_mode_ = SimulationMode::PHYSICS; boid_manager_->deactivateBoids(force_gravity_on); // Pasar parámetro para control preciso } else { // 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; } // Activar modo boids current_mode_ = SimulationMode::BOIDS; boid_manager_->activateBoids(); } } // Temas de colores void Engine::cycleTheme(bool forward) { if (forward) { theme_manager_->cycleTheme(); } else { theme_manager_->cyclePrevTheme(); } showNotificationForAction(theme_manager_->getCurrentThemeNameES()); } void Engine::switchThemeByNumpad(int numpad_key) { // Mapear tecla numpad a índice de tema según página actual int theme_index = -1; if (theme_page_ == 0) { // Página 0: Temas 0-9 (estáticos + SUNRISE) if (numpad_key >= 0 && numpad_key <= 9) { theme_index = (numpad_key == 0) ? 9 : (numpad_key - 1); } } else { // Página 1: Temas 10-14 (dinámicos) if (numpad_key >= 1 && numpad_key <= 5) { theme_index = 9 + numpad_key; } } if (theme_index != -1) { theme_manager_->switchToTheme(theme_index); showNotificationForAction(theme_manager_->getCurrentThemeNameES()); } } void Engine::toggleThemePage() { theme_page_ = (theme_page_ == 0) ? 1 : 0; showNotificationForAction((theme_page_ == 0) ? "Página 1" : "Página 2"); } void Engine::pauseDynamicTheme() { theme_manager_->pauseDynamic(); } // Sprites/Texturas void Engine::switchTexture() { switchTextureInternal(true); // Mostrar notificación en modo manual } // Control manual del benchmark (--skip-benchmark, --max-balls) void Engine::setSkipBenchmark() { skip_benchmark_ = true; } void Engine::setMaxBallsOverride(int n) { skip_benchmark_ = true; int best = DEMO_AUTO_MIN_SCENARIO; for (int i = DEMO_AUTO_MIN_SCENARIO; i <= DEMO_AUTO_MAX_SCENARIO; ++i) { if (BALL_COUNT_SCENARIOS[i] <= n) best = i; else break; } max_auto_scenario_ = best; } // Escenario custom (--custom-balls) void Engine::setCustomScenario(int balls) { custom_scenario_balls_ = balls; custom_scenario_enabled_ = true; // scene_manager_ puede no existir aún (llamada pre-init); propagación en initialize() if (scene_manager_) scene_manager_->setCustomBallCount(balls); } // Escenarios (número de pelotas) void Engine::changeScenario(int scenario_id, const char* notification_text) { // Pasar el modo actual al SceneManager para inicialización correcta scene_manager_->changeScenario(scenario_id, current_mode_); // Si estamos en modo SHAPE, regenerar la figura con nuevo número de pelotas if (current_mode_ == SimulationMode::SHAPE) { generateShape(); scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario } // Si estamos en modo BOIDS, desactivar gravedad (modo BOIDS = gravedad OFF siempre) if (current_mode_ == SimulationMode::BOIDS) { scene_manager_->forceBallsGravityOff(); } showNotificationForAction(notification_text); } // Zoom y fullscreen void Engine::handleZoomIn() { if (!fullscreen_enabled_ && !real_fullscreen_enabled_) { zoomIn(); } } void Engine::handleZoomOut() { if (!fullscreen_enabled_ && !real_fullscreen_enabled_) { zoomOut(); } } // Modos de aplicación (DEMO/LOGO) - Delegados a StateManager void Engine::toggleDemoMode() { AppMode prev_mode = state_manager_->getCurrentMode(); state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_); AppMode new_mode = state_manager_->getCurrentMode(); // Mostrar notificación según el modo resultante if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) { showNotificationForAction("MODO SANDBOX"); } else if (new_mode == AppMode::DEMO && prev_mode != AppMode::DEMO) { showNotificationForAction("MODO DEMO"); } } void Engine::toggleDemoLiteMode() { AppMode prev_mode = state_manager_->getCurrentMode(); state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_); AppMode new_mode = state_manager_->getCurrentMode(); // Mostrar notificación según el modo resultante if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) { showNotificationForAction("MODO SANDBOX"); } else if (new_mode == AppMode::DEMO_LITE && prev_mode != AppMode::DEMO_LITE) { showNotificationForAction("MODO DEMO LITE"); } } void Engine::toggleLogoMode() { AppMode prev_mode = state_manager_->getCurrentMode(); state_manager_->toggleLogoMode(current_screen_width_, current_screen_height_, scene_manager_->getBallCount()); AppMode new_mode = state_manager_->getCurrentMode(); // Mostrar notificación según el modo resultante if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) { showNotificationForAction("MODO SANDBOX"); } else if (new_mode == AppMode::LOGO && prev_mode != AppMode::LOGO) { showNotificationForAction("MODO LOGO"); } } void Engine::render() { // Limpiar framebuffer completamente (evita artefactos en barras negras al cambiar modos) SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); // Negro para barras de letterbox/integer SDL_RenderClear(renderer_); // Renderizar fondo degradado (delegado a ThemeManager) { float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b; theme_manager_->getBackgroundColors(top_r, top_g, top_b, bottom_r, bottom_g, bottom_b); // Crear quad de pantalla completa con degradado SDL_Vertex bg_vertices[4]; // Vértice superior izquierdo bg_vertices[0].position = {0, 0}; bg_vertices[0].tex_coord = {0.0f, 0.0f}; bg_vertices[0].color = {top_r, top_g, top_b, 1.0f}; // Vértice superior derecho bg_vertices[1].position = {static_cast(current_screen_width_), 0}; bg_vertices[1].tex_coord = {1.0f, 0.0f}; bg_vertices[1].color = {top_r, top_g, top_b, 1.0f}; // Vértice inferior derecho bg_vertices[2].position = {static_cast(current_screen_width_), static_cast(current_screen_height_)}; bg_vertices[2].tex_coord = {1.0f, 1.0f}; bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f}; // Vértice inferior izquierdo bg_vertices[3].position = {0, static_cast(current_screen_height_)}; bg_vertices[3].tex_coord = {0.0f, 1.0f}; bg_vertices[3].color = {bottom_r, bottom_g, bottom_b, 1.0f}; // Índices para 2 triángulos int bg_indices[6] = {0, 1, 2, 2, 3, 0}; // Renderizar sin textura (nullptr) SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6); } // Limpiar batches del frame anterior batch_vertices_.clear(); batch_indices_.clear(); // Obtener referencia a las bolas desde SceneManager const auto& balls = scene_manager_->getBalls(); if (current_mode_ == SimulationMode::SHAPE) { // MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm) // Las pelotas con menor depth_brightness (más lejos/oscuras) se renderizan primero // Bucket sort per profunditat Z (O(N) vs O(N log N)) for (size_t i = 0; i < balls.size(); i++) { int b = static_cast(balls[i]->getDepthBrightness() * (DEPTH_SORT_BUCKETS - 1)); depth_buckets_[std::clamp(b, 0, DEPTH_SORT_BUCKETS - 1)].push_back(i); } // Renderizar en orden de profundidad (bucket 0 = fons, bucket 255 = davant) for (int b = 0; b < DEPTH_SORT_BUCKETS; b++) { for (size_t idx : depth_buckets_[b]) { SDL_FRect pos = balls[idx]->getPosition(); Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP) float brightness = balls[idx]->getDepthBrightness(); float depth_scale = balls[idx]->getDepthScale(); // Mapear brightness de 0-1 a rango MIN-MAX float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f; // Aplicar factor de brillo al color int r_mod = static_cast(color.r * brightness_factor); int g_mod = static_cast(color.g * brightness_factor); int b_mod = static_cast(color.b * brightness_factor); addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, r_mod, g_mod, b_mod, depth_scale); } depth_buckets_[b].clear(); // netejar per al proper frame } } else { // MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad) const auto& balls = scene_manager_->getBalls(); size_t idx = 0; for (auto& ball : balls) { SDL_FRect pos = ball->getPosition(); Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP) addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f); idx++; } } // Renderizar todas las bolas en una sola llamada if (!batch_vertices_.empty()) { SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast(batch_vertices_.size()), batch_indices_.data(), static_cast(batch_indices_.size())); } // SISTEMA DE TEXTO ANTIGUO DESHABILITADO // Reemplazado completamente por el sistema de notificaciones (Notifier) // El doble renderizado causaba que aparecieran textos duplicados detrás de las notificaciones /* if (show_text_) { // Obtener datos del tema actual (delegado a ThemeManager) int text_color_r, text_color_g, text_color_b; theme_manager_->getCurrentThemeTextColor(text_color_r, text_color_g, text_color_b); const char* theme_name_es = theme_manager_->getCurrentThemeNameES(); // Calcular espaciado dinámico int line_height = text_renderer_.getTextHeight(); int margin = 8; // Texto del número de pelotas con color del tema text_renderer_.printPhysical(text_pos_, margin, text_.c_str(), text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y); // Mostrar nombre del tema en castellano debajo del número de pelotas // (solo si text_ NO es ya el nombre del tema, para evitar duplicación) if (theme_name_es != nullptr && text_ != theme_name_es) { int theme_text_width = text_renderer_.getTextWidth(theme_name_es); int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente int theme_y = margin + line_height; // Espaciado dinámico // Texto del nombre del tema con el mismo color text_renderer_.printPhysical(theme_x, theme_y, theme_name_es, text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y); } } */ // Renderizar UI (debug HUD, texto obsoleto, notificaciones) - delegado a UIManager ui_manager_->render(renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(), 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 if (app_logo_) { app_logo_->render(); } SDL_RenderPresent(renderer_); } void Engine::showNotificationForAction(const std::string& text) { // IMPORTANTE: Esta función solo se llama desde handlers de teclado (acciones manuales) // NUNCA se llama desde código automático (DEMO/LOGO), por lo tanto siempre mostramos notificación // Delegar a UIManager ui_manager_->showNotification(text, NOTIFICATION_DURATION); } void Engine::pushBallsAwayFromGravity() { scene_manager_->pushBallsAwayFromGravity(); } void Engine::toggleVSync() { vsync_enabled_ = !vsync_enabled_; // Actualizar texto en UIManager ui_manager_->updateVSyncText(vsync_enabled_); // Aplicar el cambio de V-Sync al renderizador SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0); } void Engine::toggleFullscreen() { // Si está en modo real fullscreen, primero salir de él if (real_fullscreen_enabled_) { toggleRealFullscreen(); // Esto lo desactiva } fullscreen_enabled_ = !fullscreen_enabled_; SDL_SetWindowFullscreen(window_, fullscreen_enabled_); // Si acabamos de salir de fullscreen, restaurar tamaño de ventana if (!fullscreen_enabled_) { SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_); SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); } // Actualizar dimensiones físicas después del cambio updatePhysicalWindowSize(); } void Engine::toggleRealFullscreen() { // Si está en modo fullscreen normal, primero desactivarlo if (fullscreen_enabled_) { fullscreen_enabled_ = false; SDL_SetWindowFullscreen(window_, false); } real_fullscreen_enabled_ = !real_fullscreen_enabled_; if (real_fullscreen_enabled_) { // Obtener resolución del escritorio int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); if (displays != nullptr && num_displays > 0) { const auto* dm = SDL_GetCurrentDisplayMode(displays[0]); if (dm != nullptr) { // Cambiar a resolución nativa del escritorio current_screen_width_ = dm->w; current_screen_height_ = dm->h; // Recrear ventana con nueva resolución SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_); SDL_SetWindowFullscreen(window_, true); // Actualizar presentación lógica del renderizador SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); // Actualizar tamaño físico de ventana y fuentes updatePhysicalWindowSize(); // Reinicar la escena con nueva resolución scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_); scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_); // Actualizar tamaño de pantalla para boids (wrapping boundaries) boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_); // Actualizar AppLogo con nueva resolución if (app_logo_) { app_logo_->updateScreenSize(current_screen_width_, current_screen_height_); } // Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones if (current_mode_ == SimulationMode::SHAPE) { generateShape(); // Regenerar figura con nuevas dimensiones de pantalla scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario } } SDL_free(displays); } } else { // Volver a resolución base (configurada por CLI o default) current_screen_width_ = base_screen_width_; current_screen_height_ = base_screen_height_; // Restaurar ventana normal con el zoom actual (no hardcoded) SDL_SetWindowFullscreen(window_, false); SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_); SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); // Restaurar presentación lógica base SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); // Actualizar tamaño físico de ventana y fuentes updatePhysicalWindowSize(); // Reinicar la escena con resolución original scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_); scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_); // Actualizar AppLogo con resolución restaurada if (app_logo_) { app_logo_->updateScreenSize(current_screen_width_, current_screen_height_); } // Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones if (current_mode_ == SimulationMode::SHAPE) { generateShape(); // Regenerar figura con nuevas dimensiones de pantalla scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario } } } void Engine::toggleIntegerScaling() { // Solo permitir cambio si estamos en modo fullscreen normal (F3) if (!fullscreen_enabled_) { return; // No hacer nada si no estamos en fullscreen } // Ciclar entre los 3 modos: INTEGER → LETTERBOX → STRETCH → INTEGER switch (current_scaling_mode_) { case ScalingMode::INTEGER: current_scaling_mode_ = ScalingMode::LETTERBOX; break; case ScalingMode::LETTERBOX: current_scaling_mode_ = ScalingMode::STRETCH; break; case ScalingMode::STRETCH: current_scaling_mode_ = ScalingMode::INTEGER; break; } // Aplicar el nuevo modo de escalado SDL_RendererLogicalPresentation presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; const char* mode_name = "INTEGER"; switch (current_scaling_mode_) { case ScalingMode::INTEGER: presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; mode_name = "INTEGER"; break; case ScalingMode::LETTERBOX: presentation = SDL_LOGICAL_PRESENTATION_LETTERBOX; mode_name = "LETTERBOX"; break; case ScalingMode::STRETCH: presentation = SDL_LOGICAL_PRESENTATION_STRETCH; mode_name = "STRETCH"; break; } SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, presentation); // Mostrar notificación del cambio std::string notification = std::string("Escalado: ") + mode_name; ui_manager_->showNotification(notification); } void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) { int vertex_index = static_cast(batch_vertices_.size()); // Crear 4 vértices para el quad (2 triángulos) SDL_Vertex vertices[4]; // Convertir colores de int (0-255) a float (0.0-1.0) float rf = r / 255.0f; float gf = g / 255.0f; float bf = b / 255.0f; // Aplicar escala al tamaño (centrado en el punto x, y) float scaled_w = w * scale; float scaled_h = h * scale; float offset_x = (w - scaled_w) / 2.0f; // Offset para centrar float offset_y = (h - scaled_h) / 2.0f; // Vértice superior izquierdo vertices[0].position = {x + offset_x, y + offset_y}; vertices[0].tex_coord = {0.0f, 0.0f}; vertices[0].color = {rf, gf, bf, 1.0f}; // Vértice superior derecho vertices[1].position = {x + offset_x + scaled_w, y + offset_y}; vertices[1].tex_coord = {1.0f, 0.0f}; vertices[1].color = {rf, gf, bf, 1.0f}; // Vértice inferior derecho vertices[2].position = {x + offset_x + scaled_w, y + offset_y + scaled_h}; vertices[2].tex_coord = {1.0f, 1.0f}; vertices[2].color = {rf, gf, bf, 1.0f}; // Vértice inferior izquierdo vertices[3].position = {x + offset_x, y + offset_y + scaled_h}; vertices[3].tex_coord = {0.0f, 1.0f}; vertices[3].color = {rf, gf, bf, 1.0f}; // Añadir vértices al batch for (int i = 0; i < 4; i++) { batch_vertices_.push_back(vertices[i]); } // Añadir índices para 2 triángulos batch_indices_.push_back(vertex_index + 0); batch_indices_.push_back(vertex_index + 1); batch_indices_.push_back(vertex_index + 2); batch_indices_.push_back(vertex_index + 2); batch_indices_.push_back(vertex_index + 3); batch_indices_.push_back(vertex_index + 0); } // Sistema de zoom dinámico int Engine::calculateMaxWindowZoom() const { // Obtener información del display usando el método de Coffee Crisis int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); if (displays == nullptr || num_displays == 0) { return WINDOW_ZOOM_MIN; // Fallback si no se puede obtener } // Obtener el modo de display actual const auto* dm = SDL_GetCurrentDisplayMode(displays[0]); if (dm == nullptr) { SDL_free(displays); return WINDOW_ZOOM_MIN; } // Calcular zoom máximo usando la fórmula de Coffee Crisis const int MAX_ZOOM = std::min(dm->w / base_screen_width_, (dm->h - WINDOW_DECORATION_HEIGHT) / base_screen_height_); SDL_free(displays); // Aplicar límites return std::max(WINDOW_ZOOM_MIN, std::min(MAX_ZOOM, WINDOW_ZOOM_MAX)); } void Engine::setWindowZoom(int new_zoom) { // Validar zoom int max_zoom = calculateMaxWindowZoom(); new_zoom = std::max(WINDOW_ZOOM_MIN, std::min(new_zoom, max_zoom)); if (new_zoom == current_window_zoom_) { return; // No hay cambio } // Obtener posición actual del centro de la ventana int current_x, current_y; SDL_GetWindowPosition(window_, ¤t_x, ¤t_y); int current_center_x = current_x + (base_screen_width_ * current_window_zoom_) / 2; int current_center_y = current_y + (base_screen_height_ * current_window_zoom_) / 2; // Calcular nuevo tamaño int new_width = base_screen_width_ * new_zoom; int new_height = base_screen_height_ * new_zoom; // Calcular nueva posición (centrada en el punto actual) int new_x = current_center_x - new_width / 2; int new_y = current_center_y - new_height / 2; // Obtener límites del escritorio para no salirse SDL_Rect display_bounds; if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &display_bounds) == 0) { // Aplicar márgenes int min_x = WINDOW_DESKTOP_MARGIN; int min_y = WINDOW_DESKTOP_MARGIN; int max_x = display_bounds.w - new_width - WINDOW_DESKTOP_MARGIN; int max_y = display_bounds.h - new_height - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT; // Limitar posición new_x = std::max(min_x, std::min(new_x, max_x)); new_y = std::max(min_y, std::min(new_y, max_y)); } // Aplicar cambios SDL_SetWindowSize(window_, new_width, new_height); SDL_SetWindowPosition(window_, new_x, new_y); current_window_zoom_ = new_zoom; // Actualizar tamaño físico de ventana y fuentes updatePhysicalWindowSize(); } void Engine::zoomIn() { setWindowZoom(current_window_zoom_ + 1); } void Engine::zoomOut() { setWindowZoom(current_window_zoom_ - 1); } void Engine::updatePhysicalWindowSize() { if (real_fullscreen_enabled_) { // En fullscreen real (F4), usar resolución del display physical_window_width_ = current_screen_width_; physical_window_height_ = current_screen_height_; } else if (fullscreen_enabled_) { // En fullscreen F3, obtener tamaño REAL del display (no del framebuffer lógico) int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); if (displays != nullptr && num_displays > 0) { const auto* dm = SDL_GetCurrentDisplayMode(displays[0]); if (dm != nullptr) { physical_window_width_ = dm->w; physical_window_height_ = dm->h; } SDL_free(displays); } } else { // En modo ventana, obtener tamaño FÍSICO real del framebuffer int window_w = 0, window_h = 0; SDL_GetWindowSizeInPixels(window_, &window_w, &window_h); physical_window_width_ = window_w; physical_window_height_ = window_h; } // Notificar a UIManager del cambio de tamaño (delegado) ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_); } // ============================================================================ // MÉTODOS PÚBLICOS PARA STATEMANAGER (automatización sin notificación) // ============================================================================ void Engine::enterShapeMode(ShapeType type) { activateShapeInternal(type); } void Engine::exitShapeMode(bool force_gravity) { toggleShapeModeInternal(force_gravity); } void Engine::switchTextureSilent() { switchTextureInternal(false); } 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) // Sistema de cambio de sprites dinámico void Engine::switchTextureInternal(bool show_notification) { if (textures_.empty()) return; // Cambiar a siguiente textura (ciclar) current_texture_index_ = (current_texture_index_ + 1) % textures_.size(); texture_ = textures_[current_texture_index_]; // Obtener nuevo tamaño de la textura int new_size = texture_->getWidth(); current_ball_size_ = new_size; // Actualizar texturas y tamaños de todas las pelotas (delegado a SceneManager) scene_manager_->updateBallTexture(texture_, new_size); // Mostrar notificación con el nombre de la textura (solo si se solicita) if (show_notification) { std::string texture_name = texture_names_[current_texture_index_]; std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper); showNotificationForAction("Sprite: " + texture_name); } } // ============================================================================ // Sistema de Figuras 3D - THIN WRAPPERS (delegan a ShapeManager) // ============================================================================ void Engine::toggleShapeModeInternal(bool force_gravity_on_exit) { shape_manager_->toggleShapeMode(force_gravity_on_exit); current_mode_ = shape_manager_->getCurrentMode(); } void Engine::activateShapeInternal(ShapeType type) { shape_manager_->activateShape(type); current_mode_ = SimulationMode::SHAPE; } void Engine::generateShape() { shape_manager_->generateShape(); } void Engine::updateShape() { shape_manager_->update(delta_time_); } // ============================================================================ // BENCHMARK DE RENDIMIENTO // ============================================================================ 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); } 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; 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(); } 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 measured_fps = static_cast(frame_count) / (BENCH_DURATION_MS / 1000.0f); if (measured_fps >= monitor_hz) { max_auto_scenario_ = idx; restore(); return; } } max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO; restore(); }