feat(game): pausa graduada amb easing i separador al HUD

This commit is contained in:
JailDesigner committed 2026-04-24 11:56:38 +02:00
1 parent 8aa4ec3daf
commit 5c70019eb5
9 files changed
+258 -61

No files matched your search

+1 -1
View File
@@ -32,7 +32,7 @@ hud:
p1: "P1"
p2: "P2"
lives: "Vides"
treasures: "Tres."
treasures: "Tres"
pyramid: "Piramide"
room: "Sala"
+4 -4
View File
@@ -31,10 +31,10 @@ input:
hud:
p1: "P1"
p2: "P2"
lives: "L"
treasures: "T"
pyramid: "PYRAMID"
room: "ROOM"
lives: "Lives"
treasures: "Tre"
pyramid: "Pyramid"
room: "Room"
menu:
titles:
+11
View File
@@ -52,3 +52,14 @@ hud:
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
flash_ms: 250 # durada del flash quan un jugador rep un hit
shake_ms: 300 # durada del shake que acompanya el flash
# Pausa graduada de la GameScene: el món no para d'un colp, s'atura amb
# easing i torna a arrancar amb una altra corba. Els timings es poden
# afinar independentment, i les corbes permeten estils diferents entrant
# i sortint (p.ex. frenar suau i arrancar seca). Corbes disponibles:
# linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
pause:
enter_ms: 400
exit_ms: 250
enter_curve: outCubic
exit_curve: inCubic
+47
View File
@@ -2,10 +2,13 @@
#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp"
#include "utils/easing.hpp"
namespace {
@@ -30,6 +33,28 @@ namespace {
dst = static_cast<Uint8>(VALUE);
}
// Registre de corbes disponibles. Afegir-ne una nova és només una
// entrada ací: la llegeix `UiConfig::curveByName` i queda exposada al
// YAML pel seu nom. No hi ha fallback mut: si el nom no hi és, l'api
// retorna linear i avisa per stderr perquè l'error no passi en silenci.
auto curveRegistry() -> const std::unordered_map<std::string_view, UiConfig::CurveFn>& {
static const std::unordered_map<std::string_view, UiConfig::CurveFn> REGISTRY = {
{"linear", &Easing::linear},
{"outQuad", &Easing::outQuad},
{"inQuad", &Easing::inQuad},
{"inOutQuad", &Easing::inOutQuad},
{"outCubic", &Easing::outCubic},
{"inCubic", &Easing::inCubic},
};
return REGISTRY;
}
void readCurve(const fkyaml::node& node, const char* key, UiConfig::CurveFn& dst) {
if (!node.contains(key)) { return; }
const auto NAME = node[key].get_value<std::string>();
dst = UiConfig::curveByName(NAME);
}
void readFont(const fkyaml::node& node, const char* key, UiConfig::Font& dst) {
if (!node.contains(key)) { return; }
const auto& f = node[key];
@@ -65,6 +90,10 @@ UiConfig::UiConfig()
hud_entry_ms_(Defaults::UI::HUD_ENTRY_MS),
hud_flash_ms_(Defaults::UI::HUD_FLASH_MS),
hud_shake_ms_(Defaults::UI::HUD_SHAKE_MS),
pause_enter_ms_(Defaults::UI::PAUSE_ENTER_MS),
pause_exit_ms_(Defaults::UI::PAUSE_EXIT_MS),
pause_enter_curve_(curveByName(Defaults::UI::PAUSE_ENTER_CURVE)),
pause_exit_curve_(curveByName(Defaults::UI::PAUSE_EXIT_CURVE)),
hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING},
placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING},
menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING},
@@ -76,6 +105,16 @@ auto UiConfig::get() -> UiConfig& {
return instance_;
}
auto UiConfig::curveByName(const std::string& name) -> CurveFn {
const auto& reg = curveRegistry();
const auto IT = reg.find(name);
if (IT == reg.end()) {
std::cerr << "UiConfig: corba '" << name << "' desconeguda — s'usa linear\n";
return &Easing::linear;
}
return IT->second;
}
void UiConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) {
@@ -132,6 +171,14 @@ void UiConfig::load(const std::string& file_path) {
readField(h, "shake_ms", hud_shake_ms_);
}
if (yaml.contains("pause")) {
const auto& p = yaml["pause"];
readField(p, "enter_ms", pause_enter_ms_);
readField(p, "exit_ms", pause_exit_ms_);
readCurve(p, "enter_curve", pause_enter_curve_);
readCurve(p, "exit_curve", pause_exit_curve_);
}
std::cout << "UiConfig: carregat des de " << file_path << '\n';
} catch (const std::exception& e) {
std::cerr << "UiConfig: error parsejant " << file_path << ": " << e.what() << '\n';
+22
View File
@@ -49,12 +49,30 @@ class UiConfig {
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; }
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
// Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1].
// Permet triar corbes des de data/ui.yaml per nom (cadena) i guardar
// el punter a la corresponent funció de `utils/easing.hpp`.
using CurveFn = auto (*)(float) -> float;
// Timings (ms).
[[nodiscard]] auto notificationMs() const -> int { return notification_ms_; }
[[nodiscard]] auto exitMs() const -> int { return exit_ms_; }
[[nodiscard]] auto hudEntryMs() const -> int { return hud_entry_ms_; }
[[nodiscard]] auto hudFlashMs() const -> int { return hud_flash_ms_; }
[[nodiscard]] auto hudShakeMs() const -> int { return hud_shake_ms_; }
[[nodiscard]] auto pauseEnterMs() const -> int { return pause_enter_ms_; }
[[nodiscard]] auto pauseExitMs() const -> int { return pause_exit_ms_; }
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
// Easing::linear com a fallback.
[[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; }
[[nodiscard]] auto pauseExitCurve() const -> CurveFn { return pause_exit_curve_; }
// Resol un nom de corba a la seua funció. Pública per permetre a
// futurs camps d'UI triar corbes pel mateix mecanisme (ex.: HUD
// entry, notifications slide...). Si el nom no està registrat,
// retorna Easing::linear i deixa un warning al log.
[[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn;
// Fonts de UI.
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
@@ -93,6 +111,10 @@ class UiConfig {
int hud_entry_ms_;
int hud_flash_ms_;
int hud_shake_ms_;
int pause_enter_ms_;
int pause_exit_ms_;
CurveFn pause_enter_curve_;
CurveFn pause_exit_curve_;
Font hud_font_;
Font placeholder_font_;
+94 -22
View File
@@ -3,6 +3,7 @@
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <filesystem>
#include <iostream>
@@ -33,6 +34,7 @@
#include "game/ui/hud.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp"
#include "utils/easing.hpp"
namespace {
@@ -178,12 +180,69 @@ void GameScene::handlePauseInput() {
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 {
// Determinem cap a quin estat anem. Si estem quiets (IDLE o
// PAUSED) arranquem la transició contrària. Si estem en marxa
// (ENTERING o EXITING) invertim: la `scale` actual es manté com
// a `from_scale` nou i la durada es recalcula proporcional a la
// distància restant — així mai es produeix un salt de velocitat.
const bool GOING_TO_PAUSE = (pause_.state == PauseState::IDLE || pause_.state == PauseState::EXITING);
// Reprenem l'stream ABANS de començar a pujar el volum: quan
// venim de PAUSED l'àudio està aturat de debò (ho fem al final
// d'ENTERING, veure updatePauseTransition); reprenem just al
// moment de començar EXITING per no perdre el primer frame
// d'stream. A les inversions mid-flight (ENTERING → EXITING
// sense tocar PAUSED) l'stream no s'ha aturat mai, així que no
// cal reprendre.
if (!GOING_TO_PAUSE && pause_.state == PauseState::PAUSED) {
Audio::get()->resumeMusic();
}
pause_.from_scale = pause_.scale;
pause_.to_scale = GOING_TO_PAUSE ? 0.0F : 1.0F;
const int TARGET_MS = GOING_TO_PAUSE ? UiConfig::get().pauseEnterMs() : UiConfig::get().pauseExitMs();
const float DISTANCE = std::fabs(pause_.to_scale - pause_.from_scale);
pause_.duration_ms = std::max(1, static_cast<int>(static_cast<float>(TARGET_MS) * DISTANCE));
pause_.elapsed_ms = 0;
pause_.curve = GOING_TO_PAUSE ? UiConfig::get().pauseEnterCurve() : UiConfig::get().pauseExitCurve();
pause_.state = GOING_TO_PAUSE ? PauseState::ENTERING : PauseState::EXITING;
std::cout << "[PAUSE] " << (GOING_TO_PAUSE ? "entering" : "exiting") << " from_scale=" << pause_.from_scale << " duration_ms=" << pause_.duration_ms << '\n';
}
}
void GameScene::updatePauseTransition(int delta_ms) {
if (pause_.state != PauseState::ENTERING && pause_.state != PauseState::EXITING) { return; }
pause_.elapsed_ms += delta_ms;
bool just_reached_pause = false;
if (pause_.elapsed_ms >= pause_.duration_ms) {
// Transició completa: enganxem al valor objectiu exacte i
// promocionem l'estat al terminal corresponent.
pause_.scale = pause_.to_scale;
const PauseState NEXT = (pause_.to_scale == 0.0F) ? PauseState::PAUSED : PauseState::IDLE;
just_reached_pause = (NEXT == PauseState::PAUSED);
pause_.state = NEXT;
} else {
const float T = static_cast<float>(pause_.elapsed_ms) / static_cast<float>(pause_.duration_ms);
const auto CURVE = (pause_.curve != nullptr) ? pause_.curve : &Easing::linear;
pause_.scale = std::lerp(pause_.from_scale, pause_.to_scale, CURVE(T));
}
// Fade del volum de música durant la transició. La base és el que
// l'usuari ha configurat a Options; multipliquem per la `scale` per
// obtenir el volum efectiu aquest frame.
const float BASE_VOLUME = Options::audio().music.volume;
Audio::get()->setMusicVolume(BASE_VOLUME * pause_.scale);
// En arribar a PAUSED aturem l'stream: així quan despausem la música
// tornarà des d'on s'ha quedat, no des d'on hauria anat a parar
// durant el temps que el joc ha estat en pausa. El `resumeMusic` es
// fa a `handlePauseInput` al començar l'EXITING, perquè la música
// sone ja mentre el volum puja des de 0.
if (just_reached_pause) {
Audio::get()->pauseMusic();
}
}
@@ -201,12 +260,13 @@ namespace {
void GameScene::update(int delta_ms) {
handlePauseInput();
updatePauseTransition(delta_ms);
#ifdef _DEBUG
// Toggle de l'editor: només si la pausa no està activa (mútuament
// excloents). L'editor mateix no consumeix la tecla — la llegim ací
// per decidir la coexistència.
if (!paused_ && detectEditorToggle()) {
// Toggle de l'editor: només si no hi ha cap transició de pausa ni
// pausa consolidada. L'editor mateix no consumeix la tecla — la
// llegim ací per decidir la coexistència.
if (pause_.state == PauseState::IDLE && detectEditorToggle()) {
editor_.toggle(tilemap_);
}
#endif
@@ -214,16 +274,19 @@ void GameScene::update(int delta_ms) {
GlobalInputs::update(delta_ms);
if (GlobalInputs::wantsQuit()) { wants_quit_ = true; }
// El món es congela durant la pausa i durant el mode editor: ni
// jugadors ni entitats avancen, ni es resolen col·lisions. Notifier i
// RenderInfo sí continuen perquè els usa la UI de sistema.
// El món avança amb `delta_ms * pause_.scale`: scale=1 corre a velocitat
// real, scale=0 el congela. Quan l'editor de debug està actiu també
// es salta completament (no l'escalem, el saltem). Notifier,
// RenderInfo i HUD reben el delta real, per això viuen fora d'aquest
// bloc i no es ralenteixen amb el dial de pausa.
#ifdef _DEBUG
const bool WORLD_FROZEN = paused_ || editor_.active();
const bool EDITOR_FROZEN = editor_.active();
#else
const bool WORLD_FROZEN = paused_;
constexpr bool EDITOR_FROZEN = false;
#endif
if (!WORLD_FROZEN) {
const float DT = static_cast<float>(delta_ms) / 1000.0F;
const int SCALED_DELTA_MS = static_cast<int>(static_cast<float>(delta_ms) * pause_.scale);
if (!EDITOR_FROZEN && SCALED_DELTA_MS > 0) {
const float DT = static_cast<float>(SCALED_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
@@ -318,19 +381,28 @@ void GameScene::render() {
editor_.render(tilemap_);
#endif
// 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_) {
// Overlay de pausa: text amb stroke blanc (índex 255) centrat amb X
// depenent de l'estat del dial. Entra lliscant des de l'esquerra
// (ENTERING), es queda al centre (PAUSED) i surt cap a la dreta
// (EXITING). La X es deriva directament de `pause_.scale`, que ja
// porta la corba aplicada — així text i món frenen alhora.
if (pause_.state != PauseState::IDLE && 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 TEXT_WIDTH = pause_font_->length(TEXT, PAUSE_KERNING);
const int OFF_LEFT = -TEXT_WIDTH;
const int OFF_RIGHT = GameCanvas::WIDTH + TEXT_WIDTH;
int x = GameCanvas::CENTER_X;
if (pause_.state == PauseState::ENTERING) {
x = Easing::lerpInt(OFF_LEFT, GameCanvas::CENTER_X, 1.0F - pause_.scale);
} else if (pause_.state == PauseState::EXITING) {
x = Easing::lerpInt(GameCanvas::CENTER_X, OFF_RIGHT, pause_.scale);
}
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,
x,
Y,
TEXT,
PAUSE_KERNING,
+41 -12
View File
@@ -1,10 +1,12 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "core/scene/scene.hpp"
#include "core/ui/ui_config.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/map/paint_layer.hpp"
#include "game/map/tilemap.hpp"
@@ -39,10 +41,16 @@ class GameScene : public Scene {
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.
// START del gamepad primari) per disparar les transicions de pausa.
// No alterna un flag binari: inicia (o inverteix mid-flight) un
// "dial" de temps que frena el món amb easing.
void handlePauseInput();
// Avança la transició activa, recalcula `pause_.scale` i aplica el
// fade de volum de música. S'executa cada frame amb el delta real
// (no escalat) perquè la transició viu en temps real.
void updatePauseTransition(int delta_ms);
Config config_;
Tilemap tilemap_;
@@ -72,19 +80,40 @@ class GameScene : public Scene {
// la destrucció.
std::unique_ptr<Hud> hud_;
// 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).
// Pausa de gameplay com a "dial" de temps. En lloc d'un interruptor,
// `pause_.scale` va de 1 (món corrent) a 0 (món congelat) amb una
// corba d'easing durant ENTERING, i al revés durant EXITING. El
// `delta_ms` del món (players + EntityManager) es multiplica per
// aquesta escala; HUD, Notifier i GlobalInputs reben sempre el
// delta real. Les inversions mid-flight capturen la `scale` actual
// com a `from_scale` i escalen la durada a la distància restant per
// mantenir la sensació de velocitat. La font és la mateixa que
// abans — ja cachejada al Director.
std::shared_ptr<Text> pause_font_;
bool paused_{false};
enum class PauseState : std::uint8_t {
IDLE, // Joc corrent; cap transició en marxa. scale == 1.
ENTERING, // Frenant cap a la pausa. scale decreix a 0.
PAUSED, // Món congelat. scale == 0.
EXITING, // Accelerant de tornada. scale creix a 1.
};
struct PauseTransition {
PauseState state{PauseState::IDLE};
float scale{1.0F}; // 0 = parat, 1 = corrent.
float from_scale{1.0F}; // Valor de `scale` al començar la transició.
float to_scale{1.0F}; // Valor objectiu (0.0 o 1.0).
int elapsed_ms{0}; // Temps transcorregut dins la transició.
int duration_ms{0}; // Durada total (escalada a la distància).
UiConfig::CurveFn curve{nullptr}; // Corba activa.
} pause_;
#ifdef _DEBUG
// Editor modal del tilemap. Mútuament excloent amb la pausa: si `paused_`
// la tecla de toggle de l'editor s'ignora, i viceversa. Quan
// `editor_.active()` és true, saltem entitats i jugador (update i
// render) i deixem el tilemap visible perquè es pugui editar.
// Editor modal del tilemap. Mútuament excloent amb la pausa: si la
// pausa no està IDLE la tecla de toggle de l'editor s'ignora, i
// viceversa. Quan `editor_.active()` és true, saltem entitats i
// jugador (update i render) i deixem el tilemap visible perquè es
// pugui editar.
Editor editor_;
#endif
};
+27 -22
View File
@@ -32,9 +32,13 @@ namespace {
// ho fa igual amb `writeMonoDigits`).
constexpr int MONO_DIGIT_W = 6;
// Separadors visuals entre etiquetes i valors.
constexpr int LABEL_GAP = 2; // px entre "P1" i "000000"
constexpr int FIELD_GAP = 6; // px entre "V 3" i "T 00"
// Separador que s'enganxa al final de l'etiqueta abans del valor
// ("P1 000000", "Vides 3"...). Canviant aquesta cadena aquí es
// controla el look-and-feel de totes les línies del HUD.
constexpr const char* SEPARATOR = " ";
// Separació extra entre camps dins d'una mateixa línia.
constexpr int FIELD_GAP = 6; // px entre "Vides 3" i "Tres. 00"
// Format zero-padded a 6 dígits per a la puntuació.
auto formatScore(int v) -> std::string {
@@ -229,56 +233,57 @@ void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index
const auto& p = players_[static_cast<std::size_t>(player_index)];
const Uint8 SCORE_C = (blk.flash_ms_left > 0) ? UiConfig::get().hudFlash() : UiConfig::get().hudValue();
// --- Línia 1: "<label> 000000" centrada ---
// --- Línia 1: "<label>-000000" centrada ---
const std::string LBL1 = label + SEPARATOR;
const std::string SCORE = formatScore(p.score);
const int LBL_W = font_->length(label);
const int LBL_W = font_->length(LBL1);
const int SC_W = font_->lengthMonoDigits(SCORE, MONO_DIGIT_W);
const int LINE1_W = LBL_W + LABEL_GAP + SC_W;
const int LINE1_W = LBL_W + SC_W;
int xx = x + ((w - LINE1_W) / 2);
font_->writeColored(xx, y + LINE1_Y, label, UiConfig::get().hudLabel());
xx += LBL_W + LABEL_GAP;
font_->writeColored(xx, y + LINE1_Y, LBL1, UiConfig::get().hudLabel());
xx += LBL_W;
font_->writeMonoDigits(xx, y + LINE1_Y, SCORE, SCORE_C, MONO_DIGIT_W);
// --- Línia 2: "<vides> <n> <tresors> <n>" centrada ---
const std::string V_LBL = Locale::get().text("hud.lives");
const std::string T_LBL = Locale::get().text("hud.treasures");
// --- Línia 2: "<vides>-<n> <tresors>-<n>" centrada ---
const std::string V_LBL = Locale::get().text("hud.lives") + SEPARATOR;
const std::string T_LBL = Locale::get().text("hud.treasures") + SEPARATOR;
const std::string V_VAL = std::to_string(std::clamp(p.lives, 0, 9));
const std::string T_VAL = formatTwoDigits(p.treasures);
const int V_LW = font_->length(V_LBL);
const int V_VW = font_->lengthMonoDigits(V_VAL, MONO_DIGIT_W);
const int T_LW = font_->length(T_LBL);
const int T_VW = font_->lengthMonoDigits(T_VAL, MONO_DIGIT_W);
const int LINE2_W = V_LW + LABEL_GAP + V_VW + FIELD_GAP + T_LW + LABEL_GAP + T_VW;
const int LINE2_W = V_LW + V_VW + FIELD_GAP + T_LW + T_VW;
xx = x + ((w - LINE2_W) / 2);
font_->writeColored(xx, y + LINE2_Y, V_LBL, UiConfig::get().hudLabel());
xx += V_LW + LABEL_GAP;
xx += V_LW;
font_->writeMonoDigits(xx, y + LINE2_Y, V_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
xx += V_VW + FIELD_GAP;
font_->writeColored(xx, y + LINE2_Y, T_LBL, UiConfig::get().hudLabel());
xx += T_LW + LABEL_GAP;
xx += T_LW;
font_->writeMonoDigits(xx, y + LINE2_Y, T_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
}
void Hud::renderIdleCenter(int x, int y, int w) const {
// --- Línia 1: "<piramide> <n>" centrada ---
const std::string PIR_LBL = Locale::get().text("hud.pyramid");
// --- Línia 1: "<piramide>-<n>" centrada ---
const std::string PIR_LBL = Locale::get().text("hud.pyramid") + SEPARATOR;
const std::string PIR_VAL = std::to_string(pyramid_);
const int PL_W = font_->length(PIR_LBL);
const int PV_W = font_->lengthMonoDigits(PIR_VAL, MONO_DIGIT_W);
const int LINE1_W = PL_W + LABEL_GAP + PV_W;
const int LINE1_W = PL_W + PV_W;
int xx = x + ((w - LINE1_W) / 2);
font_->writeColored(xx, y + LINE1_Y, PIR_LBL, UiConfig::get().hudLabel());
xx += PL_W + LABEL_GAP;
xx += PL_W;
font_->writeMonoDigits(xx, y + LINE1_Y, PIR_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
// --- Línia 2: "<sala> <r>" centrada ---
const std::string SALA_LBL = Locale::get().text("hud.room");
// --- Línia 2: "<sala>-<r>" centrada ---
const std::string SALA_LBL = Locale::get().text("hud.room") + SEPARATOR;
const std::string SALA_VAL = std::to_string(room_);
const int SL_W = font_->length(SALA_LBL);
const int SV_W = font_->lengthMonoDigits(SALA_VAL, MONO_DIGIT_W);
const int LINE2_W = SL_W + LABEL_GAP + SV_W;
const int LINE2_W = SL_W + SV_W;
xx = x + ((w - LINE2_W) / 2);
font_->writeColored(xx, y + LINE2_Y, SALA_LBL, UiConfig::get().hudLabel());
xx += SL_W + LABEL_GAP;
xx += SL_W;
font_->writeMonoDigits(xx, y + LINE2_Y, SALA_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
}
+11
View File
@@ -169,6 +169,17 @@ namespace Defaults {
constexpr int HUD_ENTRY_MS = 400;
constexpr int HUD_FLASH_MS = 250;
constexpr int HUD_SHAKE_MS = 300;
// Pausa graduada de la GameScene: el món no para d'un colp, es
// frena amb una corba i torna a arrancar amb una altra. Els noms
// de corba han de coincidir amb els registrats a
// UiConfig::curveByName (linear / outQuad / inQuad / inOutQuad /
// outCubic / inCubic). Si un nom no es reconeix, es cau a
// Easing::linear.
constexpr int PAUSE_ENTER_MS = 400;
constexpr int PAUSE_EXIT_MS = 250;
constexpr const char* PAUSE_ENTER_CURVE = "outCubic";
constexpr const char* PAUSE_EXIT_CURVE = "inCubic";
} // namespace UI
#ifdef _DEBUG