Sistema de convergencia para LOGO MODE (resolución escalable)

Implementa sistema adaptativo que evita interrupciones prematuras
en resoluciones altas. El timing ahora se ajusta según convergencia
de partículas en lugar de usar intervalos fijos.

Cambios:
- Ball: getDistanceToTarget() para medir distancia a objetivo
- Engine: shape_convergence_, logo_convergence_threshold_ y tiempos escalados
- defines.h: LOGO_CONVERGENCE_MIN/MAX (75-100%)
- updateShape(): Cálculo de % de pelotas convergidas
- toggleShapeMode(): Genera threshold aleatorio al entrar en LOGO
- setState(): Escala logo_min/max_time con resolución (base 720p)
- updateDemoMode(): Dispara cuando (tiempo>=MIN AND convergencia>=threshold) OR tiempo>=MAX

Funcionamiento:
1. Al entrar a SHAPE en LOGO: threshold random 75-100%, tiempos escalados con altura
2. Cada frame: calcula % pelotas cerca de objetivo (shape_convergence_)
3. Dispara acción cuando: (tiempo>=MIN AND convergencia>=threshold) OR tiempo>=MAX
4. Resultado: En 720p funciona como antes, en 1440p espera convergencia real

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-05 01:25:55 +02:00
parent 042c3cad1a
commit ef2f5bea01
5 changed files with 100 additions and 33 deletions

View File

@@ -303,6 +303,15 @@ 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;
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 // Aplicar fuerza de resorte hacia punto objetivo en figuras 3D
void Ball::applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime, 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 spring_k_base, float damping_base_base, float damping_near_base,

View File

@@ -91,6 +91,7 @@ class Ball {
// Sistema de atracción física hacia figuras 3D // Sistema de atracción física hacia figuras 3D
void enableRotoBallAttraction(bool enable); void enableRotoBallAttraction(bool enable);
float getDistanceToTarget() const; // Distancia actual al punto objetivo
void applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime, void applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime,
float spring_k = ROTOBALL_SPRING_K, float spring_k = ROTOBALL_SPRING_K,
float damping_base = ROTOBALL_DAMPING_BASE, float damping_base = ROTOBALL_DAMPING_BASE,

View File

@@ -211,10 +211,14 @@ constexpr int DEMO_LITE_WEIGHT_IMPULSE = 10; // Aplicar impulso (10%)
// Configuración de Modo LOGO (easter egg - "marca de agua") // Configuración de Modo LOGO (easter egg - "marca de agua")
constexpr int LOGO_MODE_MIN_BALLS = 500; // Mínimo de pelotas para activar modo logo constexpr int LOGO_MODE_MIN_BALLS = 500; // Mínimo de pelotas para activar modo logo
constexpr float LOGO_MODE_SHAPE_SCALE = 1.2f; // Escala de figura en modo logo (120%) constexpr float LOGO_MODE_SHAPE_SCALE = 1.2f; // Escala de figura en modo logo (120%)
constexpr float LOGO_ACTION_INTERVAL_MIN = 3.0f; // Tiempo mínimo entre alternancia SHAPE/PHYSICS constexpr float LOGO_ACTION_INTERVAL_MIN = 3.0f; // Tiempo mínimo entre alternancia SHAPE/PHYSICS (escalado con resolución)
constexpr float LOGO_ACTION_INTERVAL_MAX = 5.0f; // Tiempo máximo entre alternancia SHAPE/PHYSICS (más corto que DEMO) constexpr float LOGO_ACTION_INTERVAL_MAX = 5.0f; // Tiempo máximo entre alternancia SHAPE/PHYSICS (escalado con resolución)
constexpr int LOGO_WEIGHT_TOGGLE_PHYSICS = 100; // Único peso: alternar SHAPE ↔ PHYSICS (100%) constexpr int LOGO_WEIGHT_TOGGLE_PHYSICS = 100; // Único peso: alternar SHAPE ↔ PHYSICS (100%)
// Sistema de convergencia para LOGO MODE (evita interrupciones prematuras en resoluciones altas)
constexpr float LOGO_CONVERGENCE_MIN = 0.75f; // 75% mínimo (permite algo de movimiento al disparar)
constexpr float LOGO_CONVERGENCE_MAX = 1.00f; // 100% máximo (completamente formado)
// Probabilidad de salto a Logo Mode desde DEMO/DEMO_LITE (%) // Probabilidad de salto a Logo Mode desde DEMO/DEMO_LITE (%)
// Relación DEMO:LOGO = 6:1 (pasa 6x más tiempo en DEMO que en LOGO) // Relación DEMO:LOGO = 6:1 (pasa 6x más tiempo en DEMO que en LOGO)
constexpr int LOGO_JUMP_PROBABILITY_FROM_DEMO = 5; // 5% probabilidad en DEMO normal (más raro) constexpr int LOGO_JUMP_PROBABILITY_FROM_DEMO = 5; // 5% probabilidad en DEMO normal (más raro)

View File

@@ -145,37 +145,37 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
// Buscar todas las texturas PNG en data/balls/ // Buscar todas las texturas PNG en data/balls/
namespace fs = std::filesystem; namespace fs = std::filesystem;
if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) { if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
std::vector<std::pair<std::string, std::string>> texture_files; // (nombre, path) struct TextureInfo {
std::string name;
std::string path;
int width;
};
std::vector<TextureInfo> texture_files;
// Cargar información de todas las texturas (incluyendo dimensiones)
for (const auto& entry : fs::directory_iterator(balls_dir)) { for (const auto& entry : fs::directory_iterator(balls_dir)) {
if (entry.is_regular_file() && entry.path().extension() == ".png") { if (entry.is_regular_file() && entry.path().extension() == ".png") {
std::string filename = entry.path().stem().string(); // Sin extensión std::string filename = entry.path().stem().string();
std::string fullpath = entry.path().string(); std::string fullpath = entry.path().string();
texture_files.push_back({filename, fullpath});
// Cargar temporalmente para obtener dimensiones
auto temp_texture = std::make_shared<Texture>(renderer_, fullpath);
int width = temp_texture->getWidth();
texture_files.push_back({filename, fullpath, width});
} }
} }
// Ordenar alfabéticamente (normal.png será primero) // Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4)
std::sort(texture_files.begin(), texture_files.end()); std::sort(texture_files.begin(), texture_files.end(),
[](const TextureInfo& a, const TextureInfo& b) {
return a.width > b.width; // Descendente por tamaño
});
// Cargar texturas en orden (con normal.png primero si existe) // Cargar todas las texturas en orden de tamaño (0=big, 1=normal, 2=small, 3=tiny)
int normal_index = -1; for (const auto& info : texture_files) {
for (size_t i = 0; i < texture_files.size(); i++) { textures_.push_back(std::make_shared<Texture>(renderer_, info.path));
if (texture_files[i].first == "normal") { texture_names_.push_back(info.name);
normal_index = static_cast<int>(i);
break;
}
}
// Poner normal.png primero
if (normal_index > 0) {
std::swap(texture_files[0], texture_files[normal_index]);
}
// Cargar todas las texturas
for (const auto& [name, path] : texture_files) {
textures_.push_back(std::make_shared<Texture>(renderer_, path));
texture_names_.push_back(name);
} }
} }
@@ -185,8 +185,14 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
success = false; success = false;
} }
// Establecer textura inicial (índice 0 = normal.png) // Buscar índice de "normal" para usarlo como textura inicial (debería ser índice 1)
current_texture_index_ = 0; current_texture_index_ = 0; // Fallback
for (size_t i = 0; i < texture_names_.size(); i++) {
if (texture_names_[i] == "normal") {
current_texture_index_ = i; // Iniciar en "normal" (índice 1)
break;
}
}
texture_ = textures_[current_texture_index_]; texture_ = textures_[current_texture_index_];
current_ball_size_ = texture_->getWidth(); // Obtener tamaño dinámicamente current_ball_size_ = texture_->getWidth(); // Obtener tamaño dinámicamente
@@ -1411,8 +1417,13 @@ void Engine::setState(AppMode new_mode) {
float min_interval, max_interval; float min_interval, max_interval;
if (new_mode == AppMode::LOGO) { if (new_mode == AppMode::LOGO) {
min_interval = LOGO_ACTION_INTERVAL_MIN; // Escalar tiempos con resolución (720p como base)
max_interval = LOGO_ACTION_INTERVAL_MAX; float resolution_scale = current_screen_height_ / 720.0f;
logo_min_time_ = LOGO_ACTION_INTERVAL_MIN * resolution_scale;
logo_max_time_ = LOGO_ACTION_INTERVAL_MAX * resolution_scale;
min_interval = logo_min_time_;
max_interval = logo_max_time_;
} else { } else {
bool is_lite = (new_mode == AppMode::DEMO_LITE); bool is_lite = (new_mode == AppMode::DEMO_LITE);
min_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN; min_interval = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
@@ -1434,8 +1445,23 @@ void Engine::updateDemoMode() {
// Actualizar timer // Actualizar timer
demo_timer_ += delta_time_; demo_timer_ += delta_time_;
// Determinar si es hora de ejecutar acción (depende del modo)
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_;
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_;
}
// Si es hora de ejecutar acción // Si es hora de ejecutar acción
if (demo_timer_ >= demo_next_action_time_) { if (should_trigger) {
// MODO LOGO: Sistema de acciones variadas con gravedad dinámica // MODO LOGO: Sistema de acciones variadas con gravedad dinámica
if (current_app_mode_ == AppMode::LOGO) { if (current_app_mode_ == AppMode::LOGO) {
// Elegir acción aleatoria ponderada // Elegir acción aleatoria ponderada
@@ -1464,10 +1490,10 @@ void Engine::updateDemoMode() {
} }
} }
// Resetear timer con intervalos de Logo Mode // Resetear timer con intervalos escalados (logo_min_time_ y logo_max_time_)
demo_timer_ = 0.0f; demo_timer_ = 0.0f;
float interval_range = LOGO_ACTION_INTERVAL_MAX - LOGO_ACTION_INTERVAL_MIN; float interval_range = logo_max_time_ - logo_min_time_;
demo_next_action_time_ = LOGO_ACTION_INTERVAL_MIN + (rand() % 1000) / 1000.0f * interval_range; demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
// Solo salir automáticamente si NO llegamos desde MANUAL // 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) // Probabilidad de salir: 60% en cada acción → sale rápido (relación DEMO:LOGO = 6:1)
@@ -1968,6 +1994,13 @@ void Engine::toggleShapeMode(bool force_gravity_on_exit) {
} }
} }
} }
// Si estamos en LOGO MODE, generar threshold aleatorio de convergencia (75-100%)
if (current_app_mode_ == AppMode::LOGO) {
logo_convergence_threshold_ = LOGO_CONVERGENCE_MIN +
(rand() % 1000) / 1000.0f * (LOGO_CONVERGENCE_MAX - LOGO_CONVERGENCE_MIN);
shape_convergence_ = 0.0f; // Reset convergencia al entrar
}
} else { } else {
// Volver a modo física normal // Volver a modo física normal
current_mode_ = SimulationMode::PHYSICS; current_mode_ = SimulationMode::PHYSICS;
@@ -2111,6 +2144,20 @@ void Engine::updateShape() {
float depth_scale = depth_zoom_enabled_ ? (0.5f + z_normalized * 1.0f) : 1.0f; 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)
if (current_app_mode_ == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
int balls_near = 0;
float distance_threshold = SHAPE_NEAR_THRESHOLD * scale_factor;
for (const auto& ball : balls_) {
if (ball->getDistanceToTarget() < distance_threshold) {
balls_near++;
}
}
shape_convergence_ = static_cast<float>(balls_near) / balls_.size();
}
} }
// Limitar escala de figura para evitar que se salga de pantalla // Limitar escala de figura para evitar que se salga de pantalla

View File

@@ -113,6 +113,12 @@ class Engine {
float demo_timer_ = 0.0f; // Contador de tiempo para próxima acción float demo_timer_ = 0.0f; // Contador de tiempo para próxima acción
float demo_next_action_time_ = 0.0f; // Tiempo aleatorio hasta próxima acción (segundos) float demo_next_action_time_ = 0.0f; // Tiempo aleatorio hasta próxima acción (segundos)
// Sistema de convergencia para LOGO MODE (escala con resolución)
float shape_convergence_ = 0.0f; // % de pelotas cerca del objetivo (0.0-1.0)
float logo_convergence_threshold_ = 0.90f; // Threshold aleatorio (75-100%)
float logo_min_time_ = 3.0f; // Tiempo mínimo escalado con resolución
float logo_max_time_ = 5.0f; // Tiempo máximo escalado (backup)
// Estado previo antes de entrar a Logo Mode (para restaurar al salir) // Estado previo antes de entrar a Logo Mode (para restaurar al salir)
ColorTheme logo_previous_theme_ = ColorTheme::SUNSET; ColorTheme logo_previous_theme_ = ColorTheme::SUNSET;
size_t logo_previous_texture_index_ = 0; size_t logo_previous_texture_index_ = 0;