diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d03f8a..eb8f867 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ if (NOT SDL3_ttf_FOUND) endif() # Archivos fuente (excluir main_old.cpp) -file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/input/*.cpp source/shapes/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp) +file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/input/*.cpp source/scene/*.cpp source/shapes/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp) list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp") # Comprobar si se encontraron archivos fuente diff --git a/source/engine.cpp b/source/engine.cpp index 47d2898..a88428b 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -216,15 +216,17 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) { // Inicializar InputHandler (sin estado) input_handler_ = std::make_unique(); - // Inicializar ThemeManager PRIMERO (requerido por Notifier) + // Inicializar ThemeManager PRIMERO (requerido por Notifier y SceneManager) theme_manager_ = std::make_unique(); theme_manager_->initialize(); + // Inicializar SceneManager (gestión de bolas y física) + scene_manager_ = std::make_unique(current_screen_width_, current_screen_height_); + scene_manager_->initialize(0, texture_, theme_manager_.get()); // Escenario 0 (10 bolas) por defecto + // 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; @@ -295,10 +297,8 @@ void Engine::update() { // 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 - } + // Modo física normal: actualizar física de cada pelota (delegado a SceneManager) + scene_manager_->update(delta_time_); } else if (current_mode_ == SimulationMode::SHAPE) { // Modo Figura 3D: actualizar figura polimórfica updateShape(); @@ -328,9 +328,10 @@ void Engine::handleGravityToggle() { toggleShapeModeInternal(false); // Desactivar figura sin forzar gravedad ON showNotificationForAction("Gravedad Off"); } else { - switchBallsGravity(); // Toggle normal en modo física + scene_manager_->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); + const Ball* first_ball = scene_manager_->getFirstBall(); + bool gravity_on = (first_ball == nullptr) ? true : (first_ball->getGravityForce() != 0.0f); showNotificationForAction(gravity_on ? "Gravedad On" : "Gravedad Off"); } } @@ -340,9 +341,9 @@ void Engine::handleGravityDirectionChange(GravityDirection direction, const char if (current_mode_ == SimulationMode::SHAPE) { toggleShapeModeInternal(); // Desactivar figura (activa gravedad automáticamente) } else { - enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF + scene_manager_->enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF } - changeGravityDirection(direction); + scene_manager_->changeGravityDirection(direction); showNotificationForAction(notification_text); } @@ -444,8 +445,13 @@ void Engine::switchTexture() { // Escenarios (número de pelotas) void Engine::changeScenario(int scenario_id, const char* notification_text) { - scenario_ = scenario_id; - initBalls(scenario_); + // Resetear modo SHAPE si está activo + if (current_mode_ == SimulationMode::SHAPE) { + current_mode_ = SimulationMode::PHYSICS; + active_shape_.reset(); + } + + scene_manager_->changeScenario(scenario_id); showNotificationForAction(notification_text); } @@ -545,28 +551,31 @@ void Engine::render() { batch_vertices_.clear(); batch_indices_.clear(); + // Obtener referencia a las bolas desde SceneManager + const auto& balls = scene_manager_->getBalls(); + if (current_mode_ == SimulationMode::SHAPE) { // MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm) // Las pelotas con menor depth_brightness (más lejos/oscuras) se renderizan primero // 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.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(); + std::sort(render_order.begin(), render_order.end(), [&balls](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(); + 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(); + 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; @@ -580,8 +589,9 @@ void Engine::render() { } } else { // MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad) + const auto& balls = scene_manager_->getBalls(); size_t idx = 0; - for (auto& ball : balls_) { + 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); @@ -641,32 +651,33 @@ void Engine::render() { current_y += line_height; // Debug: Mostrar valores de la primera pelota (si existe) - if (!balls_.empty()) { + const Ball* first_ball = scene_manager_->getFirstBall(); + if (first_ball != nullptr) { // Línea 1: Gravedad - int grav_int = static_cast(balls_[0]->getGravityForce()); + int grav_int = static_cast(first_ball->getGravityForce()); std::string grav_text = "Gravedad: " + std::to_string(grav_int); text_renderer_debug_.printAbsolute(margin, current_y, grav_text.c_str(), {255, 0, 255, 255}); // Magenta current_y += line_height; // Línea 2: Velocidad Y - int vy_int = static_cast(balls_[0]->getVelocityY()); + int vy_int = static_cast(first_ball->getVelocityY()); std::string vy_text = "Velocidad Y: " + std::to_string(vy_int); text_renderer_debug_.printAbsolute(margin, current_y, vy_text.c_str(), {255, 0, 255, 255}); // Magenta current_y += line_height; // Línea 3: Estado superficie - std::string surface_text = balls_[0]->isOnSurface() ? "Superficie: Sí" : "Superficie: No"; + std::string surface_text = first_ball->isOnSurface() ? "Superficie: Sí" : "Superficie: No"; text_renderer_debug_.printAbsolute(margin, current_y, surface_text.c_str(), {255, 0, 255, 255}); // Magenta current_y += line_height; // Línea 4: Coeficiente de rebote (loss) - float loss_val = balls_[0]->getLossCoefficient(); + float loss_val = first_ball->getLossCoefficient(); std::string loss_text = "Rebote: " + std::to_string(loss_val).substr(0, 4); text_renderer_debug_.printAbsolute(margin, current_y, loss_text.c_str(), {255, 0, 255, 255}); // Magenta current_y += line_height; // Línea 5: Dirección de gravedad - std::string gravity_dir_text = "Dirección: " + gravityDirectionToString(current_gravity_); + std::string gravity_dir_text = "Dirección: " + gravityDirectionToString(scene_manager_->getCurrentGravity()); text_renderer_debug_.printAbsolute(margin, current_y, gravity_dir_text.c_str(), {255, 255, 0, 255}); // Amarillo current_y += line_height; } @@ -723,46 +734,12 @@ void Engine::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_]; + int num_balls = BALL_COUNT_SCENARIOS[scene_manager_->getCurrentScenario()]; std::string notification_text; if (num_balls == 1) { notification_text = "1 Pelota"; @@ -796,79 +773,7 @@ void Engine::showNotificationForAction(const std::string& text) { } 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 - } + scene_manager_->pushBallsAwayFromGravity(); } void Engine::toggleVSync() { @@ -923,7 +828,8 @@ void Engine::toggleRealFullscreen() { updatePhysicalWindowSize(); // Reinicar la escena con nueva resolución - initBalls(scenario_); + scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_); + scene_manager_->changeScenario(scene_manager_->getCurrentScenario()); } SDL_free(displays); } @@ -944,7 +850,8 @@ void Engine::toggleRealFullscreen() { updatePhysicalWindowSize(); // Reinicar la escena con resolución original - initBalls(scenario_); + scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_); + scene_manager_->changeScenario(scene_manager_->getCurrentScenario()); } } @@ -1383,10 +1290,10 @@ void Engine::updateDemoMode() { logo_current_flip_count_ = 0; } else if (action < 80) { // 20%: Forzar gravedad ON (empezar a caer mientras da vueltas) - forceBallsGravityOn(); + scene_manager_->forceBallsGravityOn(); } else { // 20%: Forzar gravedad OFF (flotar mientras da vueltas) - forceBallsGravityOff(); + scene_manager_->forceBallsGravityOff(); } // Resetear timer con intervalos escalados @@ -1426,7 +1333,7 @@ void Engine::performDemoAction(bool is_lite) { if (is_lite) { // DEMO LITE: Verificar condiciones para salto a Logo Mode - if (static_cast(balls_.size()) >= LOGO_MODE_MIN_BALLS && + if (static_cast(scene_manager_->getBallCount()) >= 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) { @@ -1436,7 +1343,7 @@ void Engine::performDemoAction(bool is_lite) { } } else { // DEMO COMPLETO: Verificar condiciones para salto a Logo Mode - if (static_cast(balls_.size()) >= LOGO_MODE_MIN_BALLS) { + if (static_cast(scene_manager_->getBallCount()) >= LOGO_MODE_MIN_BALLS) { // 15% probabilidad de saltar a Logo Mode if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) { enterLogoMode(true); // Entrar desde DEMO @@ -1462,7 +1369,7 @@ void Engine::performDemoAction(bool is_lite) { accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR; if (random_value < accumulated_weight) { GravityDirection new_direction = static_cast(rand() % 4); - changeGravityDirection(new_direction); + scene_manager_->changeGravityDirection(new_direction); return; } @@ -1505,7 +1412,7 @@ void Engine::performDemoAction(bool is_lite) { accumulated_weight += DEMO_WEIGHT_GRAVITY_DIR; if (random_value < accumulated_weight) { GravityDirection new_direction = static_cast(rand() % 4); - changeGravityDirection(new_direction); + scene_manager_->changeGravityDirection(new_direction); return; } @@ -1555,8 +1462,8 @@ void Engine::performDemoAction(bool is_lite) { 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_); + int new_scenario = valid_scenarios[rand() % 5]; + scene_manager_->changeScenario(new_scenario); return; } @@ -1630,7 +1537,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) { // Randomizar gravedad: dirección + ON/OFF GravityDirection new_direction = static_cast(rand() % 4); - changeGravityDirection(new_direction); + scene_manager_->changeGravityDirection(new_direction); if (rand() % 2 == 0) { toggleGravityOnOff(); // 50% probabilidad de desactivar gravedad } @@ -1640,8 +1547,8 @@ void Engine::randomizeOnDemoStart(bool is_lite) { // 1. Escenario (excluir índices 0, 6, 7) int valid_scenarios[] = {1, 2, 3, 4, 5}; - scenario_ = valid_scenarios[rand() % 5]; - initBalls(scenario_); + int new_scenario = valid_scenarios[rand() % 5]; + scene_manager_->changeScenario(new_scenario); // 2. Tema (elegir entre TODOS los 15 temas) int random_theme_index = rand() % 15; @@ -1676,7 +1583,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) { // 7. Gravedad: dirección + ON/OFF GravityDirection new_direction = static_cast(rand() % 4); - changeGravityDirection(new_direction); + scene_manager_->changeGravityDirection(new_direction); if (rand() % 3 == 0) { // 33% probabilidad de desactivar gravedad toggleGravityOnOff(); } @@ -1686,14 +1593,14 @@ void Engine::randomizeOnDemoStart(bool is_lite) { // 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); + bool first_ball_gravity_enabled = (!scene_manager_->hasBalls() || scene_manager_->getFirstBall()->getGravityForce() > 0.0f); if (first_ball_gravity_enabled) { // Desactivar gravedad - forceBallsGravityOff(); + scene_manager_->forceBallsGravityOff(); } else { // Activar gravedad - forceBallsGravityOn(); + scene_manager_->forceBallsGravityOn(); } } @@ -1704,10 +1611,9 @@ void Engine::toggleGravityOnOff() { // 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) { + if (static_cast(scene_manager_->getBallCount()) < LOGO_MODE_MIN_BALLS) { // Ajustar a 5000 pelotas automáticamente - scenario_ = 5; // Escenario 5000 pelotas (índice 5 en BALL_COUNT_SCENARIOS) - initBalls(scenario_); + scene_manager_->changeScenario(5); // Escenario 5000 pelotas (índice 5 en BALL_COUNT_SCENARIOS) } // Guardar estado previo (para restaurar al salir) @@ -1727,15 +1633,10 @@ void Engine::enterLogoMode(bool from_demo) { // Aplicar configuración fija del Modo Logo if (small_index != current_texture_index_) { current_texture_index_ = small_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_); - } + int new_size = texture_->getWidth(); + current_ball_size_ = new_size; + scene_manager_->updateBallTexture(texture_, new_size); } // Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA @@ -1782,15 +1683,10 @@ void Engine::exitLogoMode(bool return_to_demo) { 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_); - } + int new_size = texture_->getWidth(); + current_ball_size_ = new_size; + scene_manager_->updateBallTexture(texture_, new_size); } shape_scale_factor_ = logo_previous_shape_scale_; @@ -1827,55 +1723,9 @@ void Engine::exitLogoMode(bool return_to_demo) { // Toggle manual del Modo Logo (tecla K) // 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::switchTextureInternal(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_]; @@ -1884,13 +1734,8 @@ void Engine::switchTextureInternal(bool show_notification) { 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); + // Actualizar texturas y tamaños de todas las pelotas (delegado a SceneManager) + scene_manager_->updateBallTexture(texture_, new_size); // Mostrar notificación con el nombre de la textura (solo si se solicita) if (show_notification) { @@ -1927,14 +1772,15 @@ void Engine::toggleShapeModeInternal(bool force_gravity_on_exit) { current_mode_ = SimulationMode::PHYSICS; // Desactivar atracción y resetear escala de profundidad - for (auto& ball : balls_) { + auto& balls = scene_manager_->getBallsMutable(); + for (auto& ball : balls) { ball->enableShapeAttraction(false); ball->setDepthScale(1.0f); // Reset escala a 100% (evita "pop" visual) } // Activar gravedad al salir (solo si se especifica) if (force_gravity_on_exit) { - forceBallsGravityOn(); + scene_manager_->forceBallsGravityOn(); } // Mostrar texto informativo (solo si NO estamos en modo demo o logo) @@ -1957,7 +1803,7 @@ void Engine::activateShapeInternal(ShapeType type) { current_mode_ = SimulationMode::SHAPE; // Desactivar gravedad al entrar en modo figura - forceBallsGravityOff(); + scene_manager_->forceBallsGravityOff(); // Crear instancia polimórfica de la figura correspondiente switch (type) { @@ -1997,7 +1843,8 @@ void Engine::activateShapeInternal(ShapeType type) { generateShape(); // Activar atracción física en todas las pelotas - for (auto& ball : balls_) { + auto& balls = scene_manager_->getBallsMutable(); + for (auto& ball : balls) { ball->enableShapeAttraction(true); } @@ -2014,7 +1861,7 @@ void Engine::activateShapeInternal(ShapeType type) { void Engine::generateShape() { if (!active_shape_) return; - int num_points = static_cast(balls_.size()); + int num_points = static_cast(scene_manager_->getBallCount()); active_shape_->generatePoints(num_points, static_cast(current_screen_width_), static_cast(current_screen_height_)); } @@ -2032,8 +1879,11 @@ void Engine::updateShape() { float center_x = current_screen_width_ / 2.0f; float center_y = current_screen_height_ / 2.0f; + // Obtener referencia mutable a las bolas desde SceneManager + auto& balls = scene_manager_->getBallsMutable(); + // Actualizar cada pelota con física de atracción - for (size_t i = 0; i < balls_.size(); i++) { + 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); @@ -2048,23 +1898,23 @@ void Engine::updateShape() { float target_y = center_y + y_3d; // Actualizar target de la pelota para cálculo de convergencia - balls_[i]->setShapeTarget2D(target_x, target_y); + 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); + 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); + 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); + balls[i]->setDepthScale(depth_scale); } // Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo) @@ -2072,13 +1922,13 @@ void Engine::updateShape() { int balls_near = 0; float distance_threshold = LOGO_CONVERGENCE_DISTANCE; // 20px fijo (más permisivo) - for (const auto& ball : balls_) { + for (const auto& ball : balls) { if (ball->getDistanceToTarget() < distance_threshold) { balls_near++; } } - shape_convergence_ = static_cast(balls_near) / balls_.size(); + shape_convergence_ = static_cast(balls_near) / scene_manager_->getBallCount(); // Notificar a la figura sobre el porcentaje de convergencia // Esto permite que PNGShape decida cuándo empezar a contar para flips diff --git a/source/engine.h b/source/engine.h index be57470..655ab93 100644 --- a/source/engine.h +++ b/source/engine.h @@ -11,13 +11,14 @@ #include // for vector #include "ball.h" // for Ball -#include "defines.h" // for GravityDirection, ColorTheme, ShapeType -#include "external/texture.h" // for Texture +#include "defines.h" // for GravityDirection, ColorTheme, ShapeType +#include "external/texture.h" // for Texture #include "input/input_handler.h" // for InputHandler -#include "shapes/shape.h" // for Shape (interfaz polimórfica) -#include "text/textrenderer.h" // for TextRenderer -#include "theme_manager.h" // for ThemeManager -#include "ui/notifier.h" // for Notifier +#include "scene/scene_manager.h" // for SceneManager +#include "shapes/shape.h" // for Shape (interfaz polimórfica) +#include "text/textrenderer.h" // for TextRenderer +#include "theme_manager.h" // for ThemeManager +#include "ui/notifier.h" // for Notifier // Modos de aplicación mutuamente excluyentes enum class AppMode { @@ -79,6 +80,7 @@ class Engine { private: // === Componentes del sistema (Composición) === std::unique_ptr input_handler_; // Manejo de entradas SDL + std::unique_ptr scene_manager_; // Gestión de bolas y física // Recursos SDL SDL_Window* window_ = nullptr; @@ -90,9 +92,6 @@ class Engine { int current_ball_size_ = 10; // Tamaño actual de pelotas (dinámico, se actualiza desde texture) // Estado del simulador - std::vector> balls_; - GravityDirection current_gravity_ = GravityDirection::DOWN; - int scenario_ = 0; bool should_exit_ = false; // Sistema de timing @@ -189,14 +188,8 @@ class Engine { void render(); // Métodos auxiliares privados (llamados por la interfaz pública) - void initBalls(int value); void setText(); // DEPRECATED - usar showNotificationForAction() en su lugar void showNotificationForAction(const std::string& text); // Mostrar notificación solo en modo MANUAL - void switchBallsGravity(); - void enableBallsGravityIfDisabled(); - void forceBallsGravityOn(); - void forceBallsGravityOff(); - void changeGravityDirection(GravityDirection direction); std::string gravityDirectionToString(GravityDirection direction) const; // Sistema de gestión de estados (MANUAL/DEMO/DEMO_LITE/LOGO) @@ -214,7 +207,6 @@ class Engine { // Sistema de cambio de sprites dinámico - Métodos privados void switchTextureInternal(bool show_notification); // Implementación interna del cambio de textura - void updateBallSizes(int old_size, int new_size); // Ajusta posiciones al cambiar tamaño // Sistema de zoom dinámico - Métodos privados int calculateMaxWindowZoom() const; diff --git a/source/scene/scene_manager.cpp b/source/scene/scene_manager.cpp new file mode 100644 index 0000000..cd5d444 --- /dev/null +++ b/source/scene/scene_manager.cpp @@ -0,0 +1,199 @@ +#include "scene_manager.h" + +#include // for rand + +#include "../defines.h" // for BALL_COUNT_SCENARIOS, GRAVITY_MASS_MIN, etc +#include "../external/texture.h" // for Texture +#include "../theme_manager.h" // for ThemeManager + +SceneManager::SceneManager(int screen_width, int screen_height) + : current_gravity_(GravityDirection::DOWN) + , scenario_(0) + , screen_width_(screen_width) + , screen_height_(screen_height) + , current_ball_size_(10) + , texture_(nullptr) + , theme_manager_(nullptr) { +} + +void SceneManager::initialize(int scenario, std::shared_ptr texture, ThemeManager* theme_manager) { + scenario_ = scenario; + texture_ = texture; + theme_manager_ = theme_manager; + current_ball_size_ = texture_->getWidth(); + + // Crear bolas iniciales + changeScenario(scenario_); +} + +void SceneManager::update(float delta_time) { + // Actualizar física de todas las bolas + for (auto& ball : balls_) { + ball->update(delta_time); + } +} + +void SceneManager::changeScenario(int scenario_id) { + // Guardar escenario + scenario_ = scenario_id; + + // 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[scenario_id]; ++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(screen_width_ * BALL_SPAWN_MARGIN); + const int spawn_zone_width = 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_, + screen_width_, screen_height_, current_ball_size_, + current_gravity_, mass_factor + )); + } +} + +void SceneManager::updateBallTexture(std::shared_ptr new_texture, int new_ball_size) { + if (balls_.empty()) return; + + // Guardar tamaño antiguo + int old_size = current_ball_size_; + + // Actualizar textura y tamaño + texture_ = new_texture; + current_ball_size_ = new_ball_size; + + // Actualizar texturas de todas las pelotas + for (auto& ball : balls_) { + ball->setTexture(texture_); + } + + // Ajustar posiciones según el cambio de tamaño + updateBallSizes(old_size, new_ball_size); +} + +void SceneManager::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 SceneManager::switchBallsGravity() { + for (auto& ball : balls_) { + ball->switchGravity(); + } +} + +void SceneManager::enableBallsGravityIfDisabled() { + for (auto& ball : balls_) { + ball->enableGravityIfDisabled(); + } +} + +void SceneManager::forceBallsGravityOn() { + for (auto& ball : balls_) { + ball->forceGravityOn(); + } +} + +void SceneManager::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 SceneManager::changeGravityDirection(GravityDirection direction) { + current_gravity_ = direction; + for (auto& ball : balls_) { + ball->setGravityDirection(direction); + ball->applyRandomLateralPush(); // Aplicar empuje lateral aleatorio + } +} + +void SceneManager::updateScreenSize(int width, int height) { + screen_width_ = width; + screen_height_ = height; + + // NOTA: No actualizamos las bolas existentes, solo afecta a futuras creaciones + // Si se requiere reposicionar bolas existentes, implementar aquí +} + +// === Métodos privados === + +void SceneManager::updateBallSizes(int old_size, int new_size) { + for (auto& ball : balls_) { + SDL_FRect pos = ball->getPosition(); + + // Ajustar posición para compensar cambio de tamaño + // Si aumenta tamaño, mover hacia centro; si disminuye, alejar del centro + float center_x = screen_width_ / 2.0f; + float center_y = screen_height_ / 2.0f; + + float dx = pos.x - center_x; + float dy = pos.y - center_y; + + // Ajustar proporcionalmente (evitar divisiones por cero) + if (old_size > 0) { + float scale_factor = static_cast(new_size) / static_cast(old_size); + pos.x = center_x + dx * scale_factor; + pos.y = center_y + dy * scale_factor; + } + + // Actualizar tamaño del hitbox + ball->updateSize(new_size); + } +} diff --git a/source/scene/scene_manager.h b/source/scene/scene_manager.h new file mode 100644 index 0000000..95969f9 --- /dev/null +++ b/source/scene/scene_manager.h @@ -0,0 +1,166 @@ +#pragma once + +#include // for unique_ptr, shared_ptr +#include // for vector + +#include "../ball.h" // for Ball +#include "../defines.h" // for GravityDirection + +// Forward declarations +class Texture; +class ThemeManager; + +/** + * @class SceneManager + * @brief Gestiona toda la lógica de creación, física y actualización de bolas + * + * Responsabilidad única: Manejo de la escena (bolas, gravedad, física) + * + * Características: + * - Crea y destruye bolas según escenario seleccionado + * - Controla la dirección y estado de la gravedad + * - Actualiza física de todas las bolas cada frame + * - Proporciona acceso controlado a las bolas para rendering + * - Mantiene el Engine desacoplado de la lógica de física + */ +class SceneManager { + public: + /** + * @brief Constructor + * @param screen_width Ancho lógico de la pantalla + * @param screen_height Alto lógico de la pantalla + */ + SceneManager(int screen_width, int screen_height); + + /** + * @brief Inicializa el manager con configuración inicial + * @param scenario Escenario inicial (índice de BALL_COUNT_SCENARIOS) + * @param texture Textura compartida para sprites de bolas + * @param theme_manager Puntero al gestor de temas (para colores) + */ + void initialize(int scenario, std::shared_ptr texture, ThemeManager* theme_manager); + + /** + * @brief Actualiza física de todas las bolas + * @param delta_time Tiempo transcurrido desde último frame (segundos) + */ + void update(float delta_time); + + // === Gestión de bolas === + + /** + * @brief Cambia el número de bolas según escenario + * @param scenario_id Índice del escenario (0-7 para 10 a 50,000 bolas) + */ + void changeScenario(int scenario_id); + + /** + * @brief Actualiza textura y tamaño de todas las bolas + * @param new_texture Nueva textura compartida + * @param new_ball_size Nuevo tamaño de bolas (píxeles) + */ + void updateBallTexture(std::shared_ptr new_texture, int new_ball_size); + + // === Control de gravedad === + + /** + * @brief Aplica impulso a todas las bolas alejándolas de la superficie de gravedad + */ + void pushBallsAwayFromGravity(); + + /** + * @brief Alterna el estado de gravedad (ON/OFF) en todas las bolas + */ + void switchBallsGravity(); + + /** + * @brief Reactiva gravedad solo si estaba desactivada + */ + void enableBallsGravityIfDisabled(); + + /** + * @brief Fuerza gravedad ON en todas las bolas + */ + void forceBallsGravityOn(); + + /** + * @brief Fuerza gravedad OFF en todas las bolas (con impulso si >50% en superficie) + */ + void forceBallsGravityOff(); + + /** + * @brief Cambia la dirección de la gravedad + * @param direction Nueva dirección (UP/DOWN/LEFT/RIGHT) + */ + void changeGravityDirection(GravityDirection direction); + + // === Acceso a datos (read-only) === + + /** + * @brief Obtiene referencia constante al vector de bolas (para rendering) + */ + const std::vector>& getBalls() const { return balls_; } + + /** + * @brief Obtiene referencia mutable al vector de bolas (para ShapeManager) + * NOTA: Usar con cuidado, solo para sistemas que necesitan modificar estado de bolas + */ + std::vector>& getBallsMutable() { return balls_; } + + /** + * @brief Obtiene número total de bolas + */ + size_t getBallCount() const { return balls_.size(); } + + /** + * @brief Verifica si hay al menos una bola + */ + bool hasBalls() const { return !balls_.empty(); } + + /** + * @brief Obtiene puntero a la primera bola (para debug info) + * @return Puntero constante o nullptr si no hay bolas + */ + const Ball* getFirstBall() const { return balls_.empty() ? nullptr : balls_[0].get(); } + + /** + * @brief Obtiene dirección actual de gravedad + */ + GravityDirection getCurrentGravity() const { return current_gravity_; } + + /** + * @brief Obtiene escenario actual + */ + int getCurrentScenario() const { return scenario_; } + + /** + * @brief Actualiza resolución de pantalla (para resize/fullscreen) + * @param width Nuevo ancho lógico + * @param height Nuevo alto lógico + */ + void updateScreenSize(int width, int height); + + private: + // === Datos de escena === + std::vector> balls_; + GravityDirection current_gravity_; + int scenario_; + + // === Configuración de pantalla === + int screen_width_; + int screen_height_; + int current_ball_size_; + + // === Referencias a otros sistemas (no owned) === + std::shared_ptr texture_; + ThemeManager* theme_manager_; + + // === Métodos privados auxiliares === + + /** + * @brief Ajusta posiciones de bolas al cambiar tamaño de sprite + * @param old_size Tamaño anterior + * @param new_size Tamaño nuevo + */ + void updateBallSizes(int old_size, int new_size); +};