Fix: Sistema de convergencia y flip timing en LOGO mode

Refactoring semántico:
- Renombrar rotoball_* → shape_* (variables y métodos)
- Mejora legibilidad: aplica a todas las figuras 3D, no solo esfera

Fixes críticos:
- Fix convergencia: setShapeTarget2D() actualiza targets cada frame
- Fix getDistanceToTarget(): siempre calcula distancia (sin guarda)
- Fix lógica flip: destruir DURANTE flip N (no después de N flips)
- Añadir display CONV en debug HUD (monitoreo convergencia)

Mejoras timing:
- Reducir PNG_IDLE_TIME_LOGO: 3-5s → 2-4s (flips más dinámicos)
- Bajar CONVERGENCE_THRESHOLD: 0.8 → 0.4 (40% permite flips)

Sistema flip-waiting (LOGO mode):
- CAMINO A: Convergencia + tiempo (inmediato)
- CAMINO B: Esperar 1-3 flips y destruir durante flip (20-80% progreso)
- Tracking de flips con getFlipCount() y getFlipProgress()

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-09 11:01:41 +02:00
parent 6cb3c2eef9
commit b93028396a
8 changed files with 238 additions and 52 deletions

View File

@@ -823,6 +823,13 @@ void Engine::render() {
}
dbg_print(8, 72, mode_text.c_str(), 0, 255, 128); // Verde claro para modo
// Debug: Mostrar convergencia en modo LOGO (solo cuando está activo)
if (current_app_mode_ == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
int convergence_percent = static_cast<int>(shape_convergence_ * 100.0f);
std::string convergence_text = "CONV " + std::to_string(convergence_percent);
dbg_print(8, 80, convergence_text.c_str(), 255, 128, 0); // Naranja para convergencia
}
// Debug: Mostrar modo DEMO/LOGO activo (siempre visible cuando debug está ON)
if (current_app_mode_ == AppMode::LOGO) {
int logo_text_width = 9 * 8; // "LOGO MODE" = 9 caracteres × 8 píxeles
@@ -1831,12 +1838,43 @@ void Engine::updateDemoMode() {
bool should_trigger = false;
if (current_app_mode_ == AppMode::LOGO) {
// LOGO MODE: Esperar convergencia + tiempo mínimo (o timeout máximo)
bool min_time_reached = demo_timer_ >= logo_min_time_;
bool max_time_reached = demo_timer_ >= logo_max_time_;
bool convergence_ok = shape_convergence_ >= logo_convergence_threshold_;
// LOGO MODE: Dos caminos posibles
if (logo_waiting_for_flip_) {
// CAMINO B: Esperando a que ocurran flips
// Obtener referencia a PNGShape si está activa
PNGShape* png_shape = nullptr;
if (active_shape_ && current_mode_ == SimulationMode::SHAPE) {
png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
}
should_trigger = (min_time_reached && convergence_ok) || max_time_reached;
if (png_shape) {
int current_flip_count = png_shape->getFlipCount();
// Detectar nuevo flip completado
if (current_flip_count > logo_current_flip_count_) {
logo_current_flip_count_ = current_flip_count;
}
// Si estamos EN o DESPUÉS del flip objetivo
// +1 porque queremos actuar DURANTE el flip N, no después de completarlo
if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
// Monitorear progreso del flip actual
if (png_shape->isFlipping()) {
float flip_progress = png_shape->getFlipProgress();
if (flip_progress >= logo_target_flip_percentage_) {
should_trigger = true; // ¡Trigger durante el flip!
}
}
}
}
} else {
// CAMINO A: Esperar convergencia + tiempo (comportamiento original)
bool min_time_reached = demo_timer_ >= logo_min_time_;
bool max_time_reached = demo_timer_ >= logo_max_time_;
bool convergence_ok = shape_convergence_ >= logo_convergence_threshold_;
should_trigger = (min_time_reached && convergence_ok) || max_time_reached;
}
} else {
// DEMO/DEMO_LITE: Timer simple como antes
should_trigger = demo_timer_ >= demo_next_action_time_;
@@ -1850,19 +1888,71 @@ void Engine::updateDemoMode() {
int action = rand() % 100;
if (current_mode_ == SimulationMode::SHAPE) {
// Logo quieto (formado) → 2 opciones posibles
if (action < 50) {
// 50%: SHAPE → PHYSICS con gravedad ON (caída dramática)
toggleShapeMode(true);
// Logo quieto (formado) → Decidir camino a seguir
// DECISIÓN BIFURCADA: ¿Cambio inmediato o esperar flips?
if (logo_waiting_for_flip_) {
// Ya estábamos esperando flips, y se disparó el trigger
// → Hacer el cambio SHAPE → PHYSICS ahora (durante el flip)
if (action < 50) {
toggleShapeMode(true); // Con gravedad ON
} else {
toggleShapeMode(false); // Con gravedad OFF
}
// Resetear variables de espera de flips
logo_waiting_for_flip_ = false;
logo_current_flip_count_ = 0;
// Resetear timer
demo_timer_ = 0.0f;
float interval_range = logo_max_time_ - logo_min_time_;
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
} else if (rand() % 100 < LOGO_FLIP_WAIT_PROBABILITY) {
// CAMINO B (50%): Esperar a que ocurran 1-3 flips
logo_waiting_for_flip_ = true;
logo_target_flip_number_ = LOGO_FLIP_WAIT_MIN + rand() % (LOGO_FLIP_WAIT_MAX - LOGO_FLIP_WAIT_MIN + 1);
logo_target_flip_percentage_ = LOGO_FLIP_TRIGGER_MIN + (rand() % 1000) / 1000.0f * (LOGO_FLIP_TRIGGER_MAX - LOGO_FLIP_TRIGGER_MIN);
logo_current_flip_count_ = 0;
// Resetear contador de flips en PNGShape
if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape) {
png_shape->resetFlipCount();
}
}
// NO hacer nada más este frame - esperar a que ocurran los flips
// El trigger se ejecutará en futuras iteraciones cuando se cumplan las condiciones
} else {
// 50%: SHAPE → PHYSICS con gravedad OFF (dar vueltas sin caer)
toggleShapeMode(false);
// CAMINO A (50%): Cambio inmediato
if (action < 50) {
// 50%: SHAPE → PHYSICS con gravedad ON (caída dramática)
toggleShapeMode(true);
} else {
// 50%: SHAPE → PHYSICS con gravedad OFF (dar vueltas sin caer)
toggleShapeMode(false);
}
// Resetear variables de espera de flips al cambiar a PHYSICS
logo_waiting_for_flip_ = false;
logo_current_flip_count_ = 0;
// Resetear timer con intervalos escalados
demo_timer_ = 0.0f;
float interval_range = logo_max_time_ - logo_min_time_;
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
}
} else {
// Logo animado (PHYSICS) → 3 opciones posibles
if (action < 60) {
// 60%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones)
toggleShapeMode(false);
// Resetear variables de espera de flips al volver a SHAPE
logo_waiting_for_flip_ = false;
logo_current_flip_count_ = 0;
} else if (action < 80) {
// 20%: Forzar gravedad ON (empezar a caer mientras da vueltas)
forceBallsGravityOn();
@@ -1870,12 +1960,12 @@ void Engine::updateDemoMode() {
// 20%: Forzar gravedad OFF (flotar mientras da vueltas)
forceBallsGravityOff();
}
}
// Resetear timer con intervalos escalados (logo_min_time_ y logo_max_time_)
demo_timer_ = 0.0f;
float interval_range = logo_max_time_ - logo_min_time_;
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
// Resetear timer con intervalos escalados
demo_timer_ = 0.0f;
float interval_range = logo_max_time_ - logo_min_time_;
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
}
// Solo salir automáticamente si NO llegamos desde MANUAL
// Probabilidad de salir: 60% en cada acción → sale rápido (relación DEMO:LOGO = 6:1)
@@ -2224,9 +2314,16 @@ void Engine::enterLogoMode(bool from_demo) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape) {
png_shape->setLogoMode(true);
png_shape->resetFlipCount(); // Resetear contador de flips
}
}
// Resetear variables de espera de flips
logo_waiting_for_flip_ = false;
logo_target_flip_number_ = 0;
logo_target_flip_percentage_ = 0.0f;
logo_current_flip_count_ = 0;
// Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente)
setState(AppMode::LOGO);
}
@@ -2321,7 +2418,7 @@ void Engine::updateBallSizes(int old_size, int new_size) {
// Si ajustamos posición, aplicarla ahora
if (ball->isOnSurface()) {
ball->setRotoBallScreenPosition(pos.x, pos.y);
ball->setShapeScreenPosition(pos.x, pos.y);
}
}
}
@@ -2389,7 +2486,7 @@ void Engine::toggleShapeMode(bool force_gravity_on_exit) {
// Desactivar atracción y resetear escala de profundidad
for (auto& ball : balls_) {
ball->enableRotoBallAttraction(false);
ball->enableShapeAttraction(false);
ball->setDepthScale(1.0f); // Reset escala a 100% (evita "pop" visual)
}
@@ -2460,7 +2557,7 @@ void Engine::activateShape(ShapeType type) {
// Activar atracción física en todas las pelotas
for (auto& ball : balls_) {
ball->enableRotoBallAttraction(true);
ball->enableShapeAttraction(true);
}
// Mostrar texto informativo con nombre de figura (solo si NO estamos en modo demo o logo)
@@ -2510,10 +2607,13 @@ void Engine::updateShape() {
float target_x = center_x + x_3d;
float target_y = center_y + y_3d;
// Actualizar target de la pelota para cálculo de convergencia
balls_[i]->setShapeTarget2D(target_x, target_y);
// Aplicar fuerza de atracción física hacia el punto rotado
// Usar constantes SHAPE (mayor pegajosidad que ROTOBALL)
float shape_size = scale_factor * 80.0f; // 80px = radio base
balls_[i]->applyRotoBallForce(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)
@@ -2539,6 +2639,10 @@ void Engine::updateShape() {
}
shape_convergence_ = static_cast<float>(balls_near) / balls_.size();
// Notificar a la figura sobre el porcentaje de convergencia
// Esto permite que PNGShape decida cuándo empezar a contar para flips
active_shape_->setConvergence(shape_convergence_);
}
}