From 6f3fafbfffe531d32f9d77b423d5e1bf9326edad Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 19 Apr 2026 20:28:23 +0200 Subject: [PATCH] =?UTF-8?q?RenderInfo=20amb=20tres=20estats=20(off/dalt/ba?= =?UTF-8?q?ix),=20temps=20de=20joc=20i=20persist=C3=A8ncia;=20ombra,=20mon?= =?UTF-8?q?o-d=C3=ADgits=20i=20easing=20compartits=20(v0.14.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + data/input/keys.yaml | 5 +- source/core/input/global_inputs.cpp | 24 +-- source/core/rendering/notifier.cpp | 6 +- source/core/rendering/notifier.hpp | 3 +- source/core/rendering/render_info.cpp | 206 ++++++++++++++++++------ source/core/rendering/render_info.hpp | 46 ++++-- source/core/rendering/text.cpp | 48 ++++++ source/core/rendering/text.hpp | 6 + source/game/options.cpp | 28 +++- source/game/options.hpp | 3 +- source/game/ui/notification_presets.hpp | 16 +- source/game/ui/ui_palette.hpp | 6 +- source/utils/defaults.hpp | 27 ++-- source/utils/defines.hpp | 2 +- source/utils/easing.cpp | 26 +++ source/utils/easing.hpp | 20 +++ 17 files changed, 376 insertions(+), 97 deletions(-) create mode 100644 source/utils/easing.cpp create mode 100644 source/utils/easing.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c1c1a9c..d11614f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ set(APP_SOURCES source/core/resources/resource_helper.cpp source/core/resources/resource_cache.cpp source/core/resources/palette_loader.cpp + source/utils/easing.cpp source/utils/utils.cpp source/game/options.cpp source/game/scenes/game_scene.cpp diff --git a/data/input/keys.yaml b/data/input/keys.yaml index 3402bb0..421d48e 100644 --- a/data/input/keys.yaml +++ b/data/input/keys.yaml @@ -31,7 +31,10 @@ keys: desc: "Activa / desactiva VSync" - id: toggle_render_info code: "F12" - desc: "Mostra o amaga l'overlay de debug" + desc: "Cicle de l'overlay de debug: apagat / dalt / baix" + - id: toggle_render_time + code: "F11" + desc: "Mostra o amaga el temps de joc dins de l'overlay" - id: exit code: "Escape" desc: "Eixir (confirmar amb segona pulsació)" diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index acdf12a..7bf6d9a 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -19,7 +19,10 @@ namespace GlobalInputs { namespace { // Finestra per a confirmar l'eixida amb una segona pulsació d'Escape. - constexpr float EXIT_WINDOW_S = 1.5F; + // La lliguem a la durada de la notificació EXIT perquè el missatge i + // la confirmació expiren alhora — si no, l'usuari podria veure el + // text però ja no ser dins del rang de confirmació (o al revés). + constexpr float EXIT_WINDOW_S = static_cast(Notification::EXIT.time_ms) / 1000.0F; constexpr int MIN_ZOOM = 1; constexpr int MAX_ZOOM = 6; @@ -134,8 +137,12 @@ namespace GlobalInputs { notifyLocale(mode_key); } - void onToggleRenderInfo() { - RenderInfo::get().toggle(); + void onCycleRenderInfo() { + RenderInfo::cyclePosition(); + } + + void onToggleRenderTime() { + RenderInfo::toggleShowTime(); } void onExit() { @@ -144,12 +151,7 @@ namespace GlobalInputs { return; } exit_window_s = EXIT_WINDOW_S; - // La notificació dura el doble de la finestra; si l'usuari no - // prem una segona volta abans del timeout, es desarmarà sol. - Notifier::get().notify(Locale::get().text("input.press_again_exit"), - Notification::DEFAULT.text_color, - Notification::DEFAULT.bg_color, - static_cast(EXIT_WINDOW_S * 1000.0F)); + Notifier::get().notify(Locale::get().text("input.press_again_exit"), Notification::EXIT); } } // namespace @@ -177,7 +179,9 @@ namespace GlobalInputs { } else if (SC == KeyConfig::get().scancode("next_scaling_mode")) { onNextScalingMode(); } else if (SC == KeyConfig::get().scancode("toggle_render_info")) { - onToggleRenderInfo(); + onCycleRenderInfo(); + } else if (SC == KeyConfig::get().scancode("toggle_render_time")) { + onToggleRenderTime(); } else if (SC == KeyConfig::get().scancode("exit")) { onExit(); } diff --git a/source/core/rendering/notifier.cpp b/source/core/rendering/notifier.cpp index 02de5ae..2524677 100644 --- a/source/core/rendering/notifier.cpp +++ b/source/core/rendering/notifier.cpp @@ -25,8 +25,8 @@ void Notifier::update(int delta_ms) { switch (status_) { case Status::RISING: y_ += SLIDE_SPEED * DT; - if (y_ >= 0.0F) { - y_ = 0.0F; + if (y_ >= static_cast(MARGIN_TOP)) { + y_ = static_cast(MARGIN_TOP); status_ = Status::ACTIVE; } break; @@ -37,7 +37,7 @@ void Notifier::update(int delta_ms) { case Status::VANISHING: y_ -= SLIDE_SPEED * DT; if (y_ <= static_cast(-HEIGHT)) { - y_ = static_cast(-HEIGHT); + y_ = static_cast(-HEIGHT - MARGIN_TOP); status_ = Status::HIDDEN; } break; diff --git a/source/core/rendering/notifier.hpp b/source/core/rendering/notifier.hpp index b80f9af..7682635 100644 --- a/source/core/rendering/notifier.hpp +++ b/source/core/rendering/notifier.hpp @@ -42,6 +42,7 @@ class Notifier { static constexpr int PADDING_V = 3; static constexpr int TEXT_SIZE = 8; static constexpr int HEIGHT = TEXT_SIZE + (2 * PADDING_V); + static constexpr int MARGIN_TOP = 2; // Separació amb la vora superior del canvas. static constexpr float SLIDE_SPEED = 120.0F; private: @@ -55,7 +56,7 @@ class Notifier { std::unique_ptr text_; Status status_{Status::HIDDEN}; - float y_{static_cast(-HEIGHT)}; + float y_{static_cast(-HEIGHT - MARGIN_TOP)}; std::string current_text_; Uint8 current_text_color_{0}; Uint8 current_bg_color_{0}; diff --git a/source/core/rendering/render_info.cpp b/source/core/rendering/render_info.cpp index 8370966..b9414b8 100644 --- a/source/core/rendering/render_info.cpp +++ b/source/core/rendering/render_info.cpp @@ -1,7 +1,9 @@ #include "core/rendering/render_info.hpp" #include +#include #include +#include #include #include "core/rendering/screen.hpp" @@ -10,18 +12,33 @@ #include "game/options.hpp" #include "game/ui/ui_palette.hpp" #include "utils/defines.hpp" +#include "utils/easing.hpp" namespace { constexpr int TEXT_SIZE = 8; constexpr int PADDING_V = (RenderInfo::HEIGHT - TEXT_SIZE) / 2; + // Llindar per saltar el dibuixat d'un segment quasi totalment transparent. + // Evita una fileta d'un sol píxel quan acaba de començar a aparèixer. + constexpr float VISIBLE_EPSILON = 0.01F; + auto prettify(std::string s) -> std::string { std::ranges::replace(s, '-', ' '); std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); }); return s; } + auto formatTime(int elapsed_ms) -> std::string { + const int TOTAL_CS = elapsed_ms / 10; + const int MIN = TOTAL_CS / (60 * 100); + const int SEC = (TOTAL_CS / 100) % 60; + const int CENTI = TOTAL_CS % 100; + std::array buf{}; + std::snprintf(buf.data(), buf.size(), "%d:%02d.%02d", MIN, SEC, CENTI); + return std::string{buf.data()}; + } + } // namespace auto RenderInfo::get() -> RenderInfo& { @@ -32,80 +49,167 @@ auto RenderInfo::get() -> RenderInfo& { RenderInfo::~RenderInfo() = default; void RenderInfo::init() { - // El Director preloade la paleta "ui" i la surface "8bithud.gif"; ací - // només construïm el Text. Així Notifier i altres HUDs comparteixen - // recursos sense contenir la lògica de càrrega. auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); - // Restaura l'estat guardat a config.yaml (video.render_info). - if (Options::video.render_info) { - status_ = Status::ACTIVE; - y_ = 0.0F; + // Restaura l'estat guardat: si l'overlay estava engegat, l'activem ja + // plenament visible (sense animació d'entrada inicial). + if (Options::video.render_info_position != Defaults::RenderInfoPosition::OFF) { + visible_pos_ = Options::video.render_info_position; + info_anim_ = 1.0F; + } + + // El segment de temps pot estar activat per persistència: si la barra + // apareix ja plena, el seu segment també ha d'estar-ho. + segments_[3].mono_digits = true; + if (info_anim_ >= 1.0F) { + refreshSegments(); + for (auto& seg : segments_) { seg.anim = seg.visible ? 1.0F : 0.0F; } } } void RenderInfo::update(int delta_ms) { + elapsed_ms_ += delta_ms; const float DT = static_cast(delta_ms) / 1000.0F; - switch (status_) { - case Status::RISING: - y_ += SLIDE_SPEED * DT; - if (y_ >= 0.0F) { - y_ = 0.0F; - status_ = Status::ACTIVE; - } - break; - case Status::VANISHING: - y_ -= SLIDE_SPEED * DT; - if (y_ <= static_cast(-HEIGHT)) { - y_ = static_cast(-HEIGHT); - status_ = Status::HIDDEN; - } - break; - default: - break; + + // Transició global de posició. Si la posició desitjada coincideix amb la + // visible, entrem (anim→1). Si no, primer eixim (anim→0) del lloc actual; + // quan arriba a 0 commutem visible_pos_ i deixem que el proper frame + // dispari l'entrada. Excepció: de OFF a TOP/BOTTOM commutem en sec. + const auto DESIRED = Options::video.render_info_position; + if (DESIRED == visible_pos_) { + // Si el destí és OFF, no cal animar cap a un lloc — la barra ja ha + // d'estar fora. Sense aquest guard, en arribar de BOTTOM→OFF la + // anim tornaria a pujar cap a 1 i el render (branca BOTTOM per + // defecte) la tornaria a pintar abaix. + if (visible_pos_ == Defaults::RenderInfoPosition::OFF) { + info_anim_ = 0.0F; + } else if (info_anim_ < 1.0F) { + info_anim_ = std::min(1.0F, info_anim_ + (SLIDE_SPEED * DT)); + } + } else if (visible_pos_ == Defaults::RenderInfoPosition::OFF) { + visible_pos_ = DESIRED; + info_anim_ = 0.0F; + } else { + info_anim_ -= SLIDE_SPEED * DT; + if (info_anim_ <= 0.0F) { + info_anim_ = 0.0F; + visible_pos_ = DESIRED; + } + } + + // Decidim què ha d'eixir cada segment i fem tendir la seua anim. + refreshSegments(); + for (auto& seg : segments_) { + const float TARGET = seg.visible ? 1.0F : 0.0F; + if (seg.anim < TARGET) { + seg.anim = std::min(TARGET, seg.anim + (SEG_SPEED * DT)); + } else if (seg.anim > TARGET) { + seg.anim = std::max(TARGET, seg.anim - (SEG_SPEED * DT)); + } } } -void RenderInfo::render() { - if (status_ == Status::HIDDEN || text_ == nullptr) { return; } - - std::string line = std::to_string(Screen::get().getLastFPS()) + " fps"; +void RenderInfo::refreshSegments() { + // [0] FPS (sempre visible mentre l'overlay apareix) + segments_[0].text = std::to_string(Screen::get().getLastFPS()) + " fps"; + segments_[0].visible = true; + segments_[0].mono_digits = false; + // [1] Driver GPU (sempre visible) const auto& driver = Screen::get().getGPUDriver(); - line += " | " + (driver.empty() ? std::string("sdl") : driver); + segments_[1].text = std::string(" | ") + (driver.empty() ? std::string("sdl") : driver); + segments_[1].visible = true; + segments_[1].mono_digits = false; + // [2] Shader + preset (només amb shader activat) if (Options::video.shader.enabled) { const std::string& shader = Options::video.shader.current_shader; const std::string& preset = (shader == "crtpi") ? Options::video.shader.current_crtpi_preset : Options::video.shader.current_postfx_preset; - line += " | " + shader + " " + prettify(preset); + segments_[2].text = " | " + shader + " " + prettify(preset); } + segments_[2].visible = Options::video.shader.enabled; + segments_[2].mono_digits = false; - line = prettify(line); + // [3] Temps de joc (togglejable i persistent) + segments_[3].text = " | " + formatTime(elapsed_ms_); + segments_[3].visible = Options::video.render_info_show_time; + segments_[3].mono_digits = true; - text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, - GameCanvas::CENTER_X, - static_cast(y_) + PADDING_V, - line, - 1, - UiColor::RENDER_INFO_TEXT); + // Els tres primers segments baixem a minúscules per coherència estètica + // amb la resta de la línia; el [3] (temps) manté dígits i puntuació. + segments_[0].text = prettify(segments_[0].text); + segments_[1].text = prettify(segments_[1].text); + segments_[2].text = prettify(segments_[2].text); } -void RenderInfo::toggle() { - switch (status_) { - case Status::HIDDEN: - status_ = Status::RISING; - Options::video.render_info = true; - Options::saveToFile(); - break; - case Status::ACTIVE: - status_ = Status::VANISHING; - Options::video.render_info = false; - Options::saveToFile(); - break; - default: - break; +auto RenderInfo::segmentWidth(const Segment& seg, int digit_cell_w) const -> int { + if (text_ == nullptr || seg.text.empty()) { return 0; } + return seg.mono_digits + ? text_->lengthMonoDigits(seg.text, digit_cell_w) + : text_->length(seg.text); +} + +void RenderInfo::render() { + if (text_ == nullptr) { return; } + if (visible_pos_ == Defaults::RenderInfoPosition::OFF && info_anim_ <= 0.0F) { return; } + + const int DIGIT_CELL = text_->getCharacterSize() - 1; + + // Amplada total considerant animació individual: cada segment contribueix + // menys quan està apareixent o desapareixent, per a que el conjunt + // s'expandeixca/es contraga suaument. + float total_w = 0.0F; + for (const auto& seg : segments_) { + if (seg.anim <= 0.0F || seg.text.empty()) { continue; } + const int W = segmentWidth(seg, DIGIT_CELL); + total_w += static_cast(W) * Easing::outQuad(seg.anim); + } + if (total_w <= 0.0F) { return; } + + // Y final/inicial segons posició. El lliscament va de (fora) a (dins) + // interpolat amb outQuad de la anim global. + const int CH = TEXT_SIZE; + int final_y = 0; + int start_y = 0; + if (visible_pos_ == Defaults::RenderInfoPosition::TOP) { + final_y = PADDING_V; + start_y = -CH - 1; + } else { + final_y = GameCanvas::HEIGHT - CH - PADDING_V - 1; + start_y = GameCanvas::HEIGHT; + } + const int Y = start_y + static_cast(static_cast(final_y - start_y) * Easing::outQuad(info_anim_)); + + // Centrat horitzontal sobre l'amplada animada. + float cur_x = (static_cast(GameCanvas::WIDTH) - total_w) / 2.0F; + for (const auto& seg : segments_) { + if (seg.anim <= VISIBLE_EPSILON || seg.text.empty()) { continue; } + const int XI = static_cast(cur_x); + const int SEG_W = segmentWidth(seg, DIGIT_CELL); + + if (seg.mono_digits) { + text_->writeMonoDigits(XI + 1, Y + 1, seg.text, UiColor::RENDER_INFO_SHADOW, DIGIT_CELL); + text_->writeMonoDigits(XI, Y, seg.text, UiColor::RENDER_INFO_TEXT, DIGIT_CELL); + } else { + text_->writeColored(XI + 1, Y + 1, seg.text, UiColor::RENDER_INFO_SHADOW); + text_->writeColored(XI, Y, seg.text, UiColor::RENDER_INFO_TEXT); + } + + cur_x += static_cast(SEG_W) * Easing::outQuad(seg.anim); } } + +void RenderInfo::cyclePosition() { + const auto CURRENT = Options::video.render_info_position; + const int NEXT = (static_cast(CURRENT) + 1) % 3; + Options::video.render_info_position = static_cast(NEXT); + Options::saveToFile(); +} + +void RenderInfo::toggleShowTime() { + Options::video.render_info_show_time = !Options::video.render_info_show_time; + Options::saveToFile(); +} diff --git a/source/core/rendering/render_info.hpp b/source/core/rendering/render_info.hpp index f89c0ca..193f5f3 100644 --- a/source/core/rendering/render_info.hpp +++ b/source/core/rendering/render_info.hpp @@ -1,13 +1,19 @@ #pragma once +#include #include #include +#include + +#include "utils/defaults.hpp" class Text; -// Overlay de debug que mostra FPS, driver GPU i shader + preset actiu. Es -// dibuixa a la part superior de la pantalla amb una animació de lliscament -// (RISING / ACTIVE / VANISHING / HIDDEN). L'activa la tecla F3 a l'escena. +// Overlay de debug amb tres estats (OFF / TOP / BOTTOM). Mostra una barra +// d'info dividida en segments independents (fps, driver, shader, temps) que +// apareixen i desapareixen suaument. La posició es commuta amb una tecla +// (cicle OFF → TOP → BOTTOM) i el segment de temps es pot mostrar o amagar +// per separat; les dos opcions persisteixen a config.yaml. class RenderInfo { public: static auto get() -> RenderInfo&; @@ -17,29 +23,41 @@ class RenderInfo { auto operator=(const RenderInfo&) -> RenderInfo& = delete; auto operator=(RenderInfo&&) -> RenderInfo& = delete; - // Carrega la font 8bithud. S'ha de cridar després de configurar Screen. void init(); void update(int delta_ms); void render(); - void toggle(); - [[nodiscard]] auto isActive() const -> bool { return status_ != Status::HIDDEN; } + // Avança la posició OFF → TOP → BOTTOM → OFF i persisteix. + static void cyclePosition(); + + // Mostra/oculta el segment de temps i persisteix. Només té efecte visual + // quan la barra és visible (TOP/BOTTOM). + static void toggleShowTime(); - // Alçada fixa de l'overlay: TEXT_SIZE (8) + padding vertical. static constexpr int HEIGHT = 10; - static constexpr float SLIDE_SPEED = 120.0F; + static constexpr float SLIDE_SPEED = 5.0F; // posició global (1/s) + static constexpr float SEG_SPEED = 6.0F; // segments individuals (1/s) private: - enum class Status : std::uint8_t { HIDDEN, - RISING, - ACTIVE, - VANISHING }; + static constexpr int SEGMENT_COUNT = 4; // fps · driver · shader · temps + + struct Segment { + std::string text; + float anim{0.0F}; // 0 = fora, 1 = dins + bool visible{false}; // objectiu cap al que tendeix anim + bool mono_digits{false}; // true només al segment de temps + }; RenderInfo() = default; ~RenderInfo(); + void refreshSegments(); + [[nodiscard]] auto segmentWidth(const Segment& seg, int digit_cell_w) const -> int; + std::unique_ptr text_; - Status status_{Status::HIDDEN}; - float y_{static_cast(-HEIGHT)}; + Defaults::RenderInfoPosition visible_pos_{Defaults::RenderInfoPosition::OFF}; + float info_anim_{0.0F}; // 0..1, suavitzat amb outQuad al render + std::array segments_{}; + int elapsed_ms_{0}; }; diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index bb6176e..04c1ed1 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -224,6 +224,54 @@ void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerni } } +// Amplada fixa per dígits: cada '0'-'9' ocupa digit_cell_w centrat dins de +// la cel·la; la resta de glifs avancen amb la seua amplada natural. Emprat +// pel comptador de temps de l'overlay RenderInfo per a que els dígits no +// balleteigen quan canvien. +void Text::writeMonoDigits(int x, int y, const std::string& text, Uint8 color, int digit_cell_w, int kerning) { + int shift = 0; + std::size_t pos = 0; + + sprite_->setY(static_cast(y)); + while (pos < text.size()) { + std::uint32_t cp = nextCodepoint(text, pos); + auto it = offset_.find(cp); + if (it == offset_.end()) { it = offset_.find('?'); } + if (it == offset_.end()) { continue; } + + const bool IS_DIGIT = (cp >= '0' && cp <= '9'); + const int GLYPH_W = it->second.w; + const int CELL_W = IS_DIGIT ? digit_cell_w : GLYPH_W; + const int GLYPH_X = IS_DIGIT ? x + shift + ((CELL_W - GLYPH_W) / 2) : x + shift; + + sprite_->setClip(static_cast(it->second.x), + static_cast(it->second.y), + static_cast(box_width_), + static_cast(box_height_)); + sprite_->setX(static_cast(GLYPH_X)); + sprite_->render(1, color); + + shift += CELL_W + kerning; + } +} + +auto Text::lengthMonoDigits(const std::string& text, int digit_cell_w, int kerning) const -> int { + int shift = 0; + std::size_t pos = 0; + + while (pos < text.size()) { + std::uint32_t cp = nextCodepoint(text, pos); + auto it = offset_.find(cp); + if (it == offset_.end()) { it = offset_.find('?'); } + if (it == offset_.end()) { continue; } + + const bool IS_DIGIT = (cp >= '0' && cp <= '9'); + shift += (IS_DIGIT ? digit_cell_w : it->second.w) + kerning; + } + + return shift > 0 ? shift - kerning : 0; +} + auto Text::length(const std::string& text, int kerning) const -> int { int shift = 0; std::size_t pos = 0; diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp index 3b283a4..f0e68fb 100644 --- a/source/core/rendering/text.hpp +++ b/source/core/rendering/text.hpp @@ -51,7 +51,13 @@ class Text { void writeCentered(int x, int y, const std::string& text, int kerning = 1, int length = -1); void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = 0, Uint8 shadow_distance = 1, Uint8 shadow_color = 0, int length = -1); + // Dibuixa amb amplada fixa només per als dígits '0'-'9' (cadascun centrat + // dins d'una cel·la de digit_cell_w). La resta de glifs mantenen amplada + // natural. Evita jitter en comptadors tipus "9:59.59". + void writeMonoDigits(int x, int y, const std::string& text, Uint8 color, int digit_cell_w, int kerning = 1); + [[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; + [[nodiscard]] auto lengthMonoDigits(const std::string& text, int digit_cell_w, int kerning = 1) const -> int; [[nodiscard]] auto getCharacterSize() const -> int { return box_width_; } [[nodiscard]] auto glyphWidth(std::uint32_t codepoint, int kerning = 0) const -> int; [[nodiscard]] auto getGlyphClip(std::uint32_t codepoint) const -> SDL_FRect; diff --git a/source/game/options.cpp b/source/game/options.cpp index 235625a..e6fd367 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -43,6 +43,24 @@ namespace { return text == "linear" ? Defaults::TextureFilter::LINEAR : Defaults::TextureFilter::NEAREST; } + auto renderInfoPositionToString(Defaults::RenderInfoPosition pos) -> const char* { + switch (pos) { + case Defaults::RenderInfoPosition::TOP: + return "top"; + case Defaults::RenderInfoPosition::BOTTOM: + return "bottom"; + case Defaults::RenderInfoPosition::OFF: + return "off"; + } + return "off"; + } + + auto renderInfoPositionFromString(const std::string& text) -> Defaults::RenderInfoPosition { + if (text == "top") { return Defaults::RenderInfoPosition::TOP; } + if (text == "bottom") { return Defaults::RenderInfoPosition::BOTTOM; } + return Defaults::RenderInfoPosition::OFF; + } + auto readFileOrEmpty(const std::string& path) -> std::string { std::ifstream file(path); if (!file) { return {}; } @@ -97,7 +115,12 @@ namespace { if (node.contains("texture_filter")) { Options::video.texture_filter = filterFromString(node["texture_filter"].get_value()); } - if (node.contains("render_info")) { Options::video.render_info = node["render_info"].get_value(); } + if (node.contains("render_info_position")) { + Options::video.render_info_position = renderInfoPositionFromString(node["render_info_position"].get_value()); + } + if (node.contains("render_info_show_time")) { + Options::video.render_info_show_time = node["render_info_show_time"].get_value(); + } parseGpu(node); parseShader(node); } @@ -370,7 +393,8 @@ namespace Options { out << " vsync: " << (video.vsync ? "true" : "false") << "\n"; out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n"; out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n"; - out << " render_info: " << (video.render_info ? "true" : "false") << "\n"; + out << " render_info_position: \"" << renderInfoPositionToString(video.render_info_position) << "\"\n"; + out << " render_info_show_time: " << (video.render_info_show_time ? "true" : "false") << "\n"; out << " gpu:\n"; out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n"; out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n"; diff --git a/source/game/options.hpp b/source/game/options.hpp index 3aa9cfa..712fd01 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -34,7 +34,8 @@ namespace Options { bool vsync{Defaults::Video::VSYNC}; Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE}; Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER}; - bool render_info{Defaults::Video::RENDER_INFO}; + Defaults::RenderInfoPosition render_info_position{Defaults::Video::RENDER_INFO_POSITION}; + bool render_info_show_time{Defaults::Video::RENDER_INFO_SHOW_TIME}; Gpu gpu{}; Shader shader{}; }; diff --git a/source/game/ui/notification_presets.hpp b/source/game/ui/notification_presets.hpp index 04972cb..d758964 100644 --- a/source/game/ui/notification_presets.hpp +++ b/source/game/ui/notification_presets.hpp @@ -19,9 +19,19 @@ namespace Notification { // Per a ajustar l'aparença global de totes les notificacions sense // presetejar-les manualment, toca només aquest bloc. constexpr Preset DEFAULT{ - .text_color = 15, - .bg_color = 0, - .time_ms = 2000, + .text_color = 197, + .bg_color = 96, + .time_ms = 4000, + }; + + // Preset per al missatge de confirmació d'eixida (ESC doble). Color + // d'accent per a distingir-lo d'una notificació informativa; la durada + // coincideix amb la finestra durant la qual la segona pulsació d'ESC + // confirma el tancament — veure GlobalInputs::onExit. + constexpr Preset EXIT{ + .text_color = 88, + .bg_color = 40, + .time_ms = 4000, }; } // namespace Notification diff --git a/source/game/ui/ui_palette.hpp b/source/game/ui/ui_palette.hpp index 9220de2..b9405a3 100644 --- a/source/game/ui/ui_palette.hpp +++ b/source/game/ui/ui_palette.hpp @@ -8,9 +8,13 @@ // requereix canviar la seua constant ací — no cal buscar l'índex pel codi. namespace UiColor { - // Color per al text de l'overlay de debug RenderInfo (F3). + // Color per al text de l'overlay de debug RenderInfo (F12). constexpr Uint8 RENDER_INFO_TEXT = 47; + // Ombra desplaçada (+1, +1) per al mateix overlay. Fosca per a donar + // contrast amb qualsevol fons sense haver de pintar una caixa. + constexpr Uint8 RENDER_INFO_SHADOW = 34; + // Color de fons del canvas del joc quan no hi ha escena pintada a tota // la superfície. Útil per a distingir la zona interior del joc en // escalats que deixen banda negra (letterbox, integer, ...). diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 06e634f..e2939e1 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -23,6 +23,14 @@ namespace Defaults { LINEAR = 1, }; + // Posició de l'overlay RenderInfo: apagat, a dalt o a baix del canvas. + // Cicle OFF → TOP → BOTTOM → OFF amb la hotkey corresponent. + enum class RenderInfoPosition : std::uint8_t { + OFF = 0, + TOP = 1, + BOTTOM = 2, + }; + namespace Window { constexpr int ZOOM = 3; constexpr bool FULLSCREEN = false; @@ -37,15 +45,16 @@ namespace Defaults { constexpr bool VSYNC = true; constexpr ScalingMode SCALING_MODE = ScalingMode::LETTERBOX; constexpr TextureFilter TEXTURE_FILTER = TextureFilter::NEAREST; - constexpr bool GPU_ACCELERATION = true; // Path SDL3 GPU per defecte; fallback a SDL_Renderer si falla. - constexpr const char* GPU_PREFERRED_DRIVER = ""; // Buit = auto. Aplica a l'arrencada. - constexpr bool SHADER_ENABLED = true; // Aplica PostFX; si false, sempre passthrough. - constexpr const char* CURRENT_SHADER = "postfx"; // Shader actiu: "postfx" o "crtpi". - constexpr const char* POSTFX_DEFAULT_PRESET = "CRT"; // Preset inicial PostFX. - constexpr const char* CRTPI_DEFAULT_PRESET = "DEFAULT"; // Preset inicial CrtPi. - constexpr const char* POSTFX_FILE = "shaders/postfx.yaml"; // Relatiu a la carpeta de sistema. - constexpr const char* CRTPI_FILE = "shaders/crtpi.yaml"; // Relatiu a la carpeta de sistema. - constexpr bool RENDER_INFO = false; // Overlay de debug (FPS/driver/shader) amagat per defecte. + constexpr bool GPU_ACCELERATION = true; // Path SDL3 GPU per defecte; fallback a SDL_Renderer si falla. + constexpr const char* GPU_PREFERRED_DRIVER = ""; // Buit = auto. Aplica a l'arrencada. + constexpr bool SHADER_ENABLED = true; // Aplica PostFX; si false, sempre passthrough. + constexpr const char* CURRENT_SHADER = "postfx"; // Shader actiu: "postfx" o "crtpi". + constexpr const char* POSTFX_DEFAULT_PRESET = "CRT"; // Preset inicial PostFX. + constexpr const char* CRTPI_DEFAULT_PRESET = "DEFAULT"; // Preset inicial CrtPi. + constexpr const char* POSTFX_FILE = "shaders/postfx.yaml"; // Relatiu a la carpeta de sistema. + constexpr const char* CRTPI_FILE = "shaders/crtpi.yaml"; // Relatiu a la carpeta de sistema. + constexpr RenderInfoPosition RENDER_INFO_POSITION = RenderInfoPosition::OFF; // Overlay de debug amagat per defecte. + constexpr bool RENDER_INFO_SHOW_TIME = true; // Si l'overlay és visible, mostra el temps de joc. } // namespace Video namespace Audio { diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index e4c3e7d..c7cc6ca 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -3,7 +3,7 @@ namespace Texts { constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; - constexpr const char* VERSION = "0.13.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.14.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas { diff --git a/source/utils/easing.cpp b/source/utils/easing.cpp new file mode 100644 index 0000000..745d16d --- /dev/null +++ b/source/utils/easing.cpp @@ -0,0 +1,26 @@ +#include "utils/easing.hpp" + +namespace Easing { + + auto linear(float t) -> float { return t; } + + auto outQuad(float t) -> float { return 1.0F - ((1.0F - t) * (1.0F - t)); } + auto inQuad(float t) -> float { return t * t; } + auto inOutQuad(float t) -> float { + return t < 0.5F + ? 2.0F * t * t + : 1.0F - ((-2.0F * t + 2.0F) * (-2.0F * t + 2.0F) / 2.0F); + } + + auto outCubic(float t) -> float { + const float INV = 1.0F - t; + return 1.0F - (INV * INV * INV); + } + auto inCubic(float t) -> float { return t * t * t; } + + auto lerp(float a, float b, float t) -> float { return a + ((b - a) * t); } + auto lerpInt(int a, int b, float t) -> int { + return a + static_cast(static_cast(b - a) * t); + } + +} // namespace Easing diff --git a/source/utils/easing.hpp b/source/utils/easing.hpp new file mode 100644 index 0000000..68fb72c --- /dev/null +++ b/source/utils/easing.hpp @@ -0,0 +1,20 @@ +#pragma once + +// Funcions de suavitzat per a animacions. Totes accepten t en el rang [0, 1] +// i retornen un valor en [0, 1] (amb overshoot possible en algunes variants). +// Pensades per aplicar-se al paràmetre d'un lerp. +namespace Easing { + + auto linear(float t) -> float; + + auto outQuad(float t) -> float; + auto inQuad(float t) -> float; + auto inOutQuad(float t) -> float; + + auto outCubic(float t) -> float; + auto inCubic(float t) -> float; + + auto lerp(float a, float b, float t) -> float; + auto lerpInt(int a, int b, float t) -> int; + +} // namespace Easing