#include "engine.h" #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 #ifdef _WIN32 #include // for GetModuleFileName #endif #include "ball.h" // for Ball #include "external/mouse.h" // for Mouse namespace #include "external/texture.h" // for Texture #include "shapes/atom_shape.h" // for AtomShape #include "shapes/cube_shape.h" // for CubeShape #include "shapes/cylinder_shape.h" // for CylinderShape #include "shapes/helix_shape.h" // for HelixShape #include "shapes/icosahedron_shape.h" // for IcosahedronShape #include "shapes/lissajous_shape.h" // for LissajousShape #include "shapes/png_shape.h" // for PNGShape #include "shapes/sphere_shape.h" // for SphereShape #include "shapes/torus_shape.h" // for TorusShape // getExecutableDirectory() ya está definido en defines.h como inline // Implementación de métodos públicos bool Engine::initialize(int width, int height, int zoom, bool fullscreen) { bool success = true; // Obtener resolución de pantalla para validación if (!SDL_Init(SDL_INIT_VIDEO)) { std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl; return false; } int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); const auto* dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr; int screen_w = dm ? dm->w : 1920; // Fallback si falla int screen_h = dm ? dm->h - WINDOW_DECORATION_HEIGHT : 1080; if (displays) SDL_free(displays); // Usar parámetros o valores por defecto int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH; int logical_height = (height > 0) ? height : DEFAULT_SCREEN_HEIGHT; int window_zoom = (zoom > 0) ? zoom : DEFAULT_WINDOW_ZOOM; // VALIDACIÓN 1: Si resolución > pantalla → reset a default if (logical_width > screen_w || logical_height > screen_h) { std::cout << "Advertencia: Resolución " << logical_width << "x" << logical_height << " excede pantalla " << screen_w << "x" << screen_h << ". Usando default " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << "\n"; logical_width = DEFAULT_SCREEN_WIDTH; logical_height = DEFAULT_SCREEN_HEIGHT; window_zoom = DEFAULT_WINDOW_ZOOM; // Reset zoom también } // VALIDACIÓN 2: Calcular max_zoom y ajustar si es necesario int max_zoom = std::min(screen_w / logical_width, screen_h / logical_height); if (window_zoom > max_zoom) { std::cout << "Advertencia: Zoom " << window_zoom << " excede máximo " << max_zoom << " para " << logical_width << "x" << logical_height << ". Ajustando a " << max_zoom << "\n"; window_zoom = max_zoom; } // Si se especificaron parámetros CLI y zoom no se especificó, usar zoom=1 if ((width > 0 || height > 0) && zoom == 0) { window_zoom = 1; } // 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 { // 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 ResourcePack if (Texture::isPackLoaded()) { auto pack_resources = Texture::getPackResourceList(); // Filtrar solo los recursos en balls/ con extensión .png for (const auto& resource : pack_resources) { if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") { std::string tex_name = resource.substr(6); // Quitar "balls/" std::string name = tex_name.substr(0, tex_name.find('.')); // Quitar extensión auto texture = std::make_shared(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 ThemeManager PRIMERO (requerido por Notifier) theme_manager_ = std::make_unique(); theme_manager_->initialize(); // Calcular tamaño físico de ventana y tamaño de fuente absoluto // NOTA: Debe llamarse DESPUÉS de inicializar ThemeManager porque notifier_.init() lo necesita updatePhysicalWindowSize(); initBalls(scenario_); } return success; } void Engine::run() { while (!should_exit_) { calculateDeltaTime(); update(); handleEvents(); 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(); // Calcular FPS fps_frame_count_++; Uint64 current_time = SDL_GetTicks(); if (current_time - fps_last_time_ >= 1000) // Actualizar cada segundo { fps_current_ = fps_frame_count_; fps_frame_count_ = 0; fps_last_time_ = current_time; fps_text_ = "fps: " + std::to_string(fps_current_); } // Bifurcar actualización según modo activo if (current_mode_ == SimulationMode::PHYSICS) { // Modo física normal: actualizar física de cada pelota for (auto& ball : balls_) { ball->update(delta_time_); // Pasar delta time a cada pelota } } else if (current_mode_ == SimulationMode::SHAPE) { // Modo Figura 3D: actualizar figura polimórfica updateShape(); } // Actualizar texto (OBSOLETO: sistema antiguo, se mantiene por compatibilidad temporal) if (show_text_) { show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION); } // Actualizar sistema de notificaciones notifier_.update(current_time); // Actualizar Modo DEMO (auto-play) updateDemoMode(); // Actualizar transiciones de temas (delegado a ThemeManager) 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); // Salir del bucle si se detecta una petición de cierre if (event.type == SDL_EVENT_QUIT) { should_exit_ = true; break; } // 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; case SDLK_SPACE: pushBallsAwayFromGravity(); break; 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("Gravity 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 ? "Gravity On" : "Gravity Off"); } break; // 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("Gravity Up"); break; 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("Gravity Down"); 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("Gravity Left"); 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("Gravity Right"); 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("Physics Mode"); } else { // Mostrar nombre de la figura actual const char* shape_names[] = {"Sphere", "Lissajous", "Helix", "Torus", "Cube", "Cylinder", "Icosahedron", "Atom", "PNG Shape"}; showNotificationForAction(shape_names[static_cast(current_shape_type_)]); } break; // Selección directa de figuras 3D case SDLK_Q: activateShape(ShapeType::SPHERE); showNotificationForAction("Sphere"); break; case SDLK_W: activateShape(ShapeType::LISSAJOUS); showNotificationForAction("Lissajous"); break; case SDLK_E: activateShape(ShapeType::HELIX); showNotificationForAction("Helix"); break; case SDLK_R: activateShape(ShapeType::TORUS); showNotificationForAction("Torus"); break; case SDLK_T: activateShape(ShapeType::CUBE); showNotificationForAction("Cube"); break; case SDLK_Y: activateShape(ShapeType::CYLINDER); showNotificationForAction("Cylinder"); break; case SDLK_U: activateShape(ShapeType::ICOSAHEDRON); showNotificationForAction("Icosahedron"); break; case SDLK_I: activateShape(ShapeType::ATOM); showNotificationForAction("Atom"); break; case SDLK_O: activateShape(ShapeType::PNG_SHAPE); showNotificationForAction("PNG Shape"); 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_ ? "Depth Zoom On" : "Depth Zoom 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; } } } } 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(); if (current_mode_ == SimulationMode::SHAPE) { // MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm) // Las pelotas con menor depth_brightness (más lejos/oscuras) se renderizan primero // Crear vector de índices para ordenamiento std::vector render_order; render_order.reserve(balls_.size()); for (size_t i = 0; i < balls_.size(); i++) { render_order.push_back(i); } // Ordenar índices por profundidad Z (menor primero = fondo primero) std::sort(render_order.begin(), render_order.end(), [this](size_t a, size_t b) { return balls_[a]->getDepthBrightness() < balls_[b]->getDepthBrightness(); }); // Renderizar en orden de profundidad (fondo → frente) for (size_t idx : render_order) { SDL_FRect pos = balls_[idx]->getPosition(); Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP) float brightness = balls_[idx]->getDepthBrightness(); float depth_scale = balls_[idx]->getDepthScale(); // Mapear brightness de 0-1 a rango MIN-MAX float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f; // Aplicar factor de brillo al color int r_mod = static_cast(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); } } else { // MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad) 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())); } // Calcular factores de escala lógica → física para texto absoluto float text_scale_x = static_cast(physical_window_width_) / static_cast(current_screen_width_); float text_scale_y = static_cast(physical_window_height_) / static_cast(current_screen_height_); // 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); } } */ // Debug display (solo si está activado con tecla H) if (show_debug_) { // Obtener altura de línea para espaciado dinámico (usando fuente debug) int line_height = text_renderer_debug_.getTextHeight(); int margin = 8; // Margen constante int current_y = margin; // Y inicial // Mostrar contador de FPS en esquina superior derecha int fps_text_width = text_renderer_debug_.getTextWidth(fps_text_.c_str()); int fps_x = current_screen_width_ - fps_text_width - margin; text_renderer_debug_.printPhysical(fps_x, current_y, fps_text_.c_str(), 255, 255, 0, text_scale_x, text_scale_y); // Amarillo // Mostrar estado V-Sync en esquina superior izquierda text_renderer_debug_.printPhysical(margin, current_y, vsync_text_.c_str(), 0, 255, 255, text_scale_x, text_scale_y); // Cian current_y += line_height; // Debug: Mostrar valores de la primera pelota (si existe) if (!balls_.empty()) { // Línea 1: Gravedad int grav_int = static_cast(balls_[0]->getGravityForce()); std::string grav_text = "Gravedad: " + std::to_string(grav_int); text_renderer_debug_.printPhysical(margin, current_y, grav_text.c_str(), 255, 0, 255, text_scale_x, text_scale_y); // Magenta current_y += line_height; // Línea 2: Velocidad Y int vy_int = static_cast(balls_[0]->getVelocityY()); std::string vy_text = "Velocidad Y: " + std::to_string(vy_int); text_renderer_debug_.printPhysical(margin, current_y, vy_text.c_str(), 255, 0, 255, text_scale_x, text_scale_y); // Magenta current_y += line_height; // Línea 3: Estado superficie std::string surface_text = balls_[0]->isOnSurface() ? "Superficie: Sí" : "Superficie: No"; text_renderer_debug_.printPhysical(margin, current_y, surface_text.c_str(), 255, 0, 255, text_scale_x, text_scale_y); // Magenta current_y += line_height; // Línea 4: Coeficiente de rebote (loss) float loss_val = balls_[0]->getLossCoefficient(); std::string loss_text = "Rebote: " + std::to_string(loss_val).substr(0, 4); text_renderer_debug_.printPhysical(margin, current_y, loss_text.c_str(), 255, 0, 255, text_scale_x, text_scale_y); // Magenta current_y += line_height; // Línea 5: Dirección de gravedad std::string gravity_dir_text = "Dirección: " + gravityDirectionToString(current_gravity_); text_renderer_debug_.printPhysical(margin, current_y, gravity_dir_text.c_str(), 255, 255, 0, text_scale_x, text_scale_y); // Amarillo current_y += line_height; } // Debug: Mostrar tema actual (delegado a ThemeManager) std::string theme_text = std::string("Tema: ") + theme_manager_->getCurrentThemeNameEN(); text_renderer_debug_.printPhysical(margin, current_y, theme_text.c_str(), 255, 255, 128, text_scale_x, text_scale_y); // Amarillo claro current_y += line_height; // Debug: Mostrar modo de simulación actual std::string mode_text; if (current_mode_ == SimulationMode::PHYSICS) { mode_text = "Modo: Física"; } else if (active_shape_) { mode_text = std::string("Modo: ") + active_shape_->getName(); } else { mode_text = "Modo: Forma"; } text_renderer_debug_.printPhysical(margin, current_y, mode_text.c_str(), 0, 255, 128, text_scale_x, text_scale_y); // Verde claro current_y += line_height; // Debug: Mostrar convergencia en modo LOGO (solo cuando está activo) if (current_app_mode_ == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) { int convergence_percent = static_cast(shape_convergence_ * 100.0f); std::string convergence_text = "Convergencia: " + std::to_string(convergence_percent) + "%"; text_renderer_debug_.printPhysical(margin, current_y, convergence_text.c_str(), 255, 128, 0, text_scale_x, text_scale_y); // Naranja current_y += line_height; } // Debug: Mostrar modo DEMO/LOGO activo (siempre visible cuando debug está ON) // FIJO en tercera fila (no se mueve con otros elementos del HUD) int fixed_y = margin + (line_height * 2); // Tercera fila fija if (current_app_mode_ == AppMode::LOGO) { const char* logo_text = "Modo Logo"; int logo_text_width = text_renderer_debug_.getTextWidth(logo_text); int logo_x = (current_screen_width_ - logo_text_width) / 2; text_renderer_debug_.printPhysical(logo_x, fixed_y, logo_text, 255, 128, 0, text_scale_x, text_scale_y); // Naranja } else if (current_app_mode_ == AppMode::DEMO) { const char* demo_text = "Modo Demo"; int demo_text_width = text_renderer_debug_.getTextWidth(demo_text); int demo_x = (current_screen_width_ - demo_text_width) / 2; text_renderer_debug_.printPhysical(demo_x, fixed_y, demo_text, 255, 165, 0, text_scale_x, text_scale_y); // Naranja } else if (current_app_mode_ == AppMode::DEMO_LITE) { const char* lite_text = "Modo Demo Lite"; int lite_text_width = text_renderer_debug_.getTextWidth(lite_text); int lite_x = (current_screen_width_ - lite_text_width) / 2; text_renderer_debug_.printPhysical(lite_x, fixed_y, lite_text, 255, 200, 0, text_scale_x, text_scale_y); // Amarillo-naranja } } // Renderizar notificaciones (siempre al final, sobre todo lo demás) notifier_.render(); SDL_RenderPresent(renderer_); } void Engine::initBalls(int value) { // Si estamos en modo figura 3D, desactivarlo antes de regenerar pelotas if (current_mode_ == SimulationMode::SHAPE) { current_mode_ = SimulationMode::PHYSICS; active_shape_.reset(); // Liberar figura actual } // Limpiar las bolas actuales balls_.clear(); // Resetear gravedad al estado por defecto (DOWN) al cambiar escenario changeGravityDirection(GravityDirection::DOWN); // Crear las bolas según el escenario for (int i = 0; i < BALL_COUNT_SCENARIOS[value]; ++i) { const int SIGN = ((rand() % 2) * 2) - 1; // Genera un signo aleatorio (+ o -) // Calcular spawn zone: margen a cada lado, zona central para spawn const int margin = static_cast(current_screen_width_ * BALL_SPAWN_MARGIN); const int spawn_zone_width = current_screen_width_ - (2 * margin); const float X = (rand() % spawn_zone_width) + margin; // Posición inicial en X const float VX = (((rand() % 20) + 10) * 0.1f) * SIGN; // Velocidad en X const float VY = ((rand() % 60) - 30) * 0.1f; // Velocidad en Y // Seleccionar color de la paleta del tema actual (delegado a ThemeManager) int random_index = rand(); Color COLOR = theme_manager_->getInitialBallColor(random_index); // Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada) float mass_factor = GRAVITY_MASS_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN); balls_.emplace_back(std::make_unique(X, VX, VY, COLOR, texture_, current_screen_width_, current_screen_height_, current_ball_size_, current_gravity_, mass_factor)); } // NOTA: setText() removido - las notificaciones ahora se llaman manualmente desde cada tecla } void Engine::setText() { // Suprimir textos durante modos demo if (current_app_mode_ != AppMode::SANDBOX) return; // Generar texto de número de pelotas int num_balls = BALL_COUNT_SCENARIOS[scenario_]; std::string notification_text; if (num_balls == 1) { notification_text = "1 Pelota"; } else if (num_balls < 1000) { notification_text = std::to_string(num_balls) + " Pelotas"; } else { // Formato con separador de miles para números grandes notification_text = std::to_string(num_balls / 1000) + "," + (num_balls % 1000 < 100 ? "0" : "") + (num_balls % 1000 < 10 ? "0" : "") + std::to_string(num_balls % 1000) + " Pelotas"; } // Mostrar notificación (colores se obtienen dinámicamente desde ThemeManager) notifier_.show(notification_text, NOTIFICATION_DURATION); // Sistema antiguo (mantener temporalmente para compatibilidad) text_ = notification_text; text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2; show_text_ = true; text_init_time_ = SDL_GetTicks(); } 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 // Los colores se obtienen dinámicamente cada frame desde ThemeManager en render() // Esto permite transiciones LERP suaves y siempre usar el color del tema actual notifier_.show(text, NOTIFICATION_DURATION); } void Engine::pushBallsAwayFromGravity() { for (auto& ball : balls_) { const int SIGNO = ((rand() % 2) * 2) - 1; const float LATERAL = (((rand() % 20) + 10) * 0.1f) * SIGNO; const float MAIN = ((rand() % 40) * 0.1f) + 5; float vx = 0, vy = 0; switch (current_gravity_) { case GravityDirection::DOWN: // Impulsar ARRIBA vx = LATERAL; vy = -MAIN; break; case GravityDirection::UP: // Impulsar ABAJO vx = LATERAL; vy = MAIN; break; case GravityDirection::LEFT: // Impulsar DERECHA vx = MAIN; vy = LATERAL; break; case GravityDirection::RIGHT: // Impulsar IZQUIERDA vx = -MAIN; vy = LATERAL; break; } ball->modVel(vx, vy); // Modifica la velocidad según dirección de gravedad } } void Engine::switchBallsGravity() { for (auto& ball : balls_) { ball->switchGravity(); } } void Engine::enableBallsGravityIfDisabled() { for (auto& ball : balls_) { ball->enableGravityIfDisabled(); } } void Engine::forceBallsGravityOn() { for (auto& ball : balls_) { ball->forceGravityOn(); } } void Engine::forceBallsGravityOff() { // Contar cuántas pelotas están en superficie (suelo/techo/pared) int balls_on_surface = 0; for (const auto& ball : balls_) { if (ball->isOnSurface()) { balls_on_surface++; } } // Si la mayoría (>50%) están en superficie, aplicar impulso para que se vea el efecto float surface_ratio = static_cast(balls_on_surface) / static_cast(balls_.size()); if (surface_ratio > 0.5f) { pushBallsAwayFromGravity(); // Dar impulso contrario a gravedad } // Desactivar gravedad for (auto& ball : balls_) { ball->forceGravityOff(); } } void Engine::changeGravityDirection(GravityDirection direction) { current_gravity_ = direction; for (auto& ball : balls_) { ball->setGravityDirection(direction); ball->applyRandomLateralPush(); // Aplicar empuje lateral aleatorio } } void Engine::toggleVSync() { vsync_enabled_ = !vsync_enabled_; vsync_text_ = vsync_enabled_ ? "V-Sync: On" : "V-Sync: Off"; // Aplicar el cambio de V-Sync al renderizador SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0); } void Engine::toggleFullscreen() { // Si está en modo real fullscreen, primero salir de él if (real_fullscreen_enabled_) { toggleRealFullscreen(); // Esto lo desactiva } fullscreen_enabled_ = !fullscreen_enabled_; SDL_SetWindowFullscreen(window_, fullscreen_enabled_); // Actualizar dimensiones físicas después del cambio updatePhysicalWindowSize(); } void Engine::toggleRealFullscreen() { // Si está en modo fullscreen normal, primero desactivarlo if (fullscreen_enabled_) { fullscreen_enabled_ = false; SDL_SetWindowFullscreen(window_, false); } real_fullscreen_enabled_ = !real_fullscreen_enabled_; if (real_fullscreen_enabled_) { // Obtener resolución del escritorio int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); if (displays != nullptr && num_displays > 0) { const auto* dm = SDL_GetCurrentDisplayMode(displays[0]); if (dm != nullptr) { // Cambiar a resolución nativa del escritorio current_screen_width_ = dm->w; current_screen_height_ = dm->h; // Recrear ventana con nueva resolución SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_); SDL_SetWindowFullscreen(window_, true); // Actualizar presentación lógica del renderizador SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); // Actualizar tamaño físico de ventana y fuentes updatePhysicalWindowSize(); // Reinicar la escena con nueva resolución initBalls(scenario_); } 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 SDL_SetWindowFullscreen(window_, false); SDL_SetWindowSize(window_, base_screen_width_ * DEFAULT_WINDOW_ZOOM, base_screen_height_ * DEFAULT_WINDOW_ZOOM); // 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 initBalls(scenario_); } } 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 texto informativo text_ = "Escalado: "; text_ += mode_name; text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2; show_text_ = true; text_init_time_ = SDL_GetTicks(); } std::string Engine::gravityDirectionToString(GravityDirection direction) const { switch (direction) { case GravityDirection::DOWN: return "DOWN"; case GravityDirection::UP: return "UP"; case GravityDirection::LEFT: return "LEFT"; case GravityDirection::RIGHT: return "RIGHT"; default: return "UNKNOWN"; } } 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) // SDL_GetRenderOutputSize() falla en F3 (devuelve tamaño lógico 960x720) // Necesitamos el tamaño FÍSICO real de la pantalla 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; } // Recalcular tamaño de fuente basado en altura física // Referencia: 8px a 1440p (monitor del usuario) int font_size = (physical_window_height_ * TEXT_BASE_SIZE) / 1440; if (font_size < 6) font_size = 6; // Tamaño mínimo legible // Reinicializar TextRenderers con nuevo tamaño de fuente text_renderer_.cleanup(); text_renderer_debug_.cleanup(); text_renderer_notifier_.cleanup(); text_renderer_.init(renderer_, TEXT_FONT_PATH, font_size, TEXT_ANTIALIASING); text_renderer_debug_.init(renderer_, TEXT_FONT_PATH, font_size, TEXT_ANTIALIASING); // TextRenderer para notificaciones: Detectar DPI y ajustar tamaño // En pantallas Retina/HiDPI, el texto necesita ser más grande para ser legible int logical_w = 0, logical_h = 0; SDL_GetWindowSize(window_, &logical_w, &logical_h); // Usar physical_window_width_ que ya contiene el tamaño real del framebuffer // (calculado arriba con SDL_GetRenderOutputSize o current_screen_width_) int pixels_w = physical_window_width_; // Calcular escala DPI (1.0 normal, 2.0 Retina, 3.0 en algunos displays) float dpi_scale = (logical_w > 0) ? static_cast(pixels_w) / static_cast(logical_w) : 1.0f; // Ajustar tamaño de fuente base (16px) por escala DPI // Retina macOS: 16px * 2.0 = 32px (legible) // Normal: 16px * 1.0 = 16px int notification_font_size = static_cast(TEXT_ABSOLUTE_SIZE * dpi_scale); if (notification_font_size < 12) notification_font_size = 12; // Mínimo legible text_renderer_notifier_.init(renderer_, TEXT_FONT_PATH, notification_font_size, TEXT_ANTIALIASING); // Inicializar/actualizar Notifier con nuevas dimensiones y ThemeManager // NOTA: init() es seguro de llamar múltiples veces, solo actualiza punteros y dimensiones // Esto asegura que el notifier tenga las referencias correctas tras resize/fullscreen notifier_.init(renderer_, &text_renderer_notifier_, theme_manager_.get(), physical_window_width_, physical_window_height_); } // ============================================================================ // Sistema de gestión de estados (MANUAL/DEMO/DEMO_LITE/LOGO) // ============================================================================ void Engine::setState(AppMode new_mode) { // Si ya estamos en ese modo, no hacer nada if (current_app_mode_ == new_mode) return; // Al salir de LOGO, guardar en previous_app_mode_ (para volver al modo correcto) if (current_app_mode_ == AppMode::LOGO && new_mode != AppMode::LOGO) { previous_app_mode_ = new_mode; } // Al entrar a LOGO, guardar el modo previo if (new_mode == AppMode::LOGO) { previous_app_mode_ = current_app_mode_; } // Aplicar el nuevo modo current_app_mode_ = new_mode; // Configurar timer de demo según el modo if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) { demo_timer_ = 0.0f; float min_interval, max_interval; if (new_mode == AppMode::LOGO) { // Escalar tiempos con resolución (720p como base) float resolution_scale = current_screen_height_ / 720.0f; logo_min_time_ = LOGO_ACTION_INTERVAL_MIN * resolution_scale; logo_max_time_ = LOGO_ACTION_INTERVAL_MAX * resolution_scale; min_interval = logo_min_time_; max_interval = logo_max_time_; } else { bool is_lite = (new_mode == AppMode::DEMO_LITE); min_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN; max_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX; } demo_next_action_time_ = min_interval + (rand() % 1000) / 1000.0f * (max_interval - min_interval); } } // ============================================================================ // Sistema de Modo DEMO (auto-play) // ============================================================================ void Engine::updateDemoMode() { // Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO) if (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 = 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 (current_app_mode_ == 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) { toggleShapeMode(true); // Con gravedad ON } else { toggleShapeMode(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) toggleShapeMode(true); } else { // 50%: SHAPE → PHYSICS con gravedad OFF (dar vueltas sin caer) toggleShapeMode(false); } // Resetear variables de espera de flips al cambiar a PHYSICS logo_waiting_for_flip_ = false; logo_current_flip_count_ = 0; // Resetear timer con intervalos escalados demo_timer_ = 0.0f; float interval_range = logo_max_time_ - logo_min_time_; demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range; } } else { // Logo animado (PHYSICS) → 3 opciones posibles if (action < 60) { // 60%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones) toggleShapeMode(false); // Resetear variables de espera de flips al volver a SHAPE logo_waiting_for_flip_ = false; logo_current_flip_count_ = 0; } else if (action < 80) { // 20%: Forzar gravedad ON (empezar a caer mientras da vueltas) forceBallsGravityOn(); } else { // 20%: Forzar gravedad OFF (flotar mientras da vueltas) forceBallsGravityOff(); } // Resetear timer con intervalos escalados demo_timer_ = 0.0f; float interval_range = logo_max_time_ - logo_min_time_; demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range; } // Solo salir automáticamente si NO llegamos desde MANUAL // Probabilidad de salir: 60% en cada acción → sale rápido (relación DEMO:LOGO = 6:1) if (previous_app_mode_ != AppMode::SANDBOX && rand() % 100 < 60) { exitLogoMode(true); // Volver a DEMO/DEMO_LITE } } // 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; } } } void Engine::performDemoAction(bool is_lite) { // ============================================ // SALTO AUTOMÁTICO A LOGO MODE (Easter Egg) // ============================================ if (is_lite) { // DEMO LITE: Verificar condiciones para salto a Logo Mode if (static_cast(balls_.size()) >= LOGO_MODE_MIN_BALLS && theme_manager_->getCurrentThemeIndex() == 5) { // MONOCHROME // 10% probabilidad de saltar a Logo Mode if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) { enterLogoMode(true); // Entrar desde DEMO return; } } } else { // DEMO COMPLETO: Verificar condiciones para salto a Logo Mode if (static_cast(balls_.size()) >= LOGO_MODE_MIN_BALLS) { // 15% probabilidad de saltar a Logo Mode if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) { enterLogoMode(true); // Entrar desde DEMO return; } } } // ============================================ // ACCIONES NORMALES DE DEMO/DEMO_LITE // ============================================ int TOTAL_WEIGHT; int random_value; int accumulated_weight = 0; if (is_lite) { // DEMO LITE: Solo física/figuras TOTAL_WEIGHT = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE; random_value = rand() % TOTAL_WEIGHT; // Cambiar dirección gravedad (25%) accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR; if (random_value < accumulated_weight) { GravityDirection new_direction = static_cast(rand() % 4); changeGravityDirection(new_direction); return; } // Toggle gravedad ON/OFF (20%) accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_TOGGLE; if (random_value < accumulated_weight) { toggleGravityOnOff(); return; } // Activar figura 3D (25%) - PNG_SHAPE excluido (reservado para Logo Mode) accumulated_weight += DEMO_LITE_WEIGHT_SHAPE; if (random_value < accumulated_weight) { ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; int shape_index = rand() % 8; activateShape(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 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); changeGravityDirection(new_direction); return; } // Toggle gravedad ON/OFF (8%) accumulated_weight += DEMO_WEIGHT_GRAVITY_TOGGLE; if (random_value < accumulated_weight) { toggleGravityOnOff(); return; } // Activar figura 3D (20%) - PNG_SHAPE excluido (reservado para Logo Mode) accumulated_weight += DEMO_WEIGHT_SHAPE; if (random_value < accumulated_weight) { ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; int shape_index = rand() % 8; activateShape(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 return; } // Re-generar misma figura (8%) accumulated_weight += DEMO_WEIGHT_REGENERATE_SHAPE; if (random_value < accumulated_weight) { if (current_mode_ == SimulationMode::SHAPE && active_shape_) { generateShape(); // Re-generar sin cambiar tipo } return; } // Cambiar tema (15%) accumulated_weight += DEMO_WEIGHT_THEME; if (random_value < accumulated_weight) { // Elegir entre TODOS los 15 temas (9 estáticos + 6 dinámicos) int random_theme_index = rand() % 15; theme_manager_->switchToTheme(random_theme_index); return; } // Cambiar escenario (10%) - EXCLUIR índices 0, 6, 7 (1, 50K, 100K pelotas) accumulated_weight += DEMO_WEIGHT_SCENARIO; if (random_value < accumulated_weight) { // Escenarios válidos: índices 1, 2, 3, 4, 5 (10, 100, 500, 1000, 10000 pelotas) int valid_scenarios[] = {1, 2, 3, 4, 5}; scenario_ = valid_scenarios[rand() % 5]; initBalls(scenario_); return; } // Aplicar impulso (10%) accumulated_weight += DEMO_WEIGHT_IMPULSE; if (random_value < accumulated_weight) { pushBallsAwayFromGravity(); return; } // Toggle profundidad (3%) accumulated_weight += DEMO_WEIGHT_DEPTH_ZOOM; if (random_value < accumulated_weight) { if (current_mode_ == SimulationMode::SHAPE) { depth_zoom_enabled_ = !depth_zoom_enabled_; } return; } // Cambiar escala de figura (2%) accumulated_weight += DEMO_WEIGHT_SHAPE_SCALE; if (random_value < accumulated_weight) { if (current_mode_ == SimulationMode::SHAPE) { int scale_action = rand() % 3; if (scale_action == 0) { shape_scale_factor_ += SHAPE_SCALE_STEP; } else if (scale_action == 1) { shape_scale_factor_ -= SHAPE_SCALE_STEP; } else { shape_scale_factor_ = SHAPE_SCALE_DEFAULT; } clampShapeScale(); generateShape(); } return; } // Cambiar sprite (2%) accumulated_weight += DEMO_WEIGHT_SPRITE; if (random_value < accumulated_weight) { switchTexture(false); // Suprimir notificación en modo automático return; } } } // Randomizar todo al iniciar modo DEMO void Engine::randomizeOnDemoStart(bool is_lite) { 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) { toggleShapeMode(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]); } // Randomizar gravedad: dirección + ON/OFF GravityDirection new_direction = static_cast(rand() % 4); changeGravityDirection(new_direction); if (rand() % 2 == 0) { toggleGravityOnOff(); // 50% probabilidad de desactivar gravedad } } else { // DEMO COMPLETO: Randomizar TODO // 1. Escenario (excluir índices 0, 6, 7) int valid_scenarios[] = {1, 2, 3, 4, 5}; scenario_ = valid_scenarios[rand() % 5]; initBalls(scenario_); // 2. Tema (elegir entre TODOS los 15 temas) int random_theme_index = rand() % 15; theme_manager_->switchToTheme(random_theme_index); // 3. Sprite if (rand() % 2 == 0) { switchTexture(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); } } 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]); // 5. Profundidad (solo si estamos en figura) if (rand() % 2 == 0) { depth_zoom_enabled_ = !depth_zoom_enabled_; } // 6. Escala de figura (aleatoria entre 0.5x y 2.0x) shape_scale_factor_ = 0.5f + (rand() % 1500) / 1000.0f; clampShapeScale(); generateShape(); } // 7. Gravedad: dirección + ON/OFF GravityDirection new_direction = static_cast(rand() % 4); changeGravityDirection(new_direction); if (rand() % 3 == 0) { // 33% probabilidad de desactivar gravedad toggleGravityOnOff(); } } } // Toggle gravedad ON/OFF para todas las pelotas void Engine::toggleGravityOnOff() { // Alternar entre activar/desactivar gravedad bool first_ball_gravity_enabled = (balls_.empty() || balls_[0]->getGravityForce() > 0.0f); if (first_ball_gravity_enabled) { // Desactivar gravedad forceBallsGravityOff(); } else { // Activar gravedad forceBallsGravityOn(); } } // ============================================================================ // SISTEMA DE MODO LOGO (Easter Egg - "Marca de Agua") // ============================================================================ // Entrar al Modo Logo (manual con tecla K o automático desde DEMO) void Engine::enterLogoMode(bool from_demo) { // Verificar mínimo de pelotas if (static_cast(balls_.size()) < LOGO_MODE_MIN_BALLS) { // Ajustar a 5000 pelotas automáticamente scenario_ = 5; // Escenario 5000 pelotas (índice 5 en BALL_COUNT_SCENARIOS) initBalls(scenario_); } // 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 "tiny" size_t tiny_index = current_texture_index_; // Por defecto mantener actual for (size_t i = 0; i < texture_names_.size(); i++) { if (texture_names_[i] == "tiny") { tiny_index = i; break; } } // Aplicar configuración fija del Modo Logo if (tiny_index != current_texture_index_) { current_texture_index_ = tiny_index; int old_size = current_ball_size_; current_ball_size_ = textures_[current_texture_index_]->getWidth(); updateBallSizes(old_size, current_ball_size_); // Actualizar textura global y en cada pelota texture_ = textures_[current_texture_index_]; for (auto& ball : balls_) { ball->setTexture(texture_); } } // Cambiar a tema MONOCHROME theme_manager_->switchToTheme(5); // MONOCHROME // Establecer escala a 120% shape_scale_factor_ = LOGO_MODE_SHAPE_SCALE; clampShapeScale(); // Activar PNG_SHAPE (el logo) activateShape(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 } } // Resetear variables de espera de flips logo_waiting_for_flip_ = false; logo_target_flip_number_ = 0; logo_target_flip_percentage_ = 0.0f; logo_current_flip_count_ = 0; // Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente) setState(AppMode::LOGO); } // Salir del Modo Logo (volver a estado anterior o salir de DEMO) void Engine::exitLogoMode(bool return_to_demo) { if (current_app_mode_ != AppMode::LOGO) return; // Restaurar estado previo theme_manager_->switchToTheme(logo_previous_theme_); if (logo_previous_texture_index_ != current_texture_index_ && logo_previous_texture_index_ < textures_.size()) { current_texture_index_ = logo_previous_texture_index_; int old_size = current_ball_size_; current_ball_size_ = textures_[current_texture_index_]->getWidth(); updateBallSizes(old_size, current_ball_size_); // Actualizar textura global y en cada pelota texture_ = textures_[current_texture_index_]; for (auto& ball : balls_) { ball->setTexture(texture_); } } shape_scale_factor_ = logo_previous_shape_scale_; clampShapeScale(); generateShape(); // Desactivar modo LOGO en PNG_SHAPE (volver a flip intervals normales) if (active_shape_) { PNGShape* png_shape = dynamic_cast(active_shape_.get()); if (png_shape) { png_shape->setLogoMode(false); } } if (!return_to_demo) { // Salida manual (tecla K): volver a MANUAL setState(AppMode::SANDBOX); } else { // Volver al modo previo (DEMO o DEMO_LITE) setState(previous_app_mode_); } } // 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); for (auto& ball : balls_) { SDL_FRect pos = ball->getPosition(); // Solo ajustar posición si la pelota está en superficie if (ball->isOnSurface()) { GravityDirection grav_dir = ball->getGravityDirection(); switch (grav_dir) { case GravityDirection::DOWN: // Superficie inferior: ajustar Y hacia abajo si crece pos.y += delta_size; break; case GravityDirection::UP: // Superficie superior: ajustar Y hacia arriba si crece pos.y -= delta_size; break; case GravityDirection::LEFT: // Superficie izquierda: ajustar X hacia izquierda si crece pos.x -= delta_size; break; case GravityDirection::RIGHT: // Superficie derecha: ajustar X hacia derecha si crece pos.x += delta_size; break; } } // Actualizar tamaño del hitbox ball->updateSize(new_size); // Si ajustamos posición, aplicarla ahora if (ball->isOnSurface()) { ball->setShapeScreenPosition(pos.x, pos.y); } } } void Engine::switchTexture(bool show_notification) { if (textures_.empty()) return; // Guardar tamaño antiguo int old_size = current_ball_size_; // 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 for (auto& ball : balls_) { ball->setTexture(texture_); } // Ajustar posiciones según el cambio de tamaño updateBallSizes(old_size, new_size); // Mostrar notificación con el nombre de la textura (solo si se solicita) if (show_notification) { std::string texture_name = texture_names_[current_texture_index_]; std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper); showNotificationForAction("Sprite: " + texture_name); } } // Sistema de Figuras 3D - Alternar entre modo física y última figura (Toggle con tecla F) void Engine::toggleShapeMode(bool force_gravity_on_exit) { if (current_mode_ == SimulationMode::PHYSICS) { // Cambiar a modo figura (usar última figura seleccionada) activateShape(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) { 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 (current_app_mode_ == 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 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) { forceBallsGravityOn(); } // Mostrar texto informativo (solo si NO estamos en modo demo o logo) if (current_app_mode_ == AppMode::SANDBOX) { text_ = "Modo Física"; text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2; text_init_time_ = SDL_GetTicks(); show_text_ = true; } } } // Activar figura específica (llamado por teclas Q/W/E/R/Y/U/I o por toggleShapeMode) void Engine::activateShape(ShapeType type) { // Guardar como última figura seleccionada last_shape_type_ = type; current_shape_type_ = type; // Cambiar a modo figura current_mode_ = SimulationMode::SHAPE; // Desactivar gravedad al entrar en modo figura 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("data/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 for (auto& ball : balls_) { ball->enableShapeAttraction(true); } // Mostrar texto informativo con nombre de figura (solo si NO estamos en modo demo o logo) if (active_shape_ && current_app_mode_ == AppMode::SANDBOX) { text_ = std::string("Modo ") + active_shape_->getName(); text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2; text_init_time_ = SDL_GetTicks(); show_text_ = true; } } // Generar puntos de la figura activa void Engine::generateShape() { if (!active_shape_) return; int num_points = static_cast(balls_.size()); active_shape_->generatePoints(num_points, static_cast(current_screen_width_), static_cast(current_screen_height_)); } // Actualizar figura activa (rotación, animación, etc.) void Engine::updateShape() { if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) return; // Actualizar animación de la figura active_shape_->update(delta_time_, static_cast(current_screen_width_), static_cast(current_screen_height_)); // 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; // 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); } // Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo) if (current_app_mode_ == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) { int balls_near = 0; float distance_threshold = LOGO_CONVERGENCE_DISTANCE; // 20px fijo (más permisivo) for (const auto& ball : balls_) { if (ball->getDistanceToTarget() < distance_threshold) { balls_near++; } } shape_convergence_ = static_cast(balls_near) / balls_.size(); // Notificar a la figura sobre el porcentaje de convergencia // Esto permite que PNGShape decida cuándo empezar a contar para flips active_shape_->setConvergence(shape_convergence_); } } // Limitar escala de figura para evitar que se salga de pantalla void Engine::clampShapeScale() { // Calcular tamaño máximo permitido según resolución actual // La figura más grande (esfera/cubo) usa ~33% de altura por defecto // Permitir hasta que la figura ocupe 90% de la dimensión más pequeña float max_dimension = std::min(current_screen_width_, current_screen_height_); float base_size_factor = 0.333f; // ROTOBALL_RADIUS_FACTOR o similar float max_scale_for_screen = (max_dimension * 0.9f) / (max_dimension * base_size_factor); // Limitar entre SHAPE_SCALE_MIN y el mínimo de (SHAPE_SCALE_MAX, max_scale_for_screen) float max_allowed = std::min(SHAPE_SCALE_MAX, max_scale_for_screen); shape_scale_factor_ = std::max(SHAPE_SCALE_MIN, std::min(max_allowed, shape_scale_factor_)); }