8 Commits

Author SHA1 Message Date
9ae851d5b6 feat: limitar modo BOIDS a escenarios con ≤1.000 bolas
Pulsar B en escenarios 6-8 (o custom >1K) no activa boids y muestra
notificación. Cambiar de escenario estando en BOIDS queda bloqueado
si el destino supera BOIDS_MAX_BALLS (= SCENE_BALLS_5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 01:13:17 +01:00
e1f6fd0f39 refactor: constantes SCENE_BALLS_N y fix HUD overlay en SHAPE mode
- defines.hpp: añadir SCENE_BALLS_1..8 (topado en 50K), SCENARIO_COUNT,
  reconstruir BALL_COUNT_SCENARIOS con esas constantes
- theme_manager: añadir max_ball_count_ y setMaxBallCount() para capturar
  colores hasta el máximo real (custom incluido), eliminando literal 50000
- engine.cpp: llamar setMaxBallCount() tras inicializar ThemeManager
- gpu_sprite_batch: addFullscreenOverlay() escribe vértices directamente
  sin pasar por el guard de pushQuad(), igual que addBackground(); esto
  corrige el HUD/overlay invisible en SHAPE mode con escenario 8 (50K bolas)
- Textos UI actualizados: tecla 8, help overlay y --skip-benchmark → 50.000

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 00:55:57 +01:00
093b982e01 fix: corregir off-by-one en sprite_capacity para overlay en escenario 8
El +1 por background era incorrecto: addBackground() escribe directamente
en los vectores sin pasar por el guard de pushQuad(), así que no consume
slots del límite. El +1 que garantiza el slot del overlay ya está dentro
de init() con (max_sprites_+1). Quitarlo evita que el overlay se rechace
al llenar exactamente el escenario de 100K bolas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 00:18:51 +01:00
74d954df1e fix: corregir límite de sprites en SHAPE mode con muchas bolas
GpuSpriteBatch::init() ahora acepta capacidad dinámica para soportar
--custom-balls N con N > 200000. El buffer se dimensiona a (N+1) quads,
reservando siempre un slot para el overlay. addFullscreenOverlay() calcula
overlay_index_count_ desde el delta real de indices_ en lugar de fijarlo
a 6 incondicionalmente. Engine calcula la capacidad correcta al init.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 00:14:28 +01:00
46b24bf075 fix: corregir escalado de texto en resoluciones altas y F4 fullscreen
- updatePhysicalWindowSize() acepta logical_height opcional para
  actualizar logical_window_height_ al entrar/salir de F4 real fullscreen
- Engine pasa current_screen_height_ a UIManager en cada cambio de
  tamaño físico, propagando la resolución lógica correcta
- calculateFontSize() subdivide el rango >=900px en tres tramos más
  conservadores (/40, /48, /60) para evitar texto excesivamente grande
  en resoluciones como 2000x1200

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 23:56:59 +01:00
33cb995872 refactor: unificar log de consola y centralizar fuente de UI
- Formato uniforme [Tipo] nombre (pack/disco) en texture, textrenderer, png_shape
- Eliminar logs verbosos de logo_scaler y app_logo (resolución, escalado, etc.)
- Centralizar fuente de UI en APP_FONT (defines.hpp) con las 8 opciones comentadas
- Actualizar carpeta data/fonts con nuevas fuentes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 23:28:32 +01:00
c40eb69fc1 fix: sincronizar texto de ayuda con constantes DEFAULT_SCREEN_*/ZOOM de defines.hpp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 22:37:31 +01:00
1d2e9c5035 feat: F7/F8 redimensionan campo lógico, F1/F2 muestran notificación de zoom
- F7/F8: nuevo setFieldScale() cambia resolución lógica en pasos del 10%
  (mín 50%, máx limitado por pantalla), reinicia escena como F4
- F1/F2: muestran notificación "Zoom X%" al cambiar escala de ventana
- Ventana física = lógico × zoom en todo momento; resizeWindowCentered()
  unifica el cálculo de posición leyendo el tamaño real con SDL_GetWindowSize
- PostFXUniforms::time renombrado a screen_height; scanlines usan la altura
  lógica actual en lugar del 720 hardcodeado — F1/F2 escalan las scanlines
  visualmente, F7/F8 las mantienen a 1 franja por píxel lógico
- Eliminados logs de debug de calculateMaxWindowScale y setWindowScale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 22:35:40 +01:00
29 changed files with 269 additions and 131 deletions

BIN
data/fonts/Exo2-Regular.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,7 +6,7 @@ layout(set=3, binding=0) uniform PostFXUniforms {
float vignette_strength;
float chroma_strength;
float scanline_strength;
float time;
float screen_height;
} u;
void main() {
float ca = u.chroma_strength * 0.005;
@@ -15,7 +15,7 @@ void main() {
color.g = texture(scene, v_uv).g;
color.b = texture(scene, v_uv - vec2( ca, 0.0)).b;
color.a = texture(scene, v_uv).a;
float scan = 0.85 + 0.15 * sin(v_uv.y * 3.14159265 * 720.0);
float scan = 0.85 + 0.15 * sin(v_uv.y * 3.14159265 * u.screen_height);
color.rgb *= mix(1.0, scan, u.scanline_strength);
vec2 d = v_uv - vec2(0.5, 0.5);
float vignette = 1.0 - dot(d, d) * u.vignette_strength;

View File

@@ -5,22 +5,29 @@
#include <vector> // for std::vector in DynamicThemeKeyframe/DynamicTheme
// Configuración de ventana y pantalla
constexpr char WINDOW_CAPTION[] = "ViBe3 Physics (JailDesigner 2025)";
constexpr char WINDOW_CAPTION[] = "© 2025 ViBe3 Physics JailDesigner";
// Resolución por defecto (usada si no se especifica en CLI)
constexpr int DEFAULT_SCREEN_WIDTH = 1280; // Ancho lógico por defecto (si no hay -w)
constexpr int DEFAULT_SCREEN_HEIGHT = 720; // Alto lógico por defecto (si no hay -h)
constexpr int DEFAULT_WINDOW_ZOOM = 1; // Zoom inicial de ventana (1x = sin zoom)
// Configuración de zoom dinámico de ventana
constexpr int WINDOW_ZOOM_MIN = 1; // Zoom mínimo (320x240)
constexpr int WINDOW_ZOOM_MAX = 10; // Zoom máximo teórico (3200x2400)
// Configuración de zoom dinámico de ventana (legacy, solo usado en initialize())
constexpr int WINDOW_ZOOM_MIN = 1; // Zoom mínimo (legacy, para validación inicial)
constexpr int WINDOW_ZOOM_MAX = 10; // Zoom máximo teórico (legacy)
constexpr int WINDOW_DESKTOP_MARGIN = 10; // Margen mínimo con bordes del escritorio
constexpr int WINDOW_DECORATION_HEIGHT = 30; // Altura estimada de decoraciones del SO
// Configuración de escala de ventana por pasos (F1/F2)
constexpr float WINDOW_SCALE_STEP = 0.1f; // Incremento/decremento por pulsación (10%)
constexpr float WINDOW_SCALE_MIN = 0.5f; // Escala mínima (50% de la resolución base)
// Configuración de física
constexpr float GRAVITY_FORCE = 0.2f; // Fuerza de gravedad (píxeles/frame²)
// Fuente de la interfaz
#define APP_FONT "data/fonts/Exo2-Regular.ttf"
// Configuración de interfaz
constexpr float THEME_TRANSITION_DURATION = 0.5f; // Duración de transiciones LERP entre temas (segundos)
@@ -52,10 +59,24 @@ constexpr float BALL_SPAWN_MARGIN = 0.15f; // Margen lateral para spawn (0.25 =
// Escenarios de número de pelotas (teclas 1-8)
// Fase 1 (instanced rendering): límit pràctic ~100K a 60fps (physics bound)
constexpr int BALL_COUNT_SCENARIOS[8] = {10, 50, 100, 500, 1000, 5000, 10000, 100000};
constexpr int SCENE_BALLS_1 = 10;
constexpr int SCENE_BALLS_2 = 50;
constexpr int SCENE_BALLS_3 = 100;
constexpr int SCENE_BALLS_4 = 500;
constexpr int SCENE_BALLS_5 = 1000;
constexpr int SCENE_BALLS_6 = 5000;
constexpr int SCENE_BALLS_7 = 10000;
constexpr int SCENE_BALLS_8 = 50000; // Máximo escenario estándar (tecla 8)
constexpr int SCENARIO_COUNT = 8;
constexpr int BALL_COUNT_SCENARIOS[SCENARIO_COUNT] = {
SCENE_BALLS_1, SCENE_BALLS_2, SCENE_BALLS_3, SCENE_BALLS_4,
SCENE_BALLS_5, SCENE_BALLS_6, SCENE_BALLS_7, SCENE_BALLS_8
};
constexpr int BOIDS_MAX_BALLS = SCENE_BALLS_5; // 1 000 bolas máximo en modo BOIDS
// Límites de escenario para modos automáticos (índices en BALL_COUNT_SCENARIOS)
// BALL_COUNT_SCENARIOS = {10, 50, 100, 500, 1000, 5000, 10000, 50000}
// 0 1 2 3 4 5 6 7
constexpr int DEMO_AUTO_MIN_SCENARIO = 2; // mínimo 100 bolas
constexpr int DEMO_AUTO_MAX_SCENARIO = 7; // máximo sin restricción hardware (ajustado por benchmark)

View File

@@ -84,8 +84,8 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
window_zoom = 1;
}
// Guardar zoom calculado ANTES de crear la ventana (para F1/F2/F3/F4)
current_window_zoom_ = window_zoom;
// Guardar escala inicial (siempre 1.0 salvo que CLI haya pedido zoom > 1)
current_window_scale_ = static_cast<float>(window_zoom);
// Calcular tamaño de ventana
int window_width = logical_width * window_zoom;
@@ -231,8 +231,15 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
success = false;
}
// Capacidad = número de bolas del escenario máximo (o custom si es mayor).
// addBackground() no usa el guard de pushQuad(), así que no consume slots aquí.
// init() reserva internamente +1 quad extra garantizado para el overlay.
int sprite_capacity = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO];
if (custom_scenario_enabled_ && custom_scenario_balls_ > sprite_capacity)
sprite_capacity = custom_scenario_balls_;
sprite_batch_ = std::make_unique<GpuSpriteBatch>();
if (!sprite_batch_->init(gpu_ctx_->device())) {
if (!sprite_batch_->init(gpu_ctx_->device(), sprite_capacity)) {
std::cerr << "ERROR: No se pudo crear el sprite batch GPU" << std::endl;
success = false;
}
@@ -275,6 +282,12 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Inicializar ThemeManager PRIMERO (requerido por Notifier y SceneManager)
theme_manager_ = std::make_unique<ThemeManager>();
theme_manager_->initialize();
{
int max_balls = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO];
if (custom_scenario_enabled_ && custom_scenario_balls_ > max_balls)
max_balls = custom_scenario_balls_;
theme_manager_->setMaxBallCount(max_balls);
}
// Inicializar SceneManager (gestión de bolas y física)
scene_manager_ = std::make_unique<SceneManager>(current_screen_width_, current_screen_height_);
@@ -427,7 +440,7 @@ void Engine::calculateDeltaTime() {
void Engine::update() {
// Accumulate time for PostFX uniforms
postfx_uniforms_.time += delta_time_;
postfx_uniforms_.screen_height = static_cast<float>(current_screen_height_);
// Actualizar visibilidad del cursor (auto-ocultar tras inactividad)
Mouse::updateCursorVisibility();
@@ -551,7 +564,21 @@ void Engine::toggleDepthZoom() {
}
// Boids (comportamiento de enjambre)
bool Engine::isScenarioAllowedForBoids(int scenario_id) const {
int ball_count = (scenario_id == CUSTOM_SCENARIO_IDX)
? custom_scenario_balls_
: BALL_COUNT_SCENARIOS[scenario_id];
return ball_count <= BOIDS_MAX_BALLS;
}
void Engine::toggleBoidsMode(bool force_gravity_on) {
if (current_mode_ != SimulationMode::BOIDS) {
// Intentando activar BOIDS — verificar escenario actual
if (!isScenarioAllowedForBoids(scene_manager_->getCurrentScenario())) {
showNotificationForAction("Boids: máximo 1.000 pelotas");
return;
}
}
if (current_mode_ == SimulationMode::BOIDS) {
// Salir del modo boids
current_mode_ = SimulationMode::PHYSICS;
@@ -641,6 +668,12 @@ void Engine::setCustomScenario(int balls) {
// Escenarios (número de pelotas)
void Engine::changeScenario(int scenario_id, const char* notification_text) {
if (current_mode_ == SimulationMode::BOIDS) {
if (!isScenarioAllowedForBoids(scenario_id)) {
showNotificationForAction("Boids: máximo 1.000 pelotas");
return;
}
}
// Pasar el modo actual al SceneManager para inicialización correcta
scene_manager_->changeScenario(scenario_id, current_mode_);
@@ -953,7 +986,9 @@ void Engine::toggleFullscreen() {
// Si acabamos de salir de fullscreen, restaurar tamaño de ventana
if (!fullscreen_enabled_) {
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
int restore_w = static_cast<int>(std::round(base_screen_width_ * current_window_scale_));
int restore_h = static_cast<int>(std::round(base_screen_height_ * current_window_scale_));
SDL_SetWindowSize(window_, restore_w, restore_h);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
@@ -1018,10 +1053,13 @@ void Engine::toggleRealFullscreen() {
// Volver a resolución base (configurada por CLI o default)
current_screen_width_ = base_screen_width_;
current_screen_height_ = base_screen_height_;
current_field_scale_ = 1.0f; // Resetear escala de campo al salir de fullscreen real
// Restaurar ventana normal con el zoom actual (no hardcoded)
// Restaurar ventana normal con la escala actual
SDL_SetWindowFullscreen(window_, false);
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
int restore_w = static_cast<int>(std::round(base_screen_width_ * current_window_scale_));
int restore_h = static_cast<int>(std::round(base_screen_height_ * current_window_scale_));
SDL_SetWindowSize(window_, restore_w, restore_h);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Recrear render target offscreen con resolución base
@@ -1148,85 +1186,112 @@ void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g,
static_cast<float>(current_screen_height_));
}
// Sistema de zoom dinámico
int Engine::calculateMaxWindowZoom() const {
// Obtener información del display usando el método de Coffee Crisis
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays == nullptr || num_displays == 0) {
return WINDOW_ZOOM_MIN; // Fallback si no se puede obtener
// Sistema de escala de ventana (pasos del 10%)
float Engine::calculateMaxWindowScale() const {
SDL_Rect bounds;
if (!SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) { // bool: false = error
return WINDOW_SCALE_MIN; // Fallback solo si falla de verdad
}
// Obtener el modo de display actual
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm == nullptr) {
SDL_free(displays);
return WINDOW_ZOOM_MIN;
}
// Calcular zoom máximo usando la fórmula de Coffee Crisis
const int MAX_ZOOM = std::min(dm->w / base_screen_width_, (dm->h - WINDOW_DECORATION_HEIGHT) / base_screen_height_);
SDL_free(displays);
// Aplicar límites
return std::max(WINDOW_ZOOM_MIN, std::min(MAX_ZOOM, WINDOW_ZOOM_MAX));
float max_by_w = static_cast<float>(bounds.w - 2 * WINDOW_DESKTOP_MARGIN) / base_screen_width_;
float max_by_h = static_cast<float>(bounds.h - 2 * WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT) / base_screen_height_;
float result = std::max(WINDOW_SCALE_MIN, std::min(max_by_w, max_by_h));
return result;
}
void Engine::setWindowZoom(int new_zoom) {
// Validar zoom
int max_zoom = calculateMaxWindowZoom();
new_zoom = std::max(WINDOW_ZOOM_MIN, std::min(new_zoom, max_zoom));
// Redimensiona la ventana física manteniéndo su centro, con clamping a pantalla.
static void resizeWindowCentered(SDL_Window* window, int new_w, int new_h) {
int cur_x, cur_y, cur_w, cur_h;
SDL_GetWindowPosition(window, &cur_x, &cur_y);
SDL_GetWindowSize(window, &cur_w, &cur_h);
if (new_zoom == current_window_zoom_) {
return; // No hay cambio
int new_x = cur_x + (cur_w - new_w) / 2;
int new_y = cur_y + (cur_h - new_h) / 2;
SDL_Rect bounds;
if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) {
new_x = std::max(WINDOW_DESKTOP_MARGIN,
std::min(new_x, bounds.w - new_w - WINDOW_DESKTOP_MARGIN));
new_y = std::max(WINDOW_DESKTOP_MARGIN,
std::min(new_y, bounds.h - new_h - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT));
}
// Obtener posición actual del centro de la ventana
int current_x, current_y;
SDL_GetWindowPosition(window_, &current_x, &current_y);
int current_center_x = current_x + (base_screen_width_ * current_window_zoom_) / 2;
int current_center_y = current_y + (base_screen_height_ * current_window_zoom_) / 2;
SDL_SetWindowSize(window, new_w, new_h);
SDL_SetWindowPosition(window, new_x, new_y);
}
// Calcular nuevo tamaño
int new_width = base_screen_width_ * new_zoom;
int new_height = base_screen_height_ * new_zoom;
void Engine::setWindowScale(float new_scale) {
float max_scale = calculateMaxWindowScale();
new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale));
new_scale = std::round(new_scale * 10.0f) / 10.0f;
// Calcular nueva posición (centrada en el punto actual)
int new_x = current_center_x - new_width / 2;
int new_y = current_center_y - new_height / 2;
if (new_scale == current_window_scale_) return;
// Obtener límites del escritorio para no salirse
SDL_Rect display_bounds;
if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &display_bounds) == 0) {
// Aplicar márgenes
int min_x = WINDOW_DESKTOP_MARGIN;
int min_y = WINDOW_DESKTOP_MARGIN;
int max_x = display_bounds.w - new_width - WINDOW_DESKTOP_MARGIN;
int max_y = display_bounds.h - new_height - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT;
int new_width = static_cast<int>(std::round(current_screen_width_ * new_scale));
int new_height = static_cast<int>(std::round(current_screen_height_ * new_scale));
// Limitar posición
new_x = std::max(min_x, std::min(new_x, max_x));
new_y = std::max(min_y, std::min(new_y, max_y));
}
resizeWindowCentered(window_, new_width, new_height);
current_window_scale_ = new_scale;
// Aplicar cambios
SDL_SetWindowSize(window_, new_width, new_height);
SDL_SetWindowPosition(window_, new_x, new_y);
current_window_zoom_ = new_zoom;
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
}
void Engine::zoomIn() {
setWindowZoom(current_window_zoom_ + 1);
float prev = current_window_scale_;
setWindowScale(current_window_scale_ + WINDOW_SCALE_STEP);
if (current_window_scale_ != prev) {
char buf[32];
std::snprintf(buf, sizeof(buf), "Zoom %.0f%%", current_window_scale_ * 100.0f);
showNotificationForAction(buf);
}
}
void Engine::zoomOut() {
setWindowZoom(current_window_zoom_ - 1);
float prev = current_window_scale_;
setWindowScale(current_window_scale_ - WINDOW_SCALE_STEP);
if (current_window_scale_ != prev) {
char buf[32];
std::snprintf(buf, sizeof(buf), "Zoom %.0f%%", current_window_scale_ * 100.0f);
showNotificationForAction(buf);
}
}
void Engine::setFieldScale(float new_scale) {
float max_scale = calculateMaxWindowScale();
new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale));
new_scale = std::round(new_scale * 10.0f) / 10.0f;
if (new_scale == current_field_scale_) return;
current_field_scale_ = new_scale;
current_screen_width_ = static_cast<int>(std::round(base_screen_width_ * new_scale));
current_screen_height_ = static_cast<int>(std::round(base_screen_height_ * new_scale));
// Ajustar ventana física: campo lógico × zoom actual, manteniendo centro
int phys_w = static_cast<int>(std::round(current_screen_width_ * current_window_scale_));
int phys_h = static_cast<int>(std::round(current_screen_height_ * current_window_scale_));
resizeWindowCentered(window_, phys_w, phys_h);
// Recrear render target con nueva resolución lógica
recreateOffscreenTexture();
updatePhysicalWindowSize();
// Reiniciar escena (igual que F4)
scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
if (app_logo_) app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
if (current_mode_ == SimulationMode::SHAPE) {
generateShape();
scene_manager_->enableShapeAttractionAll(true);
}
showNotificationForAction("Campo " + std::to_string(current_screen_width_) +
" x " + std::to_string(current_screen_height_));
}
void Engine::fieldSizeUp() { setFieldScale(current_field_scale_ + WINDOW_SCALE_STEP); }
void Engine::fieldSizeDown() { setFieldScale(current_field_scale_ - WINDOW_SCALE_STEP); }
void Engine::updatePhysicalWindowSize() {
if (real_fullscreen_enabled_) {
// En fullscreen real (F4), usar resolución del display
@@ -1253,7 +1318,11 @@ void Engine::updatePhysicalWindowSize() {
}
// Notificar a UIManager del cambio de tamaño (delegado)
ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_);
// Pasar current_screen_height_ para que UIManager actualice la altura lógica
// (necesario en F4 donde la resolución lógica cambia a la del display)
ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_,
current_screen_height_);
}
// ============================================================================

View File

@@ -76,6 +76,11 @@ class Engine {
void toggleRealFullscreen();
void toggleIntegerScaling();
// Campo de juego (tamaño lógico + físico)
void fieldSizeUp();
void fieldSizeDown();
void setFieldScale(float new_scale);
// PostFX presets
void handlePostFXCycle();
void handlePostFXToggle();
@@ -187,8 +192,11 @@ class Engine {
float postfx_override_vignette_ = -1.f; // -1 = sin override
float postfx_override_chroma_ = -1.f;
// Sistema de zoom dinámico
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
// Sistema de escala de ventana
float current_window_scale_ = 1.0f;
// Escala del campo de juego lógico (F7/F8)
float current_field_scale_ = 1.0f;
// V-Sync y fullscreen
bool vsync_enabled_ = true;
@@ -242,9 +250,9 @@ class Engine {
// Sistema de cambio de sprites dinámico
void switchTextureInternal(bool show_notification);
// Sistema de zoom dinámico
int calculateMaxWindowZoom() const;
void setWindowZoom(int new_zoom);
// Sistema de escala de ventana
float calculateMaxWindowScale() const;
void setWindowScale(float new_scale);
void zoomIn();
void zoomOut();
void updatePhysicalWindowSize();
@@ -261,6 +269,9 @@ class Engine {
// PostFX helper
void applyPostFXPreset(int mode);
// Boids: comprueba si un escenario tiene ≤ BOIDS_MAX_BALLS bolas
bool isScenarioAllowedForBoids(int scenario_id) const;
// GPU helpers
bool loadGpuSpriteTexture(size_t index); // Upload one sprite texture to GPU
void recreateOffscreenTexture(); // Recreate when resolution changes

View File

@@ -50,11 +50,7 @@ bool Texture::loadFromFile(const std::string &file_path) {
delete[] resourceData; // Liberar buffer temporal
if (data != nullptr) {
if (ResourceManager::isPackLoaded()) {
std::cout << "Imagen cargada desde pack: " << filename.c_str() << std::endl;
} else {
std::cout << "Imagen cargada desde disco: " << filename.c_str() << std::endl;
}
std::cout << "[Textura] " << filename << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
}
}

View File

@@ -117,7 +117,7 @@ struct PostFXUniforms {
float vignette_strength;
float chroma_strength;
float scanline_strength;
float time;
float screen_height;
};
fragment float4 postfx_fs(PostVOut in [[stage_in]],
@@ -133,7 +133,7 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
color.a = scene.sample(samp, in.uv ).a;
// Scanlines: horizontal sine-wave at ~360 lines (one dark band per 2 px at 720p)
float scan = 0.85 + 0.15 * sin(in.uv.y * 3.14159265 * 720.0);
float scan = 0.85 + 0.15 * sin(in.uv.y * 3.14159265 * u.screen_height);
color.rgb *= mix(1.0, scan, u.scanline_strength);
// Vignette: radial edge darkening

View File

@@ -11,7 +11,7 @@ struct PostFXUniforms {
float vignette_strength; // 0 = none, 0.8 = default subtle
float chroma_strength; // 0 = off, 0.2 = default chromatic aberration
float scanline_strength; // 0 = off, 1 = full scanlines
float time; // accumulated seconds (for future animations)
float screen_height; // logical render target height (px), for resolution-independent scanlines
};
// ============================================================================

View File

@@ -7,10 +7,12 @@
// Public interface
// ---------------------------------------------------------------------------
bool GpuSpriteBatch::init(SDL_GPUDevice* device) {
// Pre-allocate GPU buffers large enough for MAX_SPRITES quads.
Uint32 max_verts = static_cast<Uint32>(MAX_SPRITES) * 4;
Uint32 max_indices = static_cast<Uint32>(MAX_SPRITES) * 6;
bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
max_sprites_ = max_sprites;
// Pre-allocate GPU buffers large enough for (max_sprites_ + 2) quads.
// The +2 reserves one slot for the background quad and one for the fullscreen overlay.
Uint32 max_verts = static_cast<Uint32>(max_sprites_ + 2) * 4;
Uint32 max_indices = static_cast<Uint32>(max_sprites_ + 2) * 6;
Uint32 vb_size = max_verts * sizeof(GpuVertex);
Uint32 ib_size = max_indices * sizeof(uint32_t);
@@ -53,8 +55,8 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device) {
return false;
}
vertices_.reserve(MAX_SPRITES * 4);
indices_.reserve(MAX_SPRITES * 6);
vertices_.reserve(static_cast<size_t>(max_sprites_ + 2) * 4);
indices_.reserve(static_cast<size_t>(max_sprites_ + 2) * 6);
return true;
}
@@ -128,9 +130,17 @@ void GpuSpriteBatch::addSprite(float x, float y, float w, float h,
}
void GpuSpriteBatch::addFullscreenOverlay() {
// El overlay es un slot reservado fuera del espacio de max_sprites_, igual que el background.
// Escribe directamente sin pasar por el guard de pushQuad().
overlay_index_offset_ = static_cast<int>(indices_.size());
pushQuad(-1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);
overlay_index_count_ = 6;
uint32_t vi = static_cast<uint32_t>(vertices_.size());
vertices_.push_back({ -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f });
vertices_.push_back({ 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f });
vertices_.push_back({ 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f });
vertices_.push_back({ -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f });
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0);
overlay_index_count_ = 6;
}
bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) {
@@ -179,7 +189,8 @@ void GpuSpriteBatch::toNDC(float px, float py,
void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
float u0, float v0, float u1, float v1,
float r, float g, float b, float a) {
if (vertices_.size() + 4 > static_cast<size_t>(MAX_SPRITES) * 4) return;
// +1 reserva el slot del background que ya entró sin pasar por este guard.
if (vertices_.size() + 4 > static_cast<size_t>(max_sprites_ + 1) * 4) return;
uint32_t vi = static_cast<uint32_t>(vertices_.size());
// TL, TR, BR, BL

View File

@@ -26,10 +26,10 @@ struct GpuVertex {
// ============================================================================
class GpuSpriteBatch {
public:
// Maximum sprites (background + UI overlay each count as one sprite)
static constexpr int MAX_SPRITES = 200000;
// Default maximum sprites (background + UI overlay each count as one sprite)
static constexpr int DEFAULT_MAX_SPRITES = 200000;
bool init(SDL_GPUDevice* device);
bool init(SDL_GPUDevice* device, int max_sprites = DEFAULT_MAX_SPRITES);
void destroy(SDL_GPUDevice* device);
void beginFrame();
@@ -83,4 +83,6 @@ private:
int sprite_index_count_ = 0;
int overlay_index_offset_ = 0;
int overlay_index_count_ = 0;
int max_sprites_ = DEFAULT_MAX_SPRITES;
};

View File

@@ -264,6 +264,17 @@ bool InputHandler::processEvents(Engine& engine) {
engine.toggleIntegerScaling();
break;
// Redimensionar campo de juego (tamaño lógico + físico)
case SDLK_F7:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
else engine.fieldSizeDown();
break;
case SDLK_F8:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
else engine.fieldSizeUp();
break;
// Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D)
case SDLK_D:
// Shift+D = Pausar tema dinámico

View File

@@ -11,9 +11,9 @@ void printHelp() {
std::cout << "ViBe3 Physics - Simulador de físicas avanzadas\n";
std::cout << "\nUso: vibe3_physics [opciones]\n\n";
std::cout << "Opciones:\n";
std::cout << " -w, --width <px> Ancho de resolución (default: 320)\n";
std::cout << " -h, --height <px> Alto de resolución (default: 240)\n";
std::cout << " -z, --zoom <n> Zoom de ventana (default: 3)\n";
std::cout << " -w, --width <px> Ancho de resolución (default: " << DEFAULT_SCREEN_WIDTH << ")\n";
std::cout << " -h, --height <px> Alto de resolución (default: " << DEFAULT_SCREEN_HEIGHT << ")\n";
std::cout << " -z, --zoom <n> Zoom de ventana (default: " << DEFAULT_WINDOW_ZOOM << ")\n";
std::cout << " -f, --fullscreen Modo pantalla completa (F3 - letterbox)\n";
std::cout << " -F, --real-fullscreen Modo pantalla completa real (F4 - nativo)\n";
std::cout << " -k, --kiosk Modo kiosko (F4 fijo, sin ESC, sin zoom)\n";
@@ -26,7 +26,7 @@ void printHelp() {
std::cout << " --chroma <float> Sobreescribir chroma_strength (activa PostFX si no hay --postfx)\n";
std::cout << " --help Mostrar esta ayuda\n\n";
std::cout << "Ejemplos:\n";
std::cout << " vibe3_physics # 320x240 zoom 3 (ventana 960x720)\n";
std::cout << " vibe3_physics # " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << " zoom " << DEFAULT_WINDOW_ZOOM << " (default)\n";
std::cout << " vibe3_physics -w 1920 -h 1080 # 1920x1080 zoom 1 (auto)\n";
std::cout << " vibe3_physics -w 640 -h 480 -z 2 # 640x480 zoom 2 (ventana 1280x960)\n";
std::cout << " vibe3_physics -f # Fullscreen letterbox (F3)\n";

View File

@@ -22,7 +22,11 @@ PNGShape::PNGShape(const char* png_path) {
}
bool PNGShape::loadPNG(const char* resource_key) {
std::cout << "[PNGShape] Cargando recurso: " << resource_key << std::endl;
{
std::string fn = std::string(resource_key);
fn = fn.substr(fn.find_last_of("\\/") + 1);
std::cout << "[PNGShape] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
}
unsigned char* file_data = nullptr;
size_t file_size = 0;
if (!ResourceManager::loadResource(resource_key, file_data, file_size)) {

View File

@@ -1,6 +1,7 @@
#include "textrenderer.hpp"
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <iostream>
#include "resource_manager.hpp"
TextRenderer::TextRenderer() : renderer_(nullptr), font_(nullptr), font_size_(0), use_antialiasing_(true), font_data_buffer_(nullptr) {
@@ -44,7 +45,11 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
// CRÍTICO: NO eliminar fontData aquí - SDL_ttf necesita estos datos en memoria
// mientras la fuente esté abierta. Se liberará en cleanup()
font_data_buffer_ = fontData;
SDL_Log("Fuente cargada desde ResourceManager: %s (%lu bytes)", font_path, (unsigned long)fontDataSize);
{
std::string fn = std::string(font_path);
fn = fn.substr(fn.find_last_of("\\/") + 1);
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
}
return true;
} else {
delete[] fontData;
@@ -102,7 +107,10 @@ bool TextRenderer::reinitialize(int new_font_size) {
// Mantener buffer en memoria (NO eliminar)
font_data_buffer_ = fontData;
font_size_ = new_font_size;
SDL_Log("Fuente recargada desde ResourceManager: %s (tamaño %d)", font_path_.c_str(), new_font_size);
{
std::string fn = font_path_.substr(font_path_.find_last_of("\\/") + 1);
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
}
return true;
} else {
delete[] fontData;

View File

@@ -640,10 +640,10 @@ std::unique_ptr<ThemeSnapshot> ThemeManager::captureCurrentSnapshot() const {
snapshot->name_en = themes_[current_theme_index_]->getNameEN();
snapshot->name_es = themes_[current_theme_index_]->getNameES();
// Capturar colores de pelotas (suficientes para escenario máximo: 50,000)
// Esto asegura LERP correcto incluso en escenarios grandes
snapshot->ball_colors.reserve(50000);
for (size_t i = 0; i < 50000; i++) {
// Capturar colores de pelotas para el máximo real de esta sesión
// (SCENE_BALLS_8 o más si hay escenario custom)
snapshot->ball_colors.reserve(max_ball_count_);
for (int i = 0; i < max_ball_count_; i++) {
snapshot->ball_colors.push_back(
themes_[current_theme_index_]->getBallColor(i, 0.0f)
);

View File

@@ -43,6 +43,7 @@ class ThemeManager {
// Inicialización
void initialize(); // Inicializa 15 temas unificados (9 estáticos + 6 dinámicos)
void setMaxBallCount(int n) { max_ball_count_ = n; } // Máximo real (escenario 8 o custom si mayor)
// Interfaz unificada (PHASE 2 + PHASE 3)
void switchToTheme(int theme_index); // Cambia a tema 0-14 con transición LERP suave (PHASE 3)
@@ -99,6 +100,9 @@ class ThemeManager {
// Snapshot del tema origen (capturado al iniciar transición)
std::unique_ptr<ThemeSnapshot> source_snapshot_; // nullptr si no hay transición
// Máximo de bolas posible en esta sesión (max(SCENE_BALLS_8, custom_balls))
int max_ball_count_ = SCENE_BALLS_8;
// ========================================
// MÉTODOS PRIVADOS
// ========================================

View File

@@ -65,12 +65,6 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
int logo_base_target_height = static_cast<int>(base_screen_height_ * APPLOGO_HEIGHT_PERCENT);
int logo_native_target_height = static_cast<int>(native_screen_height_ * APPLOGO_HEIGHT_PERCENT);
std::cout << "Pre-escalando logos:" << std::endl;
std::cout << " Base: " << base_screen_width_ << "x" << base_screen_height_
<< " (altura logo: " << logo_base_target_height << "px)" << std::endl;
std::cout << " Nativa: " << native_screen_width_ << "x" << native_screen_height_
<< " (altura logo: " << logo_native_target_height << "px)" << std::endl;
// ========================================================================
// 3. Cargar y escalar LOGO1 (data/logo/logo.png) a 2 versiones
// ========================================================================
@@ -193,7 +187,7 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
logo2_current_width_ = logo2_base_width_;
logo2_current_height_ = logo2_base_height_;
std::cout << "Logos pre-escalados exitosamente (4 texturas creadas)" << std::endl;
std::cout << "[Logo] logo.png + logo2.png (base " << logo_base_target_height << "px, nativa " << logo_native_target_height << "px)\n";
return true;
}

View File

@@ -2,6 +2,7 @@
#include <algorithm> // for std::min
#include "defines.hpp"
#include "text/textrenderer.hpp"
#include "theme_manager.hpp"
@@ -101,7 +102,7 @@ void HelpOverlay::initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, in
// Crear renderer de texto con tamaño dinámico
text_renderer_ = new TextRenderer();
text_renderer_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", font_size, true);
text_renderer_->init(renderer, APP_FONT, font_size, true);
calculateBoxDimensions();
}

View File

@@ -41,7 +41,6 @@ bool LogoScaler::detectNativeResolution(int& native_width, int& native_height) {
SDL_free(displays);
std::cout << "Resolución nativa detectada: " << native_width << "x" << native_height << std::endl;
return true;
}
@@ -73,8 +72,6 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
return nullptr;
}
std::cout << "Imagen cargada: " << path << " (" << orig_width << "x" << orig_height << ")" << std::endl;
// 2. Calcular tamaño final manteniendo aspect ratio
// El alto está fijado por target_height (APPLOGO_HEIGHT_PERCENT)
// Calcular ancho proporcional al aspect ratio original
@@ -82,8 +79,6 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
out_width = static_cast<int>(target_height * aspect_ratio);
out_height = target_height;
std::cout << " Escalando a: " << out_width << "x" << out_height << std::endl;
// 3. Alocar buffer para imagen escalada (RGBA = 4 bytes por píxel)
unsigned char* scaled_data = static_cast<unsigned char*>(malloc(out_width * out_height * 4));
if (scaled_data == nullptr) {
@@ -109,7 +104,6 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
return nullptr;
}
std::cout << " Escalado completado correctamente" << std::endl;
return scaled_data;
}

View File

@@ -90,8 +90,8 @@ void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
text_renderer_notifier_ = new TextRenderer();
// Inicializar renderers con tamaño dinámico
text_renderer_debug_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", std::max(9, current_font_size_ - 2), true);
text_renderer_notifier_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true);
text_renderer_debug_->init(renderer, APP_FONT, std::max(9, current_font_size_ - 2), true);
text_renderer_notifier_->init(renderer, APP_FONT, current_font_size_, true);
// Crear y configurar sistema de notificaciones
notifier_ = new Notifier();
@@ -172,10 +172,15 @@ void UIManager::updateVSyncText(bool enabled) {
vsync_text_ = enabled ? "V-Sync: On" : "V-Sync: Off";
}
void UIManager::updatePhysicalWindowSize(int width, int height) {
void UIManager::updatePhysicalWindowSize(int width, int height, int logical_height) {
physical_window_width_ = width;
physical_window_height_ = height;
// Actualizar altura lógica si se proporciona (ej. al entrar/salir de F4)
if (logical_height > 0) {
logical_window_height_ = logical_height;
}
// Calcular nuevo tamaño de fuente apropiado basado en altura LÓGICA
// (las dimensiones lógicas no cambian con zoom, solo con cambios explícitos de resolución)
int new_font_size = calculateFontSize(logical_window_height_);
@@ -418,9 +423,15 @@ int UIManager::calculateFontSize(int logical_height) const {
} else if (logical_height < 900) {
// Rango medio-alto (700-899px) → 18px
font_size = 18;
} else if (logical_height < 1200) {
// Rango alto (900-1199px): 900→22, 1080→27, 1199→29
font_size = logical_height / 40;
} else if (logical_height < 1600) {
// Rango muy alto (1200-1599px): 1200→25, 1440→30
font_size = logical_height / 48;
} else {
// Rango alto: proporcional (1080px→42, 1440px→55, 2160px→72)
font_size = logical_height / 26;
// Rango ultra (>=1600px): 1600→26, 2000→33, 2160→36
font_size = logical_height / 60;
}
// Aplicar límites: mínimo 9px, máximo 72px

View File

@@ -111,8 +111,9 @@ class UIManager {
* @brief Actualiza tamaño físico de ventana (cambios de fullscreen)
* @param width Nuevo ancho físico
* @param height Nuevo alto físico
* @param logical_height Nuevo alto lógico (0 = sin cambio)
*/
void updatePhysicalWindowSize(int width, int height);
void updatePhysicalWindowSize(int width, int height, int logical_height = 0);
// === Getters ===