From 459d18b49ce5f5744ac2434a6bbe00c995e1ed2b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 23 Apr 2026 09:47:59 +0200 Subject: [PATCH] feat(game): pausa amb F11/START i contorn NSEO al Text --- data/input/keys.yaml | 5 +- data/locale/ca.yaml | 1 + data/locale/en.yaml | 1 + source/core/rendering/text.cpp | 11 ++++ source/core/rendering/text.hpp | 7 +++ source/game/scenes/game_scene.cpp | 92 +++++++++++++++++++++++++------ source/game/scenes/game_scene.hpp | 14 +++++ 7 files changed, 113 insertions(+), 18 deletions(-) diff --git a/data/input/keys.yaml b/data/input/keys.yaml index ab85f94..b9ddc4a 100644 --- a/data/input/keys.yaml +++ b/data/input/keys.yaml @@ -36,8 +36,11 @@ keys: code: "F12" desc: "Cicle de l'overlay de debug: apagat / dalt / baix" - id: toggle_render_time - code: "F11" + code: "F10" desc: "Mostra o amaga el temps de joc dins de l'overlay" + - id: pause + code: "F11" + desc: "Pausa el joc (o botó START del mando)" - id: exit code: "Escape" desc: "Eixir (confirmar amb segona pulsació)" diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 9fa5054..25cf6e8 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -22,6 +22,7 @@ input: scaling_overscan: "Escalat overscan" scaling_integer: "Escalat enter" press_again_exit: "Prem una altra volta per eixir" + paused: "Pausa" menu: titles: diff --git a/data/locale/en.yaml b/data/locale/en.yaml index 47c1d35..be9517b 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -22,6 +22,7 @@ input: scaling_overscan: "Scaling overscan" scaling_integer: "Scaling integer" press_again_exit: "Press again to exit" + paused: "Paused" menu: titles: diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index fe53872..5cc266d 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -150,6 +150,7 @@ void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerni const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG); const bool COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG); const bool STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG); + const bool STROKED_4 = ((flags & STROKE4_FLAG) == STROKE4_FLAG); if (CENTERED) { x -= (Text::length(text, kerning) / 2); } @@ -157,6 +158,16 @@ void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerni writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length); } + if (STROKED_4) { + const int MAX_DIST = static_cast(shadow_distance); + for (int dist = 1; dist <= MAX_DIST; ++dist) { + writeColored(x + dist, y, text, shadow_color, kerning, length); + writeColored(x - dist, y, text, shadow_color, kerning, length); + writeColored(x, y + dist, text, shadow_color, kerning, length); + writeColored(x, y - dist, text, shadow_color, kerning, length); + } + } + if (STROKED) { const int MAX_DIST = static_cast(shadow_distance); for (int dist = 1; dist <= MAX_DIST; ++dist) { diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp index ff66ef2..ed92782 100644 --- a/source/core/rendering/text.hpp +++ b/source/core/rendering/text.hpp @@ -31,10 +31,17 @@ class Text { }; // Flags per a writeDX (composables amb |). + // `STROKE_FLAG` dibuixa un contorn complet de 8 veïns (N/S/E/O + 4 + // diagonals) al voltant del glif; `STROKE4_FLAG` només els 4 cardinals + // (N/S/E/O), amb una aparença més prima per a glifs petits on la versió + // diagonal engreixa massa. Els dos flags són mútuament excloents: si + // s'activen a la vegada, ambdós bucles s'executen i l'stroke diagonal + // queda per damunt del cardinal (no té gaire sentit combinar-los). static constexpr Uint8 COLOR_FLAG = 1; static constexpr Uint8 SHADOW_FLAG = 2; static constexpr Uint8 CENTER_FLAG = 4; static constexpr Uint8 STROKE_FLAG = 8; + static constexpr Uint8 STROKE4_FLAG = 16; // Índex del píxel "tinta" al PNG de la font. Els glifs són monocroms // sobre transparent i tots els píxels opacs usen aquest índex; la Surface diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 374bb83..ec7d561 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -12,11 +12,16 @@ #include #include "core/audio/audio.hpp" +#include "core/input/gamepad_manager.hpp" #include "core/input/global_inputs.hpp" +#include "core/input/key_config.hpp" +#include "core/locale/locale.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" +#include "core/rendering/text.hpp" #include "core/resources/resource_cache.hpp" +#include "core/system/event_buffer.hpp" #include "game/entities/entity.hpp" #include "game/entities/event_bus.hpp" #include "game/entities/player.hpp" @@ -109,32 +114,64 @@ GameScene::GameScene(Config config) // jail_audio i obri l'stream. Audio::get() està garantit no-null: // Director::init crida Audio::init abans d'instanciar GameScene. Audio::get()->playMusic(config_.music_track, -1); + + // Font de l'overlay de pausa: reutilitzem la Surface ja cachejada al + // Director (PLACEHOLDER_NAME + ".gif"). Text només parseja el .fnt a + // l'arrencada de l'escena, un sol cop. + const std::string PAUSE_FONT_KEY = std::string(Defaults::UI::Font::PLACEHOLDER_NAME) + ".gif"; + auto pause_surface = Resource::Cache::get().getSurface(PAUSE_FONT_KEY); + pause_font_ = std::make_shared(pause_surface, Defaults::UI::Font::PLACEHOLDER_FNT); + pause_font_->setDefaultKerning(Defaults::UI::Font::PLACEHOLDER_KERNING); } GameScene::~GameScene() = default; +void GameScene::handlePauseInput() { + const SDL_Scancode PAUSE_SC = KeyConfig::get().scancode("pause"); + SDL_Gamepad* pad = GamepadManager::get().getPrimary(); + for (const auto& event : EventBuffer::events) { + const bool KEY_HIT = (event.type == SDL_EVENT_KEY_DOWN && PAUSE_SC != SDL_SCANCODE_UNKNOWN && event.key.scancode == PAUSE_SC); + const bool PAD_HIT = (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN && pad != nullptr && event.gbutton.which == SDL_GetGamepadID(pad) && event.gbutton.button == SDL_GAMEPAD_BUTTON_START); + if (!KEY_HIT && !PAD_HIT) { continue; } + paused_ = !paused_; + if (paused_) { + Audio::get()->pauseMusic(); + } else { + Audio::get()->resumeMusic(); + } + } +} + void GameScene::update(int delta_ms) { + handlePauseInput(); + GlobalInputs::update(delta_ms); if (GlobalInputs::wantsQuit()) { wants_quit_ = true; } - const float DT = static_cast(delta_ms) / 1000.0F; - for (auto& player : players_) { player->update(DT); } - // El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha - // emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat - // als sarcòfags via la subscripció del bus, i aquest `update` pot - // aplicar la transició OPENING → OPEN en el mateix frame. - entity_manager_.update(DT); + // El món es congela durant la pausa: ni jugadors ni entitats avancen, + // ni es resolen col·lisions. Notifier i RenderInfo sí continuen perquè + // els usa la UI de sistema (notificacions d'hotkeys, overlay de debug). + if (!paused_) { + const float DT = static_cast(delta_ms) / 1000.0F; + for (auto& player : players_) { player->update(DT); } + // El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha + // emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat + // als sarcòfags via la subscripció del bus, i aquest `update` pot + // aplicar la transició OPENING → OPEN en el mateix frame. + entity_manager_.update(DT); + + // Detecció de contacte player↔entitat després que totes les entitats + // hagin mogut aquest frame. La porta (si està oberta) emet + // `level_complete`; la momia/trampa emet `player_hit`. Els sarcs, encara + // que tinguen collides=true, tenen la caixa lògica fora del camí del + // jugador (gràcies a Map::collides al Player::update), així que aquí + // no generen falsos contactes. + std::vector player_ptrs; + player_ptrs.reserve(players_.size()); + std::ranges::transform(players_, std::back_inserter(player_ptrs), [](const auto& p) { return p.get(); }); + entity_manager_.checkPlayerCollisions(player_ptrs); + } - // Detecció de contacte player↔entitat després que totes les entitats - // hagin mogut aquest frame. La porta (si està oberta) emet - // `level_complete`; la momia/trampa emet `player_hit`. Els sarcs, encara - // que tinguen collides=true, tenen la caixa lògica fora del camí del - // jugador (gràcies a Map::collides al Player::update), així que aquí - // no generen falsos contactes. - std::vector player_ptrs; - player_ptrs.reserve(players_.size()); - std::ranges::transform(players_, std::back_inserter(player_ptrs), [](const auto& p) { return p.get(); }); - entity_manager_.checkPlayerCollisions(player_ptrs); RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); Notifier::get().update(delta_ms); } @@ -178,6 +215,27 @@ void GameScene::render() { } } + // Overlay de pausa: text centrat amb stroke de 8 veïns (STROKE_FLAG) + // que fa de contorn. Fem el contorn blanc (índex 255 de sega-cube.pal) + // i el cos en color de notificació, prou contrastat amb el fons del + // nivell (parets fosques). Text::writeDX aplica el stroke abans que + // el fill, així que l'ordre visual queda correcte sense passos extra. + if (paused_ && pause_font_) { + constexpr Uint8 OUTLINE_WHITE = 255; + constexpr Uint8 STROKE_DIST = 1; + constexpr int PAUSE_KERNING = 0; + const std::string TEXT = Locale::get().text("input.paused"); + const int Y = GameCanvas::CENTER_Y - (pause_font_->getLineHeight() / 2); + pause_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG, + GameCanvas::CENTER_X, + Y, + TEXT, + PAUSE_KERNING, + Defaults::UI::NOTIFICATION_TEXT, + STROKE_DIST, + OUTLINE_WHITE); + } + Notifier::get().render(); RenderInfo::get().render(); Screen::get().present(); diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index a7b5489..2d9e951 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -9,6 +9,7 @@ #include "game/map/tilemap.hpp" class Player; +class Text; class GameScene : public Scene { public: @@ -31,6 +32,11 @@ class GameScene : public Scene { void render() override; private: + // Consumeix events d'aquest frame (F11 del KeyConfig "pause" o botó + // START del gamepad primari) per alternar l'estat de pausa. Quan + // s'activa, pausa la música; en reprendre, la reprén. + void handlePauseInput(); + Config config_; Tilemap tilemap_; @@ -42,4 +48,12 @@ class GameScene : public Scene { // Vector des del principi: AEE suportarà multijugador local; cada Player // té els seus propis Controls i entra al vector amb una crida extra. std::vector> players_; + + // Pausa de gameplay. Quan `paused_` és true, l'update de jugadors i + // entitats es salta (el món es congela); l'input d'UI (GlobalInputs, + // Notifier, RenderInfo) continua per no perdre hotkeys ni l'overlay de + // debug. La font s'instancia des del Surface ja cachejat al Director + // (cap altra escena l'allibera, així que l'accés és segur). + std::shared_ptr pause_font_; + bool paused_{false}; };