#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 time #include // for cout #include // for string #include // for path operations #ifdef _WIN32 #include // for GetModuleFileName #endif #include "ball.h" // for Ball #include "external/dbgtxt.h" // for dbg_init, dbg_print #include "external/texture.h" // for Texture #include "shapes/sphere_shape.h" // for SphereShape #include "shapes/cube_shape.h" // for CubeShape #include "shapes/helix_shape.h" // for HelixShape #include "shapes/wave_grid_shape.h" // for WaveGridShape #include "shapes/torus_shape.h" // for TorusShape #include "shapes/cylinder_shape.h" // for CylinderShape #include "shapes/icosahedron_shape.h" // for IcosahedronShape #include "shapes/atom_shape.h" // for AtomShape // Función auxiliar para obtener la ruta del directorio del ejecutable std::string getExecutableDirectory() { #ifdef _WIN32 char buffer[MAX_PATH]; GetModuleFileNameA(NULL, buffer, MAX_PATH); std::filesystem::path exe_path(buffer); return exe_path.parent_path().string(); #else // Para Linux/macOS se podría usar readlink("/proc/self/exe") o dladdr return "."; // Fallback para otros sistemas #endif } // Implementación de métodos públicos bool Engine::initialize() { bool success = true; if (!SDL_Init(SDL_INIT_VIDEO)) { std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl; success = false; } else { // Crear ventana principal window_ = SDL_CreateWindow(WINDOW_CAPTION, SCREEN_WIDTH * WINDOW_ZOOM, SCREEN_HEIGHT * WINDOW_ZOOM, SDL_WINDOW_OPENGL); 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 SDL_SetRenderLogicalPresentation(renderer_, SCREEN_WIDTH, SCREEN_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) { // Construir ruta absoluta a la imagen std::string exe_dir = getExecutableDirectory(); std::string texture_path = exe_dir + "/data/ball.png"; texture_ = std::make_shared(renderer_, texture_path); srand(static_cast(time(nullptr))); dbg_init(renderer_); initializeThemes(); 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() { // 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 } // Verificar auto-reinicio cuando todas las pelotas están quietas (solo en modo física) checkAutoRestart(); } else if (current_mode_ == SimulationMode::SHAPE) { // Modo Figura 3D: actualizar figura polimórfica updateShape(); } // Actualizar texto (sin cambios en la lógica) if (show_text_) { show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION); } } void Engine::handleEvents() { SDL_Event event; while (SDL_PollEvent(&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 } else { switchBallsGravity(); // Toggle normal en modo física } 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); 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); 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); 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); 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(); break; // Selección directa de figuras 3D case SDLK_Q: activateShape(ShapeType::SPHERE); break; case SDLK_W: activateShape(ShapeType::WAVE_GRID); break; case SDLK_E: activateShape(ShapeType::HELIX); break; case SDLK_R: activateShape(ShapeType::TORUS); break; case SDLK_T: activateShape(ShapeType::CUBE); break; case SDLK_Y: activateShape(ShapeType::CYLINDER); break; case SDLK_U: activateShape(ShapeType::ICOSAHEDRON); break; case SDLK_I: activateShape(ShapeType::ATOM); break; // Ciclar temas de color (movido de T a B) case SDLK_B: // Ciclar al siguiente tema current_theme_ = static_cast((static_cast(current_theme_) + 1) % (sizeof(themes_) / sizeof(themes_[0]))); initBalls(scenario_); // Regenerar bolas con nueva paleta break; // Temas de colores con teclado numérico case SDLK_KP_1: current_theme_ = ColorTheme::SUNSET; initBalls(scenario_); break; case SDLK_KP_2: current_theme_ = ColorTheme::OCEAN; initBalls(scenario_); break; case SDLK_KP_3: current_theme_ = ColorTheme::NEON; initBalls(scenario_); break; case SDLK_KP_4: current_theme_ = ColorTheme::FOREST; initBalls(scenario_); break; case SDLK_KP_5: current_theme_ = ColorTheme::RGB; initBalls(scenario_); 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(); text_ = "SCALE " + std::to_string(static_cast(shape_scale_factor_ * 100.0f + 0.5f)) + "%"; int text_width = static_cast(text_.length() * 8); text_pos_ = (current_screen_width_ - text_width) / 2; text_init_time_ = SDL_GetTicks(); show_text_ = true; } break; case SDLK_KP_MINUS: if (current_mode_ == SimulationMode::SHAPE) { shape_scale_factor_ -= SHAPE_SCALE_STEP; clampShapeScale(); text_ = "SCALE " + std::to_string(static_cast(shape_scale_factor_ * 100.0f + 0.5f)) + "%"; int text_width = static_cast(text_.length() * 8); text_pos_ = (current_screen_width_ - text_width) / 2; text_init_time_ = SDL_GetTicks(); show_text_ = true; } break; case SDLK_KP_MULTIPLY: if (current_mode_ == SimulationMode::SHAPE) { shape_scale_factor_ = SHAPE_SCALE_DEFAULT; text_ = "SCALE RESET (100%)"; int text_width = static_cast(text_.length() * 8); text_pos_ = (current_screen_width_ - text_width) / 2; text_init_time_ = SDL_GetTicks(); show_text_ = true; } break; case SDLK_KP_DIVIDE: if (current_mode_ == SimulationMode::SHAPE) { depth_zoom_enabled_ = !depth_zoom_enabled_; text_ = depth_zoom_enabled_ ? "DEPTH ZOOM ON" : "DEPTH ZOOM OFF"; int text_width = static_cast(text_.length() * 8); text_pos_ = (current_screen_width_ - text_width) / 2; text_init_time_ = SDL_GetTicks(); show_text_ = true; } break; case SDLK_1: scenario_ = 0; initBalls(scenario_); break; case SDLK_2: scenario_ = 1; initBalls(scenario_); break; case SDLK_3: scenario_ = 2; initBalls(scenario_); break; case SDLK_4: scenario_ = 3; initBalls(scenario_); break; case SDLK_5: scenario_ = 4; initBalls(scenario_); break; case SDLK_6: scenario_ = 5; initBalls(scenario_); break; case SDLK_7: scenario_ = 6; initBalls(scenario_); break; case SDLK_8: scenario_ = 7; initBalls(scenario_); 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; } } } } void Engine::render() { // Renderizar fondo degradado en lugar de color sólido renderGradientBackground(); // 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 = balls_[idx]->getColor(); 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) for (auto &ball : balls_) { SDL_FRect pos = ball->getPosition(); Color color = ball->getColor(); addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f); } } // 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())); } if (show_text_) { // Colores acordes a cada tema (para texto del número de pelotas y nombre del tema) int theme_colors[][3] = { {255, 140, 60}, // ATARDECER: Naranja cálido {80, 200, 255}, // OCEANO: Azul océano {255, 60, 255}, // NEON: Magenta brillante {100, 255, 100}, // BOSQUE: Verde natural {100, 100, 100} // RGB: Gris oscuro (para contraste con fondo blanco) }; int theme_idx = static_cast(current_theme_); // Texto del número de pelotas con color del tema dbg_print(text_pos_, 8, text_.c_str(), theme_colors[theme_idx][0], theme_colors[theme_idx][1], theme_colors[theme_idx][2]); // Mostrar nombre del tema en castellano debajo del número de pelotas std::string theme_names_es[] = {"ATARDECER", "OCEANO", "NEON", "BOSQUE", "RGB"}; std::string theme_name = theme_names_es[static_cast(current_theme_)]; int theme_text_width = static_cast(theme_name.length() * 8); // 8 píxeles por carácter int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente // Texto del nombre del tema con el mismo color dbg_print(theme_x, 24, theme_name.c_str(), theme_colors[theme_idx][0], theme_colors[theme_idx][1], theme_colors[theme_idx][2]); } // Debug display (solo si está activado con tecla H) if (show_debug_) { // Mostrar contador de FPS en esquina superior derecha int fps_text_width = static_cast(fps_text_.length() * 8); // 8 píxeles por carácter int fps_x = current_screen_width_ - fps_text_width - 8; // 8 píxeles de margen dbg_print(fps_x, 8, fps_text_.c_str(), 255, 255, 0); // Amarillo para distinguir // Mostrar estado V-Sync en esquina superior izquierda dbg_print(8, 8, vsync_text_.c_str(), 0, 255, 255); // Cian para distinguir // Debug: Mostrar valores de la primera pelota (si existe) if (!balls_.empty()) { // Línea 1: Gravedad (solo números enteros) int grav_int = static_cast(balls_[0]->getGravityForce()); std::string grav_text = "GRAV " + std::to_string(grav_int); dbg_print(8, 24, grav_text.c_str(), 255, 0, 255); // Magenta para debug // Línea 2: Velocidad Y (solo números enteros) int vy_int = static_cast(balls_[0]->getVelocityY()); std::string vy_text = "VY " + std::to_string(vy_int); dbg_print(8, 32, vy_text.c_str(), 255, 0, 255); // Magenta para debug // Línea 3: Estado superficie std::string surface_text = balls_[0]->isOnSurface() ? "SURFACE YES" : "SURFACE NO"; dbg_print(8, 40, surface_text.c_str(), 255, 0, 255); // Magenta para debug // Línea 4: Coeficiente de rebote (loss) float loss_val = balls_[0]->getLossCoefficient(); std::string loss_text = "LOSS " + std::to_string(loss_val).substr(0, 4); // Solo 2 decimales dbg_print(8, 48, loss_text.c_str(), 255, 0, 255); // Magenta para debug // Línea 5: Dirección de gravedad std::string gravity_dir_text = "GRAVITY " + gravityDirectionToString(current_gravity_); dbg_print(8, 56, gravity_dir_text.c_str(), 255, 255, 0); // Amarillo para dirección } // Debug: Mostrar tema actual std::string theme_names[] = {"SUNSET", "OCEAN", "NEON", "FOREST", "RGB"}; std::string theme_text = "THEME " + theme_names[static_cast(current_theme_)]; dbg_print(8, 64, theme_text.c_str(), 255, 255, 128); // Amarillo claro para tema // Debug: Mostrar modo de simulación actual std::string mode_text; if (current_mode_ == SimulationMode::PHYSICS) { mode_text = "MODE PHYSICS"; } else if (active_shape_) { mode_text = std::string("MODE ") + active_shape_->getName(); } else { mode_text = "MODE SHAPE"; } dbg_print(8, 72, mode_text.c_str(), 0, 255, 128); // Verde claro para modo } 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 < test_.at(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 ThemeColors &theme = themes_[static_cast(current_theme_)]; int color_index = rand() % theme.ball_colors.size(); // Cantidad variable de colores por tema const Color COLOR = theme.ball_colors[color_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_gravity_, mass_factor)); } setText(); // Actualiza el texto } void Engine::setText() { int num_balls = test_.at(scenario_); if (num_balls == 1) { text_ = "1 PELOTA"; } else { text_ = std::to_string(num_balls) + " PELOTAS"; } text_pos_ = (current_screen_width_ - static_cast(text_.length() * 8)) / 2; // Centrar texto show_text_ = true; text_init_time_ = SDL_GetTicks(); } 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() { 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_ ? "VSYNC ON" : "VSYNC 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_); } 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); // Reinicar la escena con nueva resolución initBalls(scenario_); } SDL_free(displays); } } else { // Volver a resolución original current_screen_width_ = SCREEN_WIDTH; current_screen_height_ = SCREEN_HEIGHT; // Restaurar ventana normal SDL_SetWindowFullscreen(window_, false); SDL_SetWindowSize(window_, SCREEN_WIDTH * WINDOW_ZOOM, SCREEN_HEIGHT * WINDOW_ZOOM); // Restaurar presentación lógica original SDL_SetRenderLogicalPresentation(renderer_, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); // Reinicar la escena con resolución original initBalls(scenario_); } } 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::renderGradientBackground() { // Crear quad de pantalla completa con degradado SDL_Vertex bg_vertices[4]; // Obtener colores del tema actual ThemeColors &theme = themes_[static_cast(current_theme_)]; float top_r = theme.bg_top_r; float top_g = theme.bg_top_g; float top_b = theme.bg_top_b; float bottom_r = theme.bg_bottom_r; float bottom_g = theme.bg_bottom_g; float bottom_b = theme.bg_bottom_b; // 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); } 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 / SCREEN_WIDTH, (dm->h - WINDOW_DECORATION_HEIGHT) / 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 + (SCREEN_WIDTH * current_window_zoom_) / 2; int current_center_y = current_y + (SCREEN_HEIGHT * current_window_zoom_) / 2; // Calcular nuevo tamaño int new_width = SCREEN_WIDTH * new_zoom; int new_height = 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; } void Engine::zoomIn() { setWindowZoom(current_window_zoom_ + 1); } void Engine::zoomOut() { setWindowZoom(current_window_zoom_ - 1); } void Engine::initializeThemes() { // SUNSET: Naranjas, rojos, amarillos, rosas (8 colores) themes_[0] = { 180.0f / 255.0f, 140.0f / 255.0f, 100.0f / 255.0f, // Fondo superior (naranja suave) 40.0f / 255.0f, 20.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior (púrpura oscuro) {{255, 140, 0}, {255, 69, 0}, {255, 215, 0}, {255, 20, 147}, {255, 99, 71}, {255, 165, 0}, {255, 192, 203}, {220, 20, 60}} }; // OCEAN: Azules, turquesas, blancos (8 colores) themes_[1] = { 100.0f / 255.0f, 150.0f / 255.0f, 200.0f / 255.0f, // Fondo superior (azul cielo) 20.0f / 255.0f, 40.0f / 255.0f, 80.0f / 255.0f, // Fondo inferior (azul marino) {{0, 191, 255}, {0, 255, 255}, {32, 178, 170}, {176, 224, 230}, {70, 130, 180}, {0, 206, 209}, {240, 248, 255}, {64, 224, 208}} }; // NEON: Cian, magenta, verde lima, amarillo vibrante (8 colores) themes_[2] = { 20.0f / 255.0f, 20.0f / 255.0f, 40.0f / 255.0f, // Fondo superior (negro azulado) 0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior (negro) {{0, 255, 255}, {255, 0, 255}, {50, 205, 50}, {255, 255, 0}, {255, 20, 147}, {0, 255, 127}, {138, 43, 226}, {255, 69, 0}} }; // FOREST: Verdes, marrones, amarillos otoño (8 colores) themes_[3] = { 144.0f / 255.0f, 238.0f / 255.0f, 144.0f / 255.0f, // Fondo superior (verde claro) 101.0f / 255.0f, 67.0f / 255.0f, 33.0f / 255.0f, // Fondo inferior (marrón tierra) {{34, 139, 34}, {107, 142, 35}, {154, 205, 50}, {255, 215, 0}, {210, 180, 140}, {160, 82, 45}, {218, 165, 32}, {50, 205, 50}} }; // RGB: Círculo cromático con 24 puntos (cada 15°) - Ultra precisión matemática themes_[4] = { 1.0f, 1.0f, 1.0f, // Fondo superior (blanco puro) 1.0f, 1.0f, 1.0f, // Fondo inferior (blanco puro) - sin degradado { {255, 0, 0}, // 0° - Rojo puro {255, 64, 0}, // 15° - Rojo-Naranja {255, 128, 0}, // 30° - Naranja {255, 191, 0}, // 45° - Naranja-Amarillo {255, 255, 0}, // 60° - Amarillo puro {191, 255, 0}, // 75° - Amarillo-Verde claro {128, 255, 0}, // 90° - Verde-Amarillo {64, 255, 0}, // 105° - Verde claro-Amarillo {0, 255, 0}, // 120° - Verde puro {0, 255, 64}, // 135° - Verde-Cian claro {0, 255, 128}, // 150° - Verde-Cian {0, 255, 191}, // 165° - Verde claro-Cian {0, 255, 255}, // 180° - Cian puro {0, 191, 255}, // 195° - Cian-Azul claro {0, 128, 255}, // 210° - Azul-Cian {0, 64, 255}, // 225° - Azul claro-Cian {0, 0, 255}, // 240° - Azul puro {64, 0, 255}, // 255° - Azul-Magenta claro {128, 0, 255}, // 270° - Azul-Magenta {191, 0, 255}, // 285° - Azul claro-Magenta {255, 0, 255}, // 300° - Magenta puro {255, 0, 191}, // 315° - Magenta-Rojo claro {255, 0, 128}, // 330° - Magenta-Rojo {255, 0, 64} // 345° - Magenta claro-Rojo } }; } void Engine::checkAutoRestart() { // Verificar si TODAS las pelotas están paradas bool all_stopped = true; for (const auto &ball : balls_) { if (!ball->isStopped()) { all_stopped = false; break; } } if (all_stopped) { if (!all_balls_were_stopped_) { // Primera vez que se detecta que todas están paradas all_balls_stopped_start_time_ = SDL_GetTicks(); all_balls_were_stopped_ = true; } else { // Ya estaban paradas, verificar tiempo transcurrido Uint64 current_time = SDL_GetTicks(); if (current_time - all_balls_stopped_start_time_ >= AUTO_RESTART_DELAY) { performRandomRestart(); } } } else { // Al menos una pelota se está moviendo - resetear temporizador all_balls_were_stopped_ = false; all_balls_stopped_start_time_ = 0; } } void Engine::performRandomRestart() { // Escenario aleatorio usando tamaño del array scenario_ = rand() % test_.size(); // Tema aleatorio usando tamaño del array de temas current_theme_ = static_cast(rand() % (sizeof(themes_) / sizeof(themes_[0]))); // Reinicializar pelotas con nuevo escenario y tema initBalls(scenario_); // Resetear temporizador all_balls_were_stopped_ = false; all_balls_stopped_start_time_ = 0; } // 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_); } else { // Volver a modo física normal current_mode_ = SimulationMode::PHYSICS; // Desactivar atracción y resetear escala de profundidad for (auto& ball : balls_) { ball->enableRotoBallAttraction(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 text_ = "MODO FISICA"; int text_width = static_cast(text_.length() * 8); text_pos_ = (current_screen_width_ - text_width) / 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::WAVE_GRID: active_shape_ = std::make_unique(); break; case ShapeType::CYLINDER: active_shape_ = std::make_unique(); break; case ShapeType::ICOSAHEDRON: active_shape_ = std::make_unique(); break; case ShapeType::ATOM: active_shape_ = std::make_unique(); break; default: active_shape_ = std::make_unique(); // Fallback break; } // Generar puntos de la figura generateShape(); // Activar atracción física en todas las pelotas for (auto& ball : balls_) { ball->enableRotoBallAttraction(true); } // Mostrar texto informativo con nombre de figura if (active_shape_) { text_ = std::string("MODO ") + active_shape_->getName(); int text_width = static_cast(text_.length() * 8); text_pos_ = (current_screen_width_ - text_width) / 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; // Aplicar fuerza de atracción física hacia el punto rotado // Pasar el tamaño de la figura para escalar fuerzas float shape_size = scale_factor * 80.0f; // 80px = radio base balls_[i]->applyRotoBallForce(target_x, target_y, shape_size, delta_time_); // 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); } } // 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_)); }