From 1b430500f43b1cf82b26578f40433ed63fff188c Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 19 Apr 2026 10:54:32 +0200 Subject: [PATCH] Sistema de notificacions amb presets (F4, llisca des de dalt) (v0.9.0) --- CMakeLists.txt | 1 + source/core/rendering/notifier.cpp | 86 +++++++++++++++++++++++++ source/core/rendering/notifier.hpp | 63 ++++++++++++++++++ source/core/rendering/render_info.cpp | 10 +-- source/core/rendering/screen.cpp | 11 ++++ source/core/rendering/screen.hpp | 3 + source/core/system/director.cpp | 11 +++- source/game/scenes/game_scene.cpp | 4 ++ source/game/ui/notification_presets.hpp | 27 ++++++++ source/utils/defines.hpp | 2 +- 10 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 source/core/rendering/notifier.cpp create mode 100644 source/core/rendering/notifier.hpp create mode 100644 source/game/ui/notification_presets.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 54837f0..9396f12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ set(APP_SOURCES source/main.cpp source/core/system/director.cpp source/core/rendering/screen.cpp + source/core/rendering/notifier.cpp source/core/rendering/render_info.cpp source/core/rendering/gif.cpp source/core/rendering/surface.cpp diff --git a/source/core/rendering/notifier.cpp b/source/core/rendering/notifier.cpp new file mode 100644 index 0000000..02de5ae --- /dev/null +++ b/source/core/rendering/notifier.cpp @@ -0,0 +1,86 @@ +#include "core/rendering/notifier.hpp" + +#include "core/rendering/screen.hpp" +#include "core/rendering/surface.hpp" +#include "core/rendering/text.hpp" +#include "core/resources/resource_cache.hpp" +#include "utils/defines.hpp" + +auto Notifier::get() -> Notifier& { + static Notifier instance_; + return instance_; +} + +Notifier::~Notifier() = default; + +void Notifier::init() { + // El Director preloade la paleta "ui" i la surface "8bithud.gif" abans + // de cridar aquest init(); ací només cal construir el Text. + auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); + text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); +} + +void Notifier::update(int 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::ACTIVE: + remaining_ms_ -= delta_ms; + if (remaining_ms_ <= 0) { status_ = Status::VANISHING; } + break; + case Status::VANISHING: + y_ -= SLIDE_SPEED * DT; + if (y_ <= static_cast(-HEIGHT)) { + y_ = static_cast(-HEIGHT); + status_ = Status::HIDDEN; + } + break; + default: + break; + } +} + +void Notifier::render() { + if (status_ == Status::HIDDEN || text_ == nullptr || current_text_.empty()) { return; } + + const int TEXT_W = text_->length(current_text_, 1); + const int BOX_W = TEXT_W + (2 * PADDING_H); + const int BOX_X = (GameCanvas::WIDTH - BOX_W) / 2; + const int BOX_Y = static_cast(y_); + + // Fons sòlid a través de la paleta "ui" (ARGB directe al canvas, sense + // tocar cap surface paletada). + const auto& ui_palette = Resource::Cache::get().getPalette("ui"); + Screen::get().fillRect(BOX_X, BOX_Y, BOX_W, HEIGHT, ui_palette[current_bg_color_]); + + text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, + GameCanvas::CENTER_X, + BOX_Y + PADDING_V, + current_text_, + 1, + current_text_color_); +} + +void Notifier::notify(const std::string& text) { + notify(text, Notification::DEFAULT); +} + +void Notifier::notify(const std::string& text, const Notification::Preset& preset) { + notify(text, preset.text_color, preset.bg_color, preset.time_ms); +} + +void Notifier::notify(const std::string& text, Uint8 text_color, Uint8 bg_color, int time_ms) { + // Política simple: la nova notificació reemplaça la que hi havia. Per + // gestionar-ne una cua, canviar ací per una FIFO quan calga. + current_text_ = text; + current_text_color_ = text_color; + current_bg_color_ = bg_color; + remaining_ms_ = time_ms; + status_ = Status::RISING; +} diff --git a/source/core/rendering/notifier.hpp b/source/core/rendering/notifier.hpp new file mode 100644 index 0000000..b80f9af --- /dev/null +++ b/source/core/rendering/notifier.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include +#include +#include + +#include "game/ui/notification_presets.hpp" + +class Text; + +// Popup transitori a dalt i centrat. Entra lliscant des de dalt, es queda +// visible el temps configurat i surt lliscant cap amunt. Comparteix la font +// 8bithud i la paleta "ui" amb la resta d'overlays (carregades per RenderInfo +// a l'arrencada; si Notifier::init() es crida primer, les carrega ell). +// +// Ús típic: +// Notifier::get().notify("Text ràpid"); // preset DEFAULT +// Notifier::get().notify("Text ràpid", miPreset); // preset custom +// Notifier::get().notify("Urgent", 11, 3, 800); // colors i durada explícits +class Notifier { + public: + static auto get() -> Notifier&; + + Notifier(const Notifier&) = delete; + Notifier(Notifier&&) = delete; + auto operator=(const Notifier&) -> Notifier& = delete; + auto operator=(Notifier&&) -> Notifier& = delete; + + void init(); + + void update(int delta_ms); + void render(); + + // Tres overloads; els dos simples deleguen al tercer. + void notify(const std::string& text); + void notify(const std::string& text, const Notification::Preset& preset); + void notify(const std::string& text, Uint8 text_color, Uint8 bg_color, int time_ms); + + static constexpr int PADDING_H = 6; + static constexpr int PADDING_V = 3; + static constexpr int TEXT_SIZE = 8; + static constexpr int HEIGHT = TEXT_SIZE + (2 * PADDING_V); + static constexpr float SLIDE_SPEED = 120.0F; + + private: + enum class Status : std::uint8_t { HIDDEN, + RISING, + ACTIVE, + VANISHING }; + + Notifier() = default; + ~Notifier(); + + std::unique_ptr text_; + Status status_{Status::HIDDEN}; + float y_{static_cast(-HEIGHT)}; + std::string current_text_; + Uint8 current_text_color_{0}; + Uint8 current_bg_color_{0}; + int remaining_ms_{0}; +}; diff --git a/source/core/rendering/render_info.cpp b/source/core/rendering/render_info.cpp index 9bc250c..8370966 100644 --- a/source/core/rendering/render_info.cpp +++ b/source/core/rendering/render_info.cpp @@ -32,14 +32,10 @@ auto RenderInfo::get() -> RenderInfo& { RenderInfo::~RenderInfo() = default; void RenderInfo::init() { - // Paleta compartida de UI: qualsevol HUD que la vulga només ha de cridar - // Cache::getPalette("ui") i aplicar-la a la seua surface. El .gif propi - // de la font (2 colors) es descarta a favor d'aquesta. - Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal"); - - Resource::Cache::get().loadSurface("8bithud.gif", "data/font/8bithud.gif"); + // 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"); - font_surface->setPalette(Resource::Cache::get().getPalette("ui")); text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); // Restaura l'estat guardat a config.yaml (video.render_info). diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 632f5e1..b9a83c5 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -161,6 +161,17 @@ void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) { std::ranges::fill(canvas_, COLOR); } +void Screen::fillRect(int x, int y, int w, int h, std::uint32_t argb) { + const int X0 = std::max(x, 0); + const int Y0 = std::max(y, 0); + const int X1 = std::min(x + w, canvas_width_); + const int Y1 = std::min(y + h, canvas_height_); + for (int iy = Y0; iy < Y1; ++iy) { + std::uint32_t* row = canvas_.data() + (static_cast(iy) * canvas_width_); + std::fill(row + X0, row + X1, argb); + } +} + void Screen::present() { fps_.increment(); fps_.calculate(SDL_GetTicks()); diff --git a/source/core/rendering/screen.hpp b/source/core/rendering/screen.hpp index 30e74ab..b55ef76 100644 --- a/source/core/rendering/screen.hpp +++ b/source/core/rendering/screen.hpp @@ -55,6 +55,9 @@ class Screen { void applyCrtPiParams(const Rendering::CrtPiParams& params); void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0); + // Pinta un rectangle opac al canvas ARGB del joc. Pensat per a HUDs + // simples (caixes de notificacions, barres d'estat); no gasta paleta. + void fillRect(int x, int y, int w, int h, std::uint32_t argb); void present(); [[nodiscard]] auto getRenderer() const -> SDL_Renderer* { return renderer_; } diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 13e56c9..7205a0a 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -10,8 +10,10 @@ #include #include +#include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" +#include "core/rendering/surface.hpp" #include "core/resources/resource_cache.hpp" #include "core/scene/scene.hpp" #include "core/system/event_buffer.hpp" @@ -52,9 +54,14 @@ Director::Director() { Screen::get().applyCrtPiParams(Options::buildCrtPiParams()); (void)Resource::Cache::get(); - // L'overlay de debug necessita Screen inicialitzat (llegirà el canvas - // i carregarà la font 8bithud via Resource::Cache). + // Recursos compartits d'UI (paleta + font). Es carreguen abans que els + // overlays perquè tots els reaprofiten sense duplicar la lògica. + Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal"); + Resource::Cache::get().loadSurface("8bithud.gif", "data/font/8bithud.gif"); + Resource::Cache::get().getSurface("8bithud.gif")->setPalette(Resource::Cache::get().getPalette("ui")); + RenderInfo::get().init(); + Notifier::get().init(); current_scene_ = std::make_unique(); last_ticks_ms_ = SDL_GetTicks(); diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index eebbbbe..f98fe9b 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -2,6 +2,7 @@ #include +#include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/resources/resource_cache.hpp" @@ -35,17 +36,20 @@ void GameScene::update(int delta_ms) { if (event.type == SDL_EVENT_KEY_DOWN) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) { wants_quit_ = true; } if (event.key.scancode == SDL_SCANCODE_F3) { RenderInfo::get().toggle(); } + if (event.key.scancode == SDL_SCANCODE_F4) { Notifier::get().notify("notificacio de prova"); } } } const float DT = static_cast(delta_ms) / 1000.0F; for (auto& player : players_) { player->update(DT); } RenderInfo::get().update(delta_ms); + Notifier::get().update(delta_ms); } void GameScene::render() { Screen::get().clear(0x00, 0x00, 0x00); for (auto& player : players_) { player->render(); } + Notifier::get().render(); RenderInfo::get().render(); Screen::get().present(); } diff --git a/source/game/ui/notification_presets.hpp b/source/game/ui/notification_presets.hpp new file mode 100644 index 0000000..04972cb --- /dev/null +++ b/source/game/ui/notification_presets.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +// Temes per a les notificacions de pantalla. Un "preset" captura els dos +// colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat +// en mil·lisegons. Qualsevol component que cride Notifier::notify(text) +// rebrà l'estil DEFAULT; si necessita un altre look, pot passar el preset per +// nom o especificar els camps un a un. +namespace Notification { + + struct Preset { + Uint8 text_color; + Uint8 bg_color; + int time_ms; + }; + + // Tema per defecte: text clar sobre fons fosc, 2 segons de visibilitat. + // 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, + }; + +} // namespace Notification diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index 0dba174..4897c9e 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.8.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.9.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas {