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

@@ -42,7 +42,7 @@ Ball::Ball(float x, float vx, float vy, Color color, std::shared_ptr<Texture> te
// Coeficiente base IGUAL para todas las pelotas (solo variación por rebote individual)
loss_ = BASE_BOUNCE_COEFFICIENT; // Coeficiente fijo para todas las pelotas
// Inicializar valores RotoBall
// Inicializar valores Shape (figuras 3D)
pos_3d_x_ = 0.0f;
pos_3d_y_ = 0.0f;
pos_3d_z_ = 0.0f;
@@ -50,7 +50,7 @@ Ball::Ball(float x, float vx, float vy, Color color, std::shared_ptr<Texture> te
target_y_ = pos_.y;
depth_brightness_ = 1.0f;
depth_scale_ = 1.0f;
rotoball_attraction_active_ = false;
shape_attraction_active_ = false;
}
// Actualiza la lógica de la clase
@@ -267,19 +267,19 @@ void Ball::applyRandomLateralPush() {
}
}
// Funciones para modo RotoBall
void Ball::setRotoBallPosition3D(float x, float y, float z) {
// Funciones para modo Shape (figuras 3D)
void Ball::setShapePosition3D(float x, float y, float z) {
pos_3d_x_ = x;
pos_3d_y_ = y;
pos_3d_z_ = z;
}
void Ball::setRotoBallTarget2D(float x, float y) {
void Ball::setShapeTarget2D(float x, float y) {
target_x_ = x;
target_y_ = y;
}
void Ball::setRotoBallScreenPosition(float x, float y) {
void Ball::setShapeScreenPosition(float x, float y) {
pos_.x = x;
pos_.y = y;
sprite_->setPos({x, y});
@@ -293,9 +293,9 @@ void Ball::setDepthScale(float scale) {
depth_scale_ = scale;
}
// Activar/desactivar atracción física hacia esfera RotoBall
void Ball::enableRotoBallAttraction(bool enable) {
rotoball_attraction_active_ = enable;
// Activar/desactivar atracción física hacia figuras 3D
void Ball::enableShapeAttraction(bool enable) {
shape_attraction_active_ = enable;
// Al activar atracción, resetear flags de superficie para permitir física completa
if (enable) {
@@ -305,18 +305,17 @@ void Ball::enableRotoBallAttraction(bool enable) {
// Obtener distancia actual al punto objetivo (para calcular convergencia)
float Ball::getDistanceToTarget() const {
if (!rotoball_attraction_active_) return 0.0f;
// Siempre calcular distancia (útil para convergencia en LOGO mode)
float dx = target_x_ - pos_.x;
float dy = target_y_ - pos_.y;
return sqrtf(dx * dx + dy * dy);
}
// Aplicar fuerza de resorte hacia punto objetivo en figuras 3D
void Ball::applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime,
float spring_k_base, float damping_base_base, float damping_near_base,
float near_threshold_base, float max_force_base) {
if (!rotoball_attraction_active_) return;
void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius, float deltaTime,
float spring_k_base, float damping_base_base, float damping_near_base,
float near_threshold_base, float max_force_base) {
if (!shape_attraction_active_) return;
// Calcular factor de escala basado en el radio (radio base = 80px)
// Si radius=80 → scale=1.0, si radius=160 → scale=2.0, si radius=360 → scale=4.5

View File

@@ -22,12 +22,12 @@ class Ball {
bool on_surface_; // Indica si la pelota est\u00e1 en la superficie (suelo/techo/pared)
float loss_; // Coeficiente de rebote. Pérdida de energía en cada rebote
// Datos para modo RotoBall (esfera 3D)
float pos_3d_x_, pos_3d_y_, pos_3d_z_; // Posición 3D en la esfera
// Datos para modo Shape (figuras 3D)
float pos_3d_x_, pos_3d_y_, pos_3d_z_; // Posición 3D en la figura
float target_x_, target_y_; // Posición destino 2D (proyección)
float depth_brightness_; // Brillo según profundidad Z (0.0-1.0)
float depth_scale_; // Escala según profundidad Z (0.5-1.5)
bool rotoball_attraction_active_; // ¿Está siendo atraída hacia la esfera?
bool shape_attraction_active_; // ¿Está siendo atraída hacia la figura?
public:
// Constructor
@@ -80,22 +80,22 @@ class Ball {
void updateSize(int new_size); // Actualizar tamaño de hitbox
void setTexture(std::shared_ptr<Texture> texture); // Cambiar textura del sprite
// Funciones para modo RotoBall
void setRotoBallPosition3D(float x, float y, float z);
void setRotoBallTarget2D(float x, float y);
void setRotoBallScreenPosition(float x, float y); // Establecer posición directa en pantalla
// Funciones para modo Shape (figuras 3D)
void setShapePosition3D(float x, float y, float z);
void setShapeTarget2D(float x, float y);
void setShapeScreenPosition(float x, float y); // Establecer posición directa en pantalla
void setDepthBrightness(float brightness);
float getDepthBrightness() const { return depth_brightness_; }
void setDepthScale(float scale);
float getDepthScale() const { return depth_scale_; }
// Sistema de atracción física hacia figuras 3D
void enableRotoBallAttraction(bool enable);
void enableShapeAttraction(bool enable);
float getDistanceToTarget() const; // Distancia actual al punto objetivo
void applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime,
float spring_k = SHAPE_SPRING_K,
float damping_base = SHAPE_DAMPING_BASE,
float damping_near = SHAPE_DAMPING_NEAR,
float near_threshold = SHAPE_NEAR_THRESHOLD,
float max_force = SHAPE_MAX_FORCE);
void applyShapeForce(float target_x, float target_y, float sphere_radius, float deltaTime,
float spring_k = SHAPE_SPRING_K,
float damping_base = SHAPE_DAMPING_BASE,
float damping_near = SHAPE_DAMPING_NEAR,
float near_threshold = SHAPE_NEAR_THRESHOLD,
float max_force = SHAPE_MAX_FORCE);
};

View File

@@ -192,8 +192,8 @@ constexpr bool PNG_USE_EDGES_ONLY = false; // true = solo bordes, fals
// Rotación "legible" (texto de frente con volteretas ocasionales)
constexpr float PNG_IDLE_TIME_MIN = 0.5f; // Tiempo mínimo de frente (segundos) - modo MANUAL
constexpr float PNG_IDLE_TIME_MAX = 2.0f; // Tiempo máximo de frente (segundos) - modo MANUAL
constexpr float PNG_IDLE_TIME_MIN_LOGO = 3.0f; // Tiempo mínimo de frente en LOGO MODE
constexpr float PNG_IDLE_TIME_MAX_LOGO = 5.0f; // Tiempo máximo de frente en LOGO MODE
constexpr float PNG_IDLE_TIME_MIN_LOGO = 2.0f; // Tiempo mínimo de frente en LOGO MODE
constexpr float PNG_IDLE_TIME_MAX_LOGO = 4.0f; // Tiempo máximo de frente en LOGO MODE
constexpr float PNG_FLIP_SPEED = 3.0f; // Velocidad voltereta (rad/s)
constexpr float PNG_FLIP_DURATION = 1.5f; // Duración voltereta (segundos)
@@ -250,6 +250,13 @@ constexpr float LOGO_CONVERGENCE_DISTANCE = 20.0f; // Distancia (px) para consi
constexpr int LOGO_JUMP_PROBABILITY_FROM_DEMO = 5; // 5% probabilidad en DEMO normal (más raro)
constexpr int LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE = 3; // 3% probabilidad en DEMO LITE (aún más raro)
// Sistema de espera de flips en LOGO MODE (camino alternativo de decisión)
constexpr int LOGO_FLIP_WAIT_MIN = 1; // Mínimo de flips a esperar antes de cambiar a PHYSICS
constexpr int LOGO_FLIP_WAIT_MAX = 3; // Máximo de flips a esperar
constexpr float LOGO_FLIP_TRIGGER_MIN = 0.20f; // 20% mínimo de progreso de flip para trigger
constexpr float LOGO_FLIP_TRIGGER_MAX = 0.80f; // 80% máximo de progreso de flip para trigger
constexpr int LOGO_FLIP_WAIT_PROBABILITY = 50; // 50% probabilidad de elegir el camino "esperar flip"
constexpr float PI = 3.14159265358979323846f; // Constante PI
// Función auxiliar para obtener la ruta del directorio del ejecutable

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_);
}
}

View File

@@ -128,6 +128,12 @@ class Engine {
float logo_min_time_ = 3.0f; // Tiempo mínimo escalado con resolución
float logo_max_time_ = 5.0f; // Tiempo máximo escalado (backup)
// Sistema de espera de flips en LOGO MODE (camino alternativo)
bool logo_waiting_for_flip_ = false; // true si eligió el camino "esperar flip"
int logo_target_flip_number_ = 0; // En qué flip actuar (1, 2 o 3)
float logo_target_flip_percentage_ = 0.0f; // % de flip a esperar (0.2-0.8)
int logo_current_flip_count_ = 0; // Flips observados hasta ahora
// Estado previo antes de entrar a Logo Mode (para restaurar al salir)
ColorTheme logo_previous_theme_ = ColorTheme::SUNSET;
size_t logo_previous_texture_index_ = 0;

View File

@@ -268,7 +268,15 @@ std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector
void PNGShape::update(float delta_time, float screen_width, float screen_height) {
if (!is_flipping_) {
// Estado IDLE: texto de frente con pivoteo sutil
idle_timer_ += delta_time;
// Solo contar tiempo para flips si:
// - NO está en modo LOGO, O
// - Está en modo LOGO Y ha alcanzado umbral de convergencia (80%)
bool can_start_flip = !is_logo_mode_ || convergence_threshold_reached_;
if (can_start_flip) {
idle_timer_ += delta_time;
}
// Pivoteo sutil constante (movimiento orgánico)
tilt_x_ += 0.4f * delta_time; // Velocidad sutil en X
@@ -308,6 +316,12 @@ void PNGShape::update(float delta_time, float screen_width, float screen_height)
angle_y_ = 0.0f;
}
}
// Detectar transición de flip (de true a false) para incrementar contador
if (was_flipping_last_frame_ && !is_flipping_) {
flip_count_++; // Flip completado
}
was_flipping_last_frame_ = is_flipping_;
}
void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
@@ -387,3 +401,31 @@ float PNGShape::getScaleFactor(float screen_height) const {
// Escala dinámica según resolución
return PNG_SIZE_FACTOR;
}
// Sistema de convergencia: notificar a la figura sobre el % de pelotas en posición
void PNGShape::setConvergence(float convergence) {
current_convergence_ = convergence;
// Umbral de convergencia
constexpr float CONVERGENCE_THRESHOLD = 0.4f;
// Activar threshold cuando convergencia supera el umbral
if (!convergence_threshold_reached_ && convergence >= CONVERGENCE_THRESHOLD) {
convergence_threshold_reached_ = true;
}
// Desactivar threshold cuando convergencia cae por debajo del umbral
if (convergence < CONVERGENCE_THRESHOLD) {
convergence_threshold_reached_ = false;
}
}
// Obtener progreso del flip actual (0.0 = inicio del flip, 1.0 = fin del flip)
float PNGShape::getFlipProgress() const {
if (!is_flipping_) {
return 0.0f; // No está flipping, progreso = 0
}
// Calcular progreso normalizado (0.0 - 1.0)
return flip_timer_ / PNG_FLIP_DURATION;
}

View File

@@ -43,6 +43,14 @@ private:
// Modo LOGO (intervalos de flip más largos)
bool is_logo_mode_ = false; // true = usar intervalos LOGO (más lentos)
// Sistema de convergencia (solo relevante en modo LOGO)
float current_convergence_ = 0.0f; // Porcentaje actual de convergencia (0.0-1.0)
bool convergence_threshold_reached_ = false; // true si ha alcanzado umbral mínimo (80%)
// Sistema de tracking de flips (para modo LOGO - espera de flips)
int flip_count_ = 0; // Contador de flips completados (reset al entrar a LOGO)
bool was_flipping_last_frame_ = false; // Estado previo para detectar transiciones
// Dimensiones normalizadas
float scale_factor_ = 1.0f;
float center_offset_x_ = 0.0f;
@@ -70,6 +78,18 @@ public:
const char* getName() const override { return "PNG SHAPE"; }
float getScaleFactor(float screen_height) const override;
// Consultar estado de flip
bool isFlipping() const { return is_flipping_; }
// Obtener progreso del flip actual (0.0 = inicio, 1.0 = fin)
float getFlipProgress() const;
// Obtener número de flips completados (para modo LOGO)
int getFlipCount() const { return flip_count_; }
// Resetear contador de flips (llamar al entrar a LOGO MODE)
void resetFlipCount() { flip_count_ = 0; was_flipping_last_frame_ = false; }
// Control de modo LOGO (flip intervals más largos)
void setLogoMode(bool enable) {
is_logo_mode_ = enable;
@@ -78,4 +98,7 @@ public:
float idle_max = enable ? PNG_IDLE_TIME_MAX_LOGO : PNG_IDLE_TIME_MAX;
next_idle_time_ = idle_min + (rand() % 1000) / 1000.0f * (idle_max - idle_min);
}
// Sistema de convergencia (override de Shape::setConvergence)
void setConvergence(float convergence) override;
};

View File

@@ -27,4 +27,9 @@ public:
// screen_height: altura actual de pantalla
// Retorna: factor multiplicador para constantes de física (spring_k, damping, etc.)
virtual float getScaleFactor(float screen_height) const = 0;
// Notificar a la figura sobre el porcentaje de convergencia (pelotas cerca del objetivo)
// convergence: valor de 0.0 (0%) a 1.0 (100%) indicando cuántas pelotas están en posición
// Default: no-op (la mayoría de figuras no necesitan esta información)
virtual void setConvergence(float convergence) {}
};