diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a345f3..3eeb7ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,19 @@ # CMakeLists.txt cmake_minimum_required(VERSION 3.10) -project(coffee_crisis VERSION 1.00) + +# La versió de l'app es defineix una sola vegada a source/utils/defines.hpp +# (Defines::VERSION). El Makefile ja la grepeja per als noms de release; aqui +# l'extreiem perque project(... VERSION ...) i tots els consumidors interns +# de CMake (CPack, install, etc.) usin la mateixa font de veritat. +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/source/utils/defines.hpp" _DEFINES_CONTENT) +string(REGEX MATCH "VERSION = \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _ "${_DEFINES_CONTENT}") +set(APP_VERSION "${CMAKE_MATCH_1}") +if(APP_VERSION STREQUAL "") + message(FATAL_ERROR "No s'ha pogut extreure VERSION de source/utils/defines.hpp") +endif() + +project(coffee_crisis VERSION ${APP_VERSION}) # Tipus de build per defecte (Debug) si no se n'ha especificat cap if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index 805b825..8f28983 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -7,6 +7,8 @@ #include "core/rendering/notifications.hpp" #include "core/rendering/screen.h" #include "game/options.hpp" +#include "utils/defines.hpp" +#include "version.h" namespace GlobalInputs { @@ -54,6 +56,23 @@ namespace GlobalInputs { Notifications::show(MSG, Notifications::Palette::SUCCESS, Notifications::STANDARD_MS); } + void notifyVersion() { + // Format: " v ()" + const std::string MSG = std::string(Version::APP_NAME) + " v" + Texts::VERSION + " (" + Version::GIT_HASH + ")"; + Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::LONG_MS); + } + + void notifyVSync() { + const std::string STATE = Options::video.vsync ? "ON" : "OFF"; + const std::string MSG = std::string("VSync ") + STATE; + Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::STANDARD_MS); + } + + void notifyPresentationMode() { + const std::string MSG = std::string("Mode ") + Screen::getPresentationModeName(); + Notifications::show(MSG, Notifications::Palette::CHOICE, Notifications::STANDARD_MS); + } + void onExit() { const Uint32 NOW = SDL_GetTicks(); if (NOW < exit_window_until_ticks) { @@ -94,6 +113,24 @@ namespace GlobalInputs { notifyShaderEnabled(); return true; } + if (Input::get()->checkInput(Input::Action::SHOW_VERSION, Input::Repeat::OFF)) { + notifyVersion(); + return true; + } + if (Input::get()->checkInput(Input::Action::TOGGLE_VSYNC, Input::Repeat::OFF)) { + Screen::get()->toggleVSync(); + notifyVSync(); + return true; + } + if (Input::get()->checkInput(Input::Action::NEXT_PRESENTATION_MODE, Input::Repeat::OFF)) { + Screen::get()->nextPresentationMode(); + notifyPresentationMode(); + return true; + } + if (Input::get()->checkInput(Input::Action::TOGGLE_FPS, Input::Repeat::OFF)) { + Screen::get()->toggleFps(); + return true; + } // F5/F6 només actuen quan el post-procesado està actiu. if (Screen::isShaderEnabled()) { if (Input::get()->checkInput(Input::Action::TOGGLE_SHADER_TYPE, Input::Repeat::OFF)) { diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index 22e3e16..a27c57c 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -157,25 +157,45 @@ auto Input::checkGameControllerInput(Action input, Repeat repeat, int index) -> return PRESS_EDGE; } -// Comprueba si hay almenos un input activo +// Comprueba si hay almenos un input "humano" activo (moviment, ACCEPT/CANCEL, +// FIRE_*). Exclou les accions reservades a hotkeys globals (EXIT, PAUSE, +// WINDOW_*, *SHADER*) perque prémer F1-F12 o ESC no s'ha de comptar com +// "qualsevol tecla" — ningu vol saltar una intro per modificar el zoom. auto Input::checkAnyInput(Device device, int index) -> bool { if (device == Device::ANY) { index = 0; } + auto is_skippable = [](Action a) { + switch (a) { + case Action::UP: + case Action::DOWN: + case Action::LEFT: + case Action::RIGHT: + case Action::ACCEPT: + case Action::CANCEL: + case Action::FIRE_LEFT: + case Action::FIRE_CENTER: + case Action::FIRE_RIGHT: + return true; + default: + return false; + } + }; + if (device == Device::KEYBOARD || device == Device::ANY) { const bool *key_states = SDL_GetKeyboardState(nullptr); - - if (std::ranges::any_of(key_bindings_, - [key_states](const auto &key_binding) { return key_states[key_binding.scancode]; })) { - return true; + for (std::size_t i = 0; i < key_bindings_.size(); ++i) { + if (is_skippable(static_cast(i)) && key_states[key_bindings_[i].scancode]) { + return true; + } } } if (gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) { if (device == Device::GAMECONTROLLER || device == Device::ANY) { - for (auto &game_controller_binding : game_controller_bindings_) { - if (SDL_GetGamepadButton(connected_controllers_[index], game_controller_binding.button)) { + for (std::size_t i = 0; i < game_controller_bindings_.size(); ++i) { + if (is_skippable(static_cast(i)) && SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[i].button)) { return true; } } diff --git a/source/core/input/input.h b/source/core/input/input.h index 9989bb8..a6dae65 100644 --- a/source/core/input/input.h +++ b/source/core/input/input.h @@ -50,6 +50,12 @@ class Input { TOGGLE_SHADER, TOGGLE_SHADER_TYPE, + // Diagnostic / video toggles + SHOW_VERSION, + TOGGLE_VSYNC, + NEXT_PRESENTATION_MODE, + TOGGLE_FPS, + // Centinela final (usar para sizing) NUMBER_OF_INPUTS }; @@ -61,12 +67,12 @@ class Input { ~Input(); // Destructor - void update(); // Actualiza el estado del objeto + void update(); // Actualiza el estado del objeto void bindKey(Action input, SDL_Scancode code); // Asigna inputs a teclas void bindGameControllerButton(Action input, SDL_GamepadButton button); // Asigna inputs a botones del mando auto checkInput(Action input, Repeat repeat = Repeat::ON, Device device = Device::ANY, int index = 0) -> bool; // Comprueba si un input esta activo - auto checkAnyInput(Device device = Device::ANY, int index = 0) -> bool; // Comprueba si hay almenos un input activo + auto checkAnyInput(Device device = Device::ANY, int index = 0) -> bool; // Comprueba si hay almenos un input activo auto discoverGameController() -> bool; // Busca si hay un mando conectado @@ -82,9 +88,9 @@ class Input { [[nodiscard]] auto getNumControllers() const -> int; // Obten el numero de mandos conectados auto getControllerName(int index) -> std::string; // Obten el nombre de un mando de juego - void setVerbose(bool value); // Establece si ha de mostrar mensajes - void disableUntil(Disable value); // Deshabilita las entradas durante un periodo de tiempo - void enable(); // Hablita las entradas + void setVerbose(bool value); // Establece si ha de mostrar mensajes + void disableUntil(Disable value); // Deshabilita las entradas durante un periodo de tiempo + void enable(); // Hablita las entradas private: struct KeyBindings { diff --git a/source/core/rendering/notifications.cpp b/source/core/rendering/notifications.cpp index 7689612..8350ea4 100644 --- a/source/core/rendering/notifications.cpp +++ b/source/core/rendering/notifications.cpp @@ -6,13 +6,14 @@ namespace Notifications { namespace { - // Paleta pastel. Per a tunejar l'aparença només cal tocar aquí. + // Paleta semi-saturada: a mig cami entre pastel i color "pur". Manté + // contrast del outline (foscor) sense diluir el matís. // (Color no és literal type ⇒ const, no constexpr.) - const Color INFO_COLOR{0xF0, 0xE0, 0x90}; // groc trigo - const Color TOGGLE_COLOR{0xA0, 0xE0, 0xF0}; // cian gel - const Color CHOICE_COLOR{0xE0, 0xA0, 0xE0}; // rosa orquídia - const Color SUCCESS_COLOR{0xB0, 0xE6, 0xB0}; // verd menta - const Color DANGER_COLOR{0xF0, 0xA0, 0xA0}; // rosa salmó + const Color INFO_COLOR{0xF0, 0xD0, 0x40}; // groc + const Color TOGGLE_COLOR{0x60, 0xC0, 0xF0}; // cian + const Color CHOICE_COLOR{0xD0, 0x60, 0xD0}; // magenta + const Color SUCCESS_COLOR{0x70, 0xD0, 0x70}; // verd + const Color DANGER_COLOR{0xF0, 0x60, 0x60}; // vermell // Factor de foscor per a l'outline (~40% de la lluminositat del // color base): manté el matís i queda prou fosc per a contrastar diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 9c426a4..54ea974 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -169,9 +169,12 @@ void Screen::start() { // Vuelca el contenido del renderizador en pantalla void Screen::blit() { - // Dibuja la notificación activa sobre el gameCanvas antes de presentar + updateFps(); + + // Dibuja la notificación activa i, si toca, l'overlay de FPS sobre el gameCanvas SDL_SetRenderTarget(renderer_, game_canvas_); renderNotification(); + renderFps(); #ifndef NO_SHADERS // Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb @@ -326,16 +329,27 @@ void Screen::detectMaxZoom() { #endif } -// Establece el escalado entero -void Screen::setIntegerScale(bool enabled) { - if (Options::video.integer_scale == enabled) { return; } - Options::video.integer_scale = enabled; +// Estableix el mode de presentacio del canvas i reaplica el layout +void Screen::setPresentationMode(Options::PresentationMode mode) { + if (Options::video.presentation_mode == mode) { return; } + Options::video.presentation_mode = mode; +#ifndef NO_SHADERS + if (shader_backend_) { + shader_backend_->setPresentationMode(static_cast(mode)); + } +#endif setVideoMode(Options::video.fullscreen); } -// Alterna el escalado entero -void Screen::toggleIntegerScale() { - setIntegerScale(!Options::video.integer_scale); +// Cicla integer_scale -> letterbox -> stretched -> overscan -> integer_scale +void Screen::nextPresentationMode() { + setPresentationMode(Options::nextPresentationMode(Options::video.presentation_mode)); +} + +// Nom curt del mode actual (per a notificacions). Static perque no necessita +// estat d'instancia: nomes consulta Options::video. +auto Screen::getPresentationModeName() -> const char * { + return Options::presentationModeToString(Options::video.presentation_mode); } // Establece el V-Sync @@ -407,39 +421,75 @@ void Screen::applyFullscreenLayout() { computeFullscreenGameRect(); } -// Calcula el rectángulo dest para fullscreen: integer_scale / aspect ratio +// Calcula el rectangle dest segons el PresentationMode actiu. +// INTEGER_SCALE: x sencera maxima (1x, 2x, 3x...) centrada amb barres. +// LETTERBOX: mante aspect ratio, ajusta al menor dels eixos, barres. +// STRETCHED: omple tota la finestra deformant la relacio d'aspecte. +// OVERSCAN: mante aspect ratio omplint la finestra (retalla el sobrant). void Screen::computeFullscreenGameRect() { - if (Options::video.integer_scale) { - // Calcula el tamaño de la escala máxima - int scale = 0; - while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) { - scale++; - } + const float CANVAS_RATIO = static_cast(game_canvas_width_) / static_cast(game_canvas_height_); + const float WINDOW_RATIO = static_cast(window_width_) / static_cast(window_height_); - dest_.w = game_canvas_width_ * scale; - dest_.h = game_canvas_height_ * scale; - dest_.x = (window_width_ - dest_.w) / 2; - dest_.y = (window_height_ - dest_.h) / 2; - } else { - // Manté la relació d'aspecte sense escalat enter (letterbox/pillarbox). - float ratio = (float)game_canvas_width_ / (float)game_canvas_height_; - if ((window_width_ - game_canvas_width_) >= (window_height_ - game_canvas_height_)) { - dest_.h = window_height_; - dest_.w = static_cast(std::lround(window_height_ * ratio)); - dest_.x = (window_width_ - dest_.w) / 2; - dest_.y = (window_height_ - dest_.h) / 2; - } else { + switch (Options::video.presentation_mode) { + case Options::PresentationMode::INTEGER_SCALE: { + int scale = 0; + while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) { + scale++; + } + dest_.w = game_canvas_width_ * scale; + dest_.h = game_canvas_height_ * scale; + break; + } + case Options::PresentationMode::LETTERBOX: { + if (WINDOW_RATIO >= CANVAS_RATIO) { + dest_.h = window_height_; + dest_.w = static_cast(std::lround(window_height_ * CANVAS_RATIO)); + } else { + dest_.w = window_width_; + dest_.h = static_cast(std::lround(window_width_ / CANVAS_RATIO)); + } + break; + } + case Options::PresentationMode::STRETCHED: { dest_.w = window_width_; - dest_.h = static_cast(std::lround(window_width_ / ratio)); - dest_.x = (window_width_ - dest_.w) / 2; - dest_.y = (window_height_ - dest_.h) / 2; + dest_.h = window_height_; + break; + } + case Options::PresentationMode::OVERSCAN: { + // Mante aspect: dimensiona al major dels eixos (l'altre desborda). + if (WINDOW_RATIO >= CANVAS_RATIO) { + dest_.w = window_width_; + dest_.h = static_cast(std::lround(window_width_ / CANVAS_RATIO)); + } else { + dest_.h = window_height_; + dest_.w = static_cast(std::lround(window_height_ * CANVAS_RATIO)); + } + break; } } + dest_.x = (window_width_ - dest_.w) / 2; + dest_.y = (window_height_ - dest_.h) / 2; } -// Aplica la logical presentation y persiste el estado en options +// Aplica la logical presentation segons el PresentationMode actiu (ruta SDL_Renderer fallback). +// La ruta GPU calcula el viewport ella mateixa via computeViewport(). void Screen::applyLogicalPresentation(bool fullscreen) { - SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, SDL_LOGICAL_PRESENTATION_LETTERBOX); + SDL_RendererLogicalPresentation lp = SDL_LOGICAL_PRESENTATION_LETTERBOX; + switch (Options::video.presentation_mode) { + case Options::PresentationMode::INTEGER_SCALE: + lp = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; + break; + case Options::PresentationMode::LETTERBOX: + lp = SDL_LOGICAL_PRESENTATION_LETTERBOX; + break; + case Options::PresentationMode::STRETCHED: + lp = SDL_LOGICAL_PRESENTATION_STRETCH; + break; + case Options::PresentationMode::OVERSCAN: + lp = SDL_LOGICAL_PRESENTATION_OVERSCAN; + break; + } + SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, lp); Options::video.fullscreen = fullscreen; } @@ -461,14 +511,16 @@ void Screen::clearNotification() { notification_message_.clear(); } -// Dibuja la notificación activa (si la hay) sobre el gameCanvas +// Dibuja la notificación activa (si la hay) sobre el gameCanvas. La Y es +// el `notification_y_` configurat, desplacat cap avall si en overscan part +// de la franja superior queda fora de pantalla. void Screen::renderNotification() { if (notification_text_ == nullptr || SDL_GetTicks() >= notification_end_time_) { return; } notification_text_->writeDX(Text::FLAG_CENTER | Text::FLAG_COLOR | Text::FLAG_STROKE, game_canvas_width_ / 2, - notification_y_, + notification_y_ + safeNotificationY(), notification_message_, 1, notification_text_color_, @@ -476,6 +528,70 @@ void Screen::renderNotification() { notification_outline_color_); } +// Alterna la visibilitat de l'overlay de FPS. No persisteix a config. +void Screen::toggleFps() { + fps_visible_ = !fps_visible_; + if (fps_visible_) { + fps_window_start_ticks_ = SDL_GetTicks(); + fps_frame_count_ = 0; + fps_value_ = 0; + } +} + +auto Screen::isFpsVisible() const -> bool { + return fps_visible_; +} + +// Acumula frames i recalcula el FPS cada segon real (no afectat per dt del joc). +// Cridat des de blit() una vegada per frame. +void Screen::updateFps() { + if (!fps_visible_) { return; } + ++fps_frame_count_; + const Uint32 NOW = SDL_GetTicks(); + const Uint32 ELAPSED = NOW - fps_window_start_ticks_; + if (ELAPSED >= 1000) { + fps_value_ = static_cast((static_cast(fps_frame_count_) * 1000) / ELAPSED); + fps_frame_count_ = 0; + fps_window_start_ticks_ = NOW; + } +} + +// Dibuixa "NN FPS" a dalt a la dreta del canvas, mateixa Y que les notificacions +// (incloent l'ajust per overscan) i amb la mateixa font 8bithud. +void Screen::renderFps() { + if (!fps_visible_ || notification_text_ == nullptr) { return; } + constexpr int RIGHT_MARGIN = 2; + const Color FPS_COLOR{0x60, 0xD0, 0x70}; // verd (mateix to que SUCCESS de notificacions) + const Color FPS_OUTLINE{0x26, 0x53, 0x2C}; // ~40% darken del verd + const std::string MSG = std::to_string(fps_value_) + " FPS"; + const int TEXT_W = notification_text_->lenght(MSG, 1); + const int X = game_canvas_width_ - TEXT_W - RIGHT_MARGIN; + const int Y = notification_y_ + safeNotificationY(); + notification_text_->writeDX(Text::FLAG_COLOR | Text::FLAG_STROKE, + X, + Y, + MSG, + 1, + FPS_COLOR, + 1, + FPS_OUTLINE); +} + +// Y minima del canvas visible. Solo no zero quan estem en overscan i l'aspect +// de finestra obliga a escalar mes ample que alt (el canvas vertical desborda +// i la franja superior es retalla). En cas contrari (qualsevol altre mode, o +// overscan amb retall horitzontal nomes), retorna 0. +auto Screen::safeNotificationY() const -> int { + if (Options::video.presentation_mode != Options::PresentationMode::OVERSCAN) { return 0; } + if (window_width_ <= 0 || window_height_ <= 0 || game_canvas_height_ <= 0) { return 0; } + const float CANVAS_RATIO = static_cast(game_canvas_width_) / static_cast(game_canvas_height_); + const float WINDOW_RATIO = static_cast(window_width_) / static_cast(window_height_); + if (WINDOW_RATIO < CANVAS_RATIO) { return 0; } // retall horitzontal nomes + const float OVERSCAN_SCALE = static_cast(window_width_) / static_cast(game_canvas_width_); + const float VH = static_cast(game_canvas_height_) * OVERSCAN_SCALE; + return static_cast(std::ceil((VH - static_cast(window_height_)) / (2.0F * OVERSCAN_SCALE))); +} + // ============================================================================ // Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al // principi del fitxer i l'anonymous namespace amb els callbacks natius). @@ -545,7 +661,7 @@ void Screen::initShaders() { } } if (shader_backend_->isHardwareAccelerated()) { - shader_backend_->setScaleMode(Options::video.integer_scale); + shader_backend_->setPresentationMode(static_cast(Options::video.presentation_mode)); shader_backend_->setVSync(Options::video.vsync); // Resol els índexs de preset a partir del nom emmagatzemat al config. diff --git a/source/core/rendering/screen.h b/source/core/rendering/screen.h index 52540df..568acce 100644 --- a/source/core/rendering/screen.h +++ b/source/core/rendering/screen.h @@ -6,7 +6,8 @@ #include // for string #include // for vector -#include "utils/utils.h" // for Color +#include "game/options.hpp" // for Options::PresentationMode +#include "utils/utils.h" // for Color #ifndef NO_SHADERS #include "core/rendering/shader_backend.hpp" // for Rendering::ShaderType @@ -51,17 +52,18 @@ class Screen { void blit(); // Vuelca el contenido del renderizador en pantalla // Video y ventana - void setVideoMode(bool fullscreen); // Establece el modo de video - void toggleVideoMode(); // Cambia entre pantalla completa y ventana - void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten - static void syncFullscreenFlagFromBrowser(bool is_fullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten - void toggleIntegerScale(); // Alterna el escalado entero - void setIntegerScale(bool enabled); // Establece el escalado entero - void toggleVSync(); // Alterna el V-Sync - void setVSync(bool enabled); // Establece el V-Sync - auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió) - auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió) - auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió) + void setVideoMode(bool fullscreen); // Establece el modo de video + void toggleVideoMode(); // Cambia entre pantalla completa y ventana + void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten + static void syncFullscreenFlagFromBrowser(bool is_fullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten + void nextPresentationMode(); // Cicla integer_scale -> letterbox -> stretched -> overscan + void setPresentationMode(Options::PresentationMode mode); // Estableix el mode de presentacio del canvas + [[nodiscard]] static auto getPresentationModeName() -> const char *; // Nom curt del mode actual (per a notificacions) + void toggleVSync(); // Alterna el V-Sync + void setVSync(bool enabled); // Establece el V-Sync + auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió) + auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió) + auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió) // Borde void setBorderColor(Color color); // Cambia el color del borde @@ -71,6 +73,10 @@ class Screen { void notify(const std::string &text, Color text_color, Color outline_color, Uint32 duration_ms); // Muestra una notificación en la línea superior del canvas durante durationMs. Sobrescribe cualquier notificación activa (sin apilación). void clearNotification(); // Limpia la notificación actual + // FPS overlay (debug, no persistent) + void toggleFps(); // Alterna la visibilitat de l'overlay de FPS + [[nodiscard]] auto isFpsVisible() const -> bool; // Estat actual + // GPU / shaders (post-procesado). En builds con NO_SHADERS (Emscripten) son no-op. void initShaders(); // Crea el backend GPU si no existe y lo inicializa void shutdownShaders(); // Libera el backend GPU @@ -110,7 +116,12 @@ class Screen { void registerEmscriptenEventCallbacks(); // Registra los callbacks nativos de Emscripten para fullscreenchange y orientationchange. No-op fuera de Emscripten // Notificaciones - void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas + void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas + [[nodiscard]] auto safeNotificationY() const -> int; // Y minima dins del canvas que segueix sent visible en overscan (segons aspect ratio finestra/canvas) + + // FPS overlay + void updateFps(); // Acumula temps i recalcula fps cada segon (a cridar des de blit) + void renderFps(); // Dibuixa "NN FPS" a dalt a la dreta del canvas #ifndef NO_SHADERS // Aplica els paràmetres actuals del shader al backend segons options @@ -139,6 +150,12 @@ class Screen { Uint32 notification_end_time_; // SDL_GetTicks() hasta el cual se muestra int notification_y_; // Fila vertical en el canvas virtual + // FPS overlay (debug, no persistent) + bool fps_visible_{false}; // F10 toggle + int fps_value_{0}; // Ultim valor calculat (frames per segon) + int fps_frame_count_{0}; // Frames acumulats durant la finestra actual + Uint32 fps_window_start_ticks_{0}; // Inici de la finestra d'1s actual (SDL_GetTicks) + #ifndef NO_SHADERS // GPU / shaders std::unique_ptr shader_backend_; // Backend GPU (nullptr si inactivo) diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index 1cfdf7f..814be2d 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -292,21 +292,48 @@ namespace Rendering { // computeViewport — dimensions lògiques del canvas dins del swapchain (letterbox) // --------------------------------------------------------------------------- auto SDL3GPUShader::computeViewport(Uint32 sw, Uint32 sh) const -> Viewport { + const auto SWF = static_cast(sw); + const auto SHF = static_cast(sh); + const float CANVAS_RATIO = static_cast(game_width_) / static_cast(game_height_); + const float WINDOW_RATIO = SWF / SHF; + float vw = 0.0F; float vh = 0.0F; - if (integer_scale_) { - const int SCALE = std::max(1, std::min(static_cast(sw) / game_width_, static_cast(sh) / game_height_)); - vw = static_cast(game_width_ * SCALE); - vh = static_cast(game_height_ * SCALE); - } else { - const float SCALE = std::min( - static_cast(sw) / static_cast(game_width_), - static_cast(sh) / static_cast(game_height_)); - vw = static_cast(game_width_) * SCALE; - vh = static_cast(game_height_) * SCALE; + switch (presentation_mode_) { + case PresentationMode::INTEGER_SCALE: { + const int SCALE = std::max(1, std::min(static_cast(sw) / game_width_, static_cast(sh) / game_height_)); + vw = static_cast(game_width_ * SCALE); + vh = static_cast(game_height_ * SCALE); + break; + } + case PresentationMode::LETTERBOX: { + if (WINDOW_RATIO >= CANVAS_RATIO) { + vh = SHF; + vw = SHF * CANVAS_RATIO; + } else { + vw = SWF; + vh = SWF / CANVAS_RATIO; + } + break; + } + case PresentationMode::STRETCHED: { + vw = SWF; + vh = SHF; + break; + } + case PresentationMode::OVERSCAN: { + if (WINDOW_RATIO >= CANVAS_RATIO) { + vw = SWF; + vh = SWF / CANVAS_RATIO; + } else { + vh = SHF; + vw = SHF * CANVAS_RATIO; + } + break; + } } - const float VX = std::floor((static_cast(sw) - vw) * 0.5F); - const float VY = std::floor((static_cast(sh) - vh) * 0.5F); + const float VX = std::floor((SWF - vw) * 0.5F); + const float VY = std::floor((SHF - vh) * 0.5F); return {.x = VX, .y = VY, .w = vw, .h = vh}; } @@ -569,8 +596,8 @@ namespace Rendering { } } - void SDL3GPUShader::setScaleMode(bool integer_scale) { - integer_scale_ = integer_scale; + void SDL3GPUShader::setPresentationMode(PresentationMode mode) { + presentation_mode_ = mode; } // --------------------------------------------------------------------------- diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index bd7e353..be38b6e 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -97,8 +97,8 @@ namespace Rendering { // Activa/desactiva VSync en el swapchain void setVSync(bool vsync) override; - // Activa/desactiva escalado entero (integer scale) - void setScaleMode(bool integer_scale) override; + // Estableix el mode de presentacio del canvas + void setPresentationMode(PresentationMode mode) override; // Selecciona el shader de post-procesado activo (POSTFX o CRTPI) void setActiveShader(ShaderType type) override; @@ -195,7 +195,7 @@ namespace Rendering { std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige) bool is_initialized_ = false; bool vsync_ = true; - bool integer_scale_ = false; + PresentationMode presentation_mode_ = PresentationMode::INTEGER_SCALE; }; } // namespace Rendering diff --git a/source/core/rendering/shader_backend.hpp b/source/core/rendering/shader_backend.hpp index 7414aaa..6bd48b8 100644 --- a/source/core/rendering/shader_backend.hpp +++ b/source/core/rendering/shader_backend.hpp @@ -112,9 +112,16 @@ namespace Rendering { virtual void setVSync(bool /*vsync*/) {} /** - * @brief Activa o desactiva el escalado entero (integer scale) + * @brief Estableix el mode de presentacio del canvas dins del swapchain. + * El backend calcula el viewport en consequencia. */ - virtual void setScaleMode(bool /*integer_scale*/) {} + enum class PresentationMode : std::uint8_t { + INTEGER_SCALE, + LETTERBOX, + STRETCHED, + OVERSCAN + }; + virtual void setPresentationMode(PresentationMode /*mode*/) {} /** * @brief Verifica si el backend está usando aceleración por hardware diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index d077248..66fc844 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -232,6 +232,10 @@ void Director::initInput() { Input::get()->bindKey(Input::Action::TOGGLE_SHADER, SDL_SCANCODE_F4); Input::get()->bindKey(Input::Action::TOGGLE_SHADER_TYPE, SDL_SCANCODE_F5); Input::get()->bindKey(Input::Action::NEXT_SHADER_PRESET, SDL_SCANCODE_F6); + Input::get()->bindKey(Input::Action::TOGGLE_VSYNC, SDL_SCANCODE_F7); + Input::get()->bindKey(Input::Action::NEXT_PRESENTATION_MODE, SDL_SCANCODE_F8); + Input::get()->bindKey(Input::Action::TOGGLE_FPS, SDL_SCANCODE_F10); + Input::get()->bindKey(Input::Action::SHOW_VERSION, SDL_SCANCODE_F11); // Mando - Movimiento del jugador Input::get()->bindGameControllerButton(Input::Action::UP, SDL_GAMEPAD_BUTTON_DPAD_UP); diff --git a/source/game/defaults.hpp b/source/game/defaults.hpp index d78bd50..fefd861 100644 --- a/source/game/defaults.hpp +++ b/source/game/defaults.hpp @@ -19,7 +19,8 @@ namespace Defaults::Video { constexpr SDL_ScaleMode SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST; constexpr bool FULLSCREEN = false; constexpr bool VSYNC = true; - constexpr bool INTEGER_SCALE = true; + // INTEGER_SCALE eliminat: ara es part de PresentationMode (a options.hpp). + // El default es defineix literal alli: PresentationMode::INTEGER_SCALE. constexpr bool GPU_ACCELERATION = true; constexpr const char *GPU_PREFERRED_DRIVER = ""; constexpr bool SHADER_ENABLED = false; diff --git a/source/game/options.cpp b/source/game/options.cpp index 16cc1d2..165c3c2 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -31,6 +31,42 @@ namespace Options { std::string crtpi_file_path; int current_crtpi_preset = 0; + // Conversions PresentationMode <-> string per a config.yaml i UI + auto presentationModeToString(PresentationMode m) -> const char * { + switch (m) { + case PresentationMode::INTEGER_SCALE: + return "integer_scale"; + case PresentationMode::LETTERBOX: + return "letterbox"; + case PresentationMode::STRETCHED: + return "stretched"; + case PresentationMode::OVERSCAN: + return "overscan"; + } + return "integer_scale"; + } + + auto presentationModeFromString(const std::string &s) -> PresentationMode { + if (s == "letterbox") { return PresentationMode::LETTERBOX; } + if (s == "stretched") { return PresentationMode::STRETCHED; } + if (s == "overscan") { return PresentationMode::OVERSCAN; } + return PresentationMode::INTEGER_SCALE; + } + + auto nextPresentationMode(PresentationMode m) -> PresentationMode { + switch (m) { + case PresentationMode::INTEGER_SCALE: + return PresentationMode::LETTERBOX; + case PresentationMode::LETTERBOX: + return PresentationMode::STRETCHED; + case PresentationMode::STRETCHED: + return PresentationMode::OVERSCAN; + case PresentationMode::OVERSCAN: + return PresentationMode::INTEGER_SCALE; + } + return PresentationMode::INTEGER_SCALE; + } + // Lectura tolerant d'un camp YAML: assigna a `target` el valor del camp // si existeix i el tipus encaixa. Si la clau no hi és o el tipus YAML // no és compatible amb T, conserva el valor previ de `target` (default). @@ -82,7 +118,16 @@ namespace Options { parseBoolField(vid, "fullscreen", video.fullscreen); parseBoolField(vid, "vsync", video.vsync); - parseBoolField(vid, "integer_scale", video.integer_scale); + // presentation_mode (nou): prefereix string explicit; cau a integer_scale legacy (bool) si no. + std::string pm_str; + if (tryGet(vid, "presentation_mode", pm_str)) { + video.presentation_mode = presentationModeFromString(pm_str); + } else { + bool legacy_integer_scale = true; + if (tryGet(vid, "integer_scale", legacy_integer_scale)) { + video.presentation_mode = legacy_integer_scale ? PresentationMode::INTEGER_SCALE : PresentationMode::LETTERBOX; + } + } int scale_mode_int = static_cast(video.scale_mode); if (tryGet(vid, "scale_mode", scale_mode_int)) { video.scale_mode = static_cast(scale_mode_int); @@ -197,7 +242,7 @@ namespace Options { // En Emscripten la ventana la gestiona el navegador window.zoom = 4; video.fullscreen = false; - video.integer_scale = true; + video.presentation_mode = PresentationMode::INTEGER_SCALE; #endif // Dispositius d'entrada per defecte @@ -283,7 +328,8 @@ namespace Options { file << "video:\n"; file << " fullscreen: " << boolToString(video.fullscreen) << "\n"; file << " vsync: " << boolToString(video.vsync) << "\n"; - file << " integer_scale: " << boolToString(video.integer_scale) << "\n"; + file << " presentation_mode: " << presentationModeToString(video.presentation_mode) + << " # integer_scale | letterbox | stretched | overscan\n"; file << " scale_mode: " << static_cast(video.scale_mode) << " # " << static_cast(SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast(SDL_SCALEMODE_LINEAR) << ": linear\n"; diff --git a/source/game/options.hpp b/source/game/options.hpp index bcf0c11..5dd73e7 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -18,6 +18,16 @@ namespace Options { + // Modes de presentacio del canvas virtual a la finestra. Tot fullscreen i + // windowed amb zoom no-fit el respecta; en windowed amb zoom exacte (1x/2x/3x) + // l'efecte es null perque el canvas ja encaixa amb la finestra. + enum class PresentationMode : std::uint8_t { + INTEGER_SCALE, // Multiple enter (1x, 2x, 3x...), centrat, amb barres + LETTERBOX, // Mante aspect ratio, centrat, amb barres + STRETCHED, // Omple tota la finestra, deforma l'aspect ratio + OVERSCAN // Mante aspect ratio i omple la finestra retallant el contingut fora + }; + struct Window { std::string caption = Defaults::Window::CAPTION; int zoom = Defaults::Window::ZOOM; @@ -42,11 +52,16 @@ namespace Options { SDL_ScaleMode scale_mode = Defaults::Video::SCALE_MODE; bool fullscreen = Defaults::Video::FULLSCREEN; bool vsync = Defaults::Video::VSYNC; - bool integer_scale = Defaults::Video::INTEGER_SCALE; + PresentationMode presentation_mode = PresentationMode::INTEGER_SCALE; GPU gpu; ShaderConfig shader; }; + // Conversions string <-> PresentationMode per a config.yaml i notificacions + auto presentationModeToString(PresentationMode m) -> const char *; + auto presentationModeFromString(const std::string &s) -> PresentationMode; + auto nextPresentationMode(PresentationMode m) -> PresentationMode; + struct Music { bool enabled = Defaults::Music::ENABLED; float volume = Defaults::Music::VOLUME; diff --git a/source/game/scenes/instructions.cpp b/source/game/scenes/instructions.cpp index 4f6fb79..e4d540a 100644 --- a/source/game/scenes/instructions.cpp +++ b/source/game/scenes/instructions.cpp @@ -239,7 +239,7 @@ void Instructions::checkInput() { // pulsació; el quit es propaga via Director::iterate. if (GlobalInputs::handle()) { return; } - if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) { + if (Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) { if (mode_ == Mode::AUTO) { finished_ = true; } else { diff --git a/source/game/scenes/intro.cpp b/source/game/scenes/intro.cpp index 0aa3082..b019e7f 100644 --- a/source/game/scenes/intro.cpp +++ b/source/game/scenes/intro.cpp @@ -197,7 +197,7 @@ void Intro::checkInput() { // pulsació; el quit es propaga via Director::iterate. if (GlobalInputs::handle()) { return; } - if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) { + if (Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) { Audio::get()->stopMusic(); section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_1; diff --git a/source/game/scenes/logo.cpp b/source/game/scenes/logo.cpp index 877e3f5..e8f3cb9 100644 --- a/source/game/scenes/logo.cpp +++ b/source/game/scenes/logo.cpp @@ -65,7 +65,7 @@ void Logo::checkInput() { // pulsació; el quit es propaga via Director::iterate. if (GlobalInputs::handle()) { return; } - if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) { + if (Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_1; } diff --git a/source/game/scenes/title.cpp b/source/game/scenes/title.cpp index b9f2911..f6c8828 100644 --- a/source/game/scenes/title.cpp +++ b/source/game/scenes/title.cpp @@ -952,7 +952,16 @@ void Title::handleEvent(const SDL_Event *event) { } if (section_->subsection == SUBSECTION_TITLE_3) { - if ((event->type == SDL_EVENT_KEY_UP) || (event->type == SDL_EVENT_JOYSTICK_BUTTON_UP)) { + // Activa menu i reinicia el countdown de demo nomes amb tecles "humanes". + // F1-F12 i ESC son hotkeys globals (zoom, fullscreen, shaders, exit, version) + // i no han d'interferir amb el flux de l'UI del titol. + bool human_input = (event->type == SDL_EVENT_JOYSTICK_BUTTON_UP); + if (event->type == SDL_EVENT_KEY_UP) { + const SDL_Scancode S = event->key.scancode; + const bool IS_RESERVED = (S == SDL_SCANCODE_ESCAPE) || (S >= SDL_SCANCODE_F1 && S <= SDL_SCANCODE_F12); + human_input = !IS_RESERVED; + } + if (human_input) { menu_visible_ = true; demo_remaining_s_ = DEMO_TIMEOUT_S; } diff --git a/source/game/scenes/title.h b/source/game/scenes/title.h index 0b303c4..7c47e3a 100644 --- a/source/game/scenes/title.h +++ b/source/game/scenes/title.h @@ -33,7 +33,7 @@ class Title { void handleEvent(const SDL_Event *event); // Procesa un evento private: - static constexpr const char *COPYRIGHT = "@2020 JailDesigner (v2.3.4)"; + static constexpr const char *COPYRIGHT = "@2020 JailDesigner"; // Time-based: temps màxim a la pantalla del títol abans de tornar al // logo o llançar el demo. 800 frames a 60Hz ⇒ 13.333 s. static constexpr float DEMO_TIMEOUT_S = 13.333F; diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index fffd22e..f51cbde 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -1,5 +1,5 @@ #pragma once namespace Texts { - constexpr const char* VERSION = "2.3.4"; // Versión del juego (también usada por el Makefile) + constexpr const char* VERSION = "2.4.0"; // Font de veritat: tambe la usen el Makefile (release names) i CMakeLists (project VERSION) } // namespace Texts