feat(game): pausa amb F11/START i contorn NSEO al Text

This commit is contained in:
JailDesigner committed 2026-04-23 09:47:59 +02:00
1 parent 243ba06351
commit 459d18b49c
7 files changed
+96 -1

No files matched your search

+4 -1
View File
@@ -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ó)"
+1
View File
@@ -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:
+1
View File
@@ -22,6 +22,7 @@ input:
scaling_overscan: "Scaling overscan"
scaling_integer: "Scaling integer"
press_again_exit: "Press again to exit"
paused: "Paused"
menu:
titles:
+11
View File
@@ -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<int>(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<int>(shadow_distance);
for (int dist = 1; dist <= MAX_DIST; ++dist) {
+7
View File
@@ -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
+58
View File
@@ -12,11 +12,16 @@
#include <vector>
#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,14 +114,44 @@ 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<Text>(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; }
// 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<float>(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
@@ -135,6 +170,8 @@ void GameScene::update(int delta_ms) {
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();
+14
View File
@@ -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<std::unique_ptr<Player>> 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<Text> pause_font_;
bool paused_{false};
};