Refactor fase 2: Extraer SceneManager de Engine

Migra toda la lógica de gestión de bolas y física a SceneManager
siguiendo el principio de Single Responsibility (SRP).

## Archivos Nuevos

**source/scene/scene_manager.h:**
- Declaración de clase SceneManager
- Gestión de bolas (creación, destrucción, actualización)
- Control de gravedad direccional y estado
- Métodos de acceso: getBalls(), getBallsMutable(), getFirstBall()
- Constructor: SceneManager(screen_width, screen_height)

**source/scene/scene_manager.cpp:**
- Implementación de lógica de escena (~200 líneas)
- changeScenario(): Crea N bolas según escenario
- pushBallsAwayFromGravity(): Impulso direccional
- switchBallsGravity(), forceBallsGravityOn/Off()
- changeGravityDirection(): Cambio de dirección física
- updateBallTexture(): Actualiza textura y tamaño
- updateScreenSize(): Ajusta resolución de pantalla
- updateBallSizes(): Reescala pelotas desde centro

## Archivos Modificados

**source/engine.h:**
- Agregado: #include "scene/scene_manager.h"
- Agregado: std::unique_ptr<SceneManager> scene_manager_
- Removido: std::vector<std::unique_ptr<Ball>> balls_
- Removido: GravityDirection current_gravity_
- Removido: int scenario_
- Removidos métodos privados: initBalls(), switchBallsGravity(),
  enableBallsGravityIfDisabled(), forceBallsGravityOn/Off(),
  changeGravityDirection(), updateBallSizes()

**source/engine.cpp:**
- initialize(): Crea scene_manager_ con resolución
- update(): Delega a scene_manager_->update()
- render(): Usa scene_manager_->getBalls()
- changeScenario(): Delega a scene_manager_
- pushBallsAwayFromGravity(): Delega a scene_manager_
- handleGravityToggle(): Usa scene_manager_->switchBallsGravity()
- handleGravityDirectionChange(): Delega dirección
- switchTextureInternal(): Usa updateBallTexture()
- toggleShapeModeInternal(): Usa getBallsMutable()
- activateShapeInternal(): Usa forceBallsGravityOff()
- updateShape(): Usa getBallsMutable() para asignar targets
- Debug HUD: Usa getFirstBall() para info
- toggleRealFullscreen(): Usa updateScreenSize() + changeScenario()
- performDemoAction(): Delega gravedad y escenarios
- randomizeOnDemoStart(): Delega changeScenario()
- toggleGravityOnOff(): Usa forceBallsGravity*()
- enterLogoMode(): Usa getBallCount() y changeScenario()
- exitLogoMode(): Usa updateBallTexture()
- Removidos ~150 líneas de implementación movidas a SceneManager

**CMakeLists.txt:**
- Agregado source/scene/*.cpp a file(GLOB SOURCE_FILES ...)

## Resultado

- Engine.cpp reducido de 2341 → ~2150 líneas (-191 líneas)
- SceneManager: 202 líneas de lógica de física/escena
- Separación clara: Engine coordina, SceneManager ejecuta física
- 100% funcional: Compila sin errores ni warnings
- Preparado para Fase 3 (UIManager)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-10 11:59:32 +02:00
parent b8d3c60e58
commit f93879b803
5 changed files with 460 additions and 253 deletions

View File

@@ -216,15 +216,17 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
// Inicializar InputHandler (sin estado)
input_handler_ = std::make_unique<InputHandler>();
// Inicializar ThemeManager PRIMERO (requerido por Notifier)
// Inicializar ThemeManager PRIMERO (requerido por Notifier y SceneManager)
theme_manager_ = std::make_unique<ThemeManager>();
theme_manager_->initialize();
// Inicializar SceneManager (gestión de bolas y física)
scene_manager_ = std::make_unique<SceneManager>(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<size_t> 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<int>(balls_[0]->getGravityForce());
int grav_int = static_cast<int>(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<int>(balls_[0]->getVelocityY());
int vy_int = static_cast<int>(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<int>(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<Ball>(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<float>(balls_on_surface) / static_cast<float>(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<int>(balls_.size()) >= LOGO_MODE_MIN_BALLS &&
if (static_cast<int>(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<int>(balls_.size()) >= LOGO_MODE_MIN_BALLS) {
if (static_cast<int>(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<GravityDirection>(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<GravityDirection>(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<GravityDirection>(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<GravityDirection>(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<int>(balls_.size()) < LOGO_MODE_MIN_BALLS) {
if (static_cast<int>(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<float>(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<int>(balls_.size());
int num_points = static_cast<int>(scene_manager_->getBallCount());
active_shape_->generatePoints(num_points, static_cast<float>(current_screen_width_), static_cast<float>(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<int>(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<float>(balls_near) / balls_.size();
shape_convergence_ = static_cast<float>(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