feat(ui): captura F9, tema a data/ui.yaml i retocs estètics

Captura de pantalla amb F9: volca el canvas ARGB a
screenshots/scr_<timestamp>.png via stb_image_write.

Tot el tema d'UI (colors, timings, fonts, flag hud.bg_enabled)
passa a data/ui.yaml via UiConfig; els valors de Defaults::UI
queden com a fallbacks si el fitxer falta o li manca alguna clau.

Retocs estètics: paleta "uzebox" per a la UI, strings del HUD
(P1/P2/V/T/PIRAMIDE/SALA) traduïts a locale, i fons del marcador
opcional amb backdrop continu de franja per evitar el trencament
visual durant el shake o l'animació d'entrada dels panells.
This commit is contained in:
JailDesigner committed 2026-04-24 10:58:02 +02:00
1 parent 82b229222f
commit dc8a7723e3
26 files changed
+2591 -208

No files matched your search

+6 -3
View File
@@ -9,11 +9,14 @@ Checks:
- -bugprone-easily-swappable-parameters - -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions - -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays,-warnings-as-errors - -modernize-avoid-c-arrays,-warnings-as-errors
# Fals positiu recurrent en codi C de tercers (source/external/stb_vorbis.c). # Fals positius recurrents en codi C de tercers dins source/external/.
# HeaderFilterRegex no aplica a clang-analyzer-*, així que cal desactivar # HeaderFilterRegex no aplica a clang-analyzer-*, així que cal desactivar
# explícitament. El check inferència sobre variables locals de legacy C # explícitament. El primer: inferència sobre variables locals de legacy C
# genera reports que no reflecteixen bugs reals. # (stb_vorbis.c). El segon: ús de malloc() a stb_image_write.h, que
# l'analitzador marca com a possible "size=0" en rutes que a la pràctica
# no es donen.
- -clang-analyzer-core.UndefinedBinaryOperatorResult - -clang-analyzer-core.UndefinedBinaryOperatorResult
- -clang-analyzer-optin.portability.UnixAPI
WarningsAsErrors: '*' WarningsAsErrors: '*'
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables) # Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
+2
View File
@@ -56,6 +56,7 @@ set(APP_SOURCES
source/core/audio/audio_adapter.cpp source/core/audio/audio_adapter.cpp
source/core/audio/jail_audio.cpp source/core/audio/jail_audio.cpp
source/core/locale/locale.cpp source/core/locale/locale.cpp
source/core/ui/ui_config.cpp
source/core/input/key_config.cpp source/core/input/key_config.cpp
source/core/input/global_inputs.cpp source/core/input/global_inputs.cpp
source/core/input/input_state.cpp source/core/input/input_state.cpp
@@ -66,6 +67,7 @@ set(APP_SOURCES
source/core/rendering/notifier.cpp source/core/rendering/notifier.cpp
source/core/rendering/render_info.cpp source/core/rendering/render_info.cpp
source/core/rendering/gif.cpp source/core/rendering/gif.cpp
source/core/rendering/screenshot.cpp
source/core/rendering/surface.cpp source/core/rendering/surface.cpp
source/core/rendering/text.cpp source/core/rendering/text.cpp
source/core/rendering/sprite/sprite.cpp source/core/rendering/sprite/sprite.cpp
+6 -6
View File
@@ -29,18 +29,18 @@ keys:
- id: toggle_vsync - id: toggle_vsync
code: "F8" code: "F8"
desc: "Activa / desactiva VSync" desc: "Activa / desactiva VSync"
- id: menu_toggle - id: screenshot
code: "F9" code: "F9"
desc: "Obri / tanca el menú de servei" desc: "Captura de pantalla (PNG)"
- id: toggle_render_info - id: toggle_render_info
code: "F12"
desc: "Cicle de l'overlay de debug: apagat / dalt / baix"
- id: toggle_render_time
code: "F10" code: "F10"
desc: "Mostra o amaga el temps de joc dins de l'overlay" desc: "Cicle de l'overlay de debug: apagat / dalt / baix"
- id: pause - id: pause
code: "F11" code: "F11"
desc: "Pausa el joc (o botó START del mando)" desc: "Pausa el joc (o botó START del mando)"
- id: menu_toggle
code: "F12"
desc: "Obri / tanca el menú de servei"
- id: editor_toggle - id: editor_toggle
code: "9" code: "9"
desc: "Entra/surt del mode editor (només Debug)" desc: "Entra/surt del mode editor (només Debug)"
+9
View File
@@ -14,6 +14,7 @@ input:
shader_off: "Shaders desactivats" shader_off: "Shaders desactivats"
vsync_on: "VSync activat" vsync_on: "VSync activat"
vsync_off: "VSync desactivat" vsync_off: "VSync desactivat"
screenshot: "Captura"
shader: "Shader" shader: "Shader"
preset: "Preset" preset: "Preset"
scaling_disabled: "Escalat sense" scaling_disabled: "Escalat sense"
@@ -27,6 +28,14 @@ input:
editor_off: "Editor OFF" editor_off: "Editor OFF"
editor_save_error: "Editor: error guardant el mapa" editor_save_error: "Editor: error guardant el mapa"
hud:
p1: "P1"
p2: "P2"
lives: "Vides"
treasures: "Tres."
pyramid: "Piramide"
room: "Sala"
menu: menu:
titles: titles:
root: "Menú de servei" root: "Menú de servei"
+9
View File
@@ -14,6 +14,7 @@ input:
shader_off: "Shaders off" shader_off: "Shaders off"
vsync_on: "VSync on" vsync_on: "VSync on"
vsync_off: "VSync off" vsync_off: "VSync off"
screenshot: "Screenshot"
shader: "Shader" shader: "Shader"
preset: "Preset" preset: "Preset"
scaling_disabled: "Scaling off" scaling_disabled: "Scaling off"
@@ -27,6 +28,14 @@ input:
editor_off: "Editor OFF" editor_off: "Editor OFF"
editor_save_error: "Editor: map save failed" editor_save_error: "Editor: map save failed"
hud:
p1: "P1"
p2: "P2"
lives: "L"
treasures: "T"
pyramid: "PYRAMID"
room: "ROOM"
menu: menu:
titles: titles:
root: "Service menu" root: "Service menu"
+14 -14
View File
@@ -39,20 +39,20 @@ geometry:
data: data:
- [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
- [ 1,230,233,233,233,233,233,233,233,233,233,233,233,233,233,233,233,233,233,231, 1] - [ 1, 91,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105, 92, 1]
- [ 1,202,292,260,292,292,260,292,292,260,292,292,260,292,260,292,292,260,260,200, 1] - [ 1, 98,113,113, 95,113, 94,113,113,113,113,113,113,113, 94,113,102,113,113, 96, 1]
- [ 1,202, 32, 32, 2, 32, 65, 34, 34, 32, 65, 2, 32, 32, 65, 32, 34, 32, 32,200, 1] - [ 1, 98, 8, 8, 6, 8, 17, 8, 10, 8, 17, 8, 8, 8, 17, 8, 8, 8, 8, 96, 1]
- [ 1,202, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 34, 2, 2, 2, 32,200, 1] - [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1,202, 65, 32, 32, 33, 33, 32, 32, 32, 32, 32, 2, 32, 32, 32, 34, 32, 65,200, 1] - [ 1, 98, 17, 8, 9, 2, 8, 10, 8, 8, 8, 8, 10, 8, 8, 26, 8, 8, 17, 96, 1]
- [ 1,202, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 32,200, 1] - [ 1, 98, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1,202, 65, 32, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 34, 32, 32, 32, 65,200, 1] - [ 1, 98, 17, 8, 8, 26, 8, 8, 9, 8, 8, 8, 8, 8, 10, 8, 6, 8, 17, 96, 1]
- [ 1,202, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 32,200, 1] - [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1,202, 65, 32, 32, 32, 33, 33, 32, 32, 32, 32, 2, 32,181, 32, 32, 32, 65,200, 1] - [ 1, 98, 17, 8, 10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8, 10, 8, 17, 96, 1]
- [ 1,202, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 32,200, 1] - [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 6, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1,202, 65, 32, 33, 2, 34, 32, 32, 32, 32, 32, 32, 33, 32, 32, 34, 32, 65,200, 1] - [ 1, 98, 17, 8, 10, 8, 8, 10, 2, 8, 8, 26, 8, 8, 8, 8, 10, 8, 17, 96, 1]
- [ 1,202, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 32, 2, 2, 2, 32,200, 1] - [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1,202, 32, 32, 34, 34, 65, 32, 2, 32, 65, 32, 34, 33, 65, 32, 32, 32, 32,200, 1] - [ 1, 98, 8, 10, 6, 6, 17, 8, 8, 8, 17, 8, 9, 8, 17, 8, 2, 8, 8, 96, 1]
- [ 1,262,169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,263, 1] - [ 1, 99, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89,100, 1]
# Spawns d'entitats per a aquest nivell. Es llegeixen des de # Spawns d'entitats per a aquest nivell. Es llegeixen des de
# EntityManager::loadFromLevel al GameScene. Convencions: # EntityManager::loadFromLevel al GameScene. Convencions:
+259
View File
@@ -0,0 +1,259 @@
JASC-PAL
0100
256
0 0 0
36 0 0
72 0 0
109 0 0
145 0 0
182 0 0
218 0 0
255 0 0
0 36 0
36 36 0
72 36 0
109 36 0
145 36 0
182 36 0
218 36 0
255 36 0
0 72 0
36 72 0
72 72 0
109 72 0
145 72 0
182 72 0
218 72 0
255 72 0
0 109 0
36 109 0
72 109 0
109 109 0
145 109 0
182 109 0
218 109 0
255 109 0
0 145 0
36 145 0
72 145 0
109 145 0
145 145 0
182 145 0
218 145 0
255 145 0
0 182 0
36 182 0
72 182 0
109 182 0
145 182 0
182 182 0
218 182 0
255 182 0
0 218 0
36 218 0
72 218 0
109 218 0
145 218 0
182 218 0
218 218 0
255 218 0
0 255 0
36 255 0
72 255 0
109 255 0
145 255 0
182 255 0
218 255 0
255 255 0
0 0 85
36 0 85
72 0 85
109 0 85
145 0 85
182 0 85
218 0 85
255 0 85
0 36 85
36 36 85
72 36 85
109 36 85
145 36 85
182 36 85
218 36 85
255 36 85
0 72 85
36 72 85
72 72 85
109 72 85
145 72 85
182 72 85
218 72 85
255 72 85
0 109 85
36 109 85
72 109 85
109 109 85
145 109 85
182 109 85
218 109 85
255 109 85
0 145 85
36 145 85
72 145 85
109 145 85
145 145 85
182 145 85
218 145 85
255 145 85
0 182 85
36 182 85
72 182 85
109 182 85
145 182 85
182 182 85
218 182 85
255 182 85
0 218 85
36 218 85
72 218 85
109 218 85
145 218 85
182 218 85
218 218 85
255 218 85
0 255 85
36 255 85
72 255 85
109 255 85
145 255 85
182 255 85
218 255 85
255 255 85
0 0 170
36 0 170
72 0 170
109 0 170
145 0 170
182 0 170
218 0 170
255 0 170
0 36 170
36 36 170
72 36 170
109 36 170
145 36 170
182 36 170
218 36 170
255 36 170
0 72 170
36 72 170
72 72 170
109 72 170
145 72 170
182 72 170
218 72 170
255 72 170
0 109 170
36 109 170
72 109 170
109 109 170
145 109 170
182 109 170
218 109 170
255 109 170
0 145 170
36 145 170
72 145 170
109 145 170
145 145 170
182 145 170
218 145 170
255 145 170
0 182 170
36 182 170
72 182 170
109 182 170
145 182 170
182 182 170
218 182 170
255 182 170
0 218 170
36 218 170
72 218 170
109 218 170
145 218 170
182 218 170
218 218 170
255 218 170
0 255 170
36 255 170
72 255 170
109 255 170
145 255 170
182 255 170
218 255 170
255 255 170
0 0 255
36 0 255
72 0 255
109 0 255
145 0 255
182 0 255
218 0 255
255 0 255
0 36 255
36 36 255
72 36 255
109 36 255
145 36 255
182 36 255
218 36 255
255 36 255
0 72 255
36 72 255
72 72 255
109 72 255
145 72 255
182 72 255
218 72 255
255 72 255
0 109 255
36 109 255
72 109 255
109 109 255
145 109 255
182 109 255
218 109 255
255 109 255
0 145 255
36 145 255
72 145 255
109 145 255
145 145 255
182 145 255
218 145 255
255 145 255
0 182 255
36 182 255
72 182 255
109 182 255
145 182 255
182 182 255
218 182 255
255 182 255
0 218 255
36 218 255
72 218 255
109 218 255
145 218 255
182 218 255
218 218 255
255 218 255
0 255 255
36 255 255
72 255 255
109 255 255
145 255 255
182 255 255
218 255 255
255 255 255
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

+54
View File
@@ -0,0 +1,54 @@
# Tema de la UI: índexs de paleta, timings, fonts i flags visuals. Tota
# clau absent cau al valor compilat a Defaults::UI (veure defaults.hpp).
colors:
render_info_text: 58
render_info_shadow: 16
canvas_bg: 72
notification_text: 232
notification_bg: 64
exit_text: 71
exit_bg: 2
menu_bg: 2
menu_border: 232
menu_title: 237
menu_label: 58
menu_value: 232
menu_cursor: 71
menu_disabled: 16
hud_bg: 9
hud_label: 58
hud_value: 187
hud_flash: 237
timings:
notification_ms: 4000
exit_ms: 4000
fonts:
hud:
name: "8bithud"
gif: "data/fonts/8bithud.gif"
fnt: "data/fonts/8bithud.fnt"
kerning: 1
placeholder:
name: "ari-w9500-condensed-display"
gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
kerning: 1
menu_title:
name: "ari-w9500-display"
gif: "data/fonts/ari-w9500-display.gif"
fnt: "data/fonts/ari-w9500-display.fnt"
kerning: 0
menu_items:
name: "ari-w9500-condensed-display"
gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
kerning: 0
hud:
bg_enabled: true
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
+19 -9
View File
@@ -11,7 +11,9 @@
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include "core/rendering/render_info.hpp" #include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/screenshot.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/options_apply.hpp" #include "game/options_apply.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
@@ -24,7 +26,7 @@ namespace GlobalInputs {
// La lliguem a la durada de la notificació EXIT perquè el missatge i // La lliguem a la durada de la notificació EXIT perquè el missatge i
// la confirmació expiren alhora — si no, l'usuari podria veure el // 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). // text però ja no ser dins del rang de confirmació (o al revés).
constexpr float EXIT_WINDOW_S = static_cast<float>(Notification::EXIT.time_ms) / 1000.0F; // El valor concret viu a data/ui.yaml (timings.exit_ms) via UiConfig.
constexpr int MIN_ZOOM = 1; constexpr int MIN_ZOOM = 1;
constexpr int MAX_ZOOM = 6; constexpr int MAX_ZOOM = 6;
@@ -67,6 +69,18 @@ namespace GlobalInputs {
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off"); notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
} }
// Volca el canvas ARGB actual a PNG dins de `screenshots/`. Si ha
// fallat l'escriptura, el handler no mostra notificació (retorn buit).
void onScreenshot() {
const auto* buffer = Screen::get().getCanvasArgb();
const int W = Screen::get().getCanvasWidth();
const int H = Screen::get().getCanvasHeight();
const std::string FILENAME = Screenshot::save(buffer, W, H);
if (!FILENAME.empty()) {
Notification::notify(Locale::get().text("input.screenshot") + ' ' + FILENAME);
}
}
void onToggleShader() { void onToggleShader() {
const bool NEW_ENABLED = !Options::video().shader.enabled; const bool NEW_ENABLED = !Options::video().shader.enabled;
Options::setShaderEnabled(NEW_ENABLED); Options::setShaderEnabled(NEW_ENABLED);
@@ -140,17 +154,13 @@ namespace GlobalInputs {
Options::setVideoRenderInfoPosition(static_cast<Defaults::RenderInfoPosition>(NEXT)); Options::setVideoRenderInfoPosition(static_cast<Defaults::RenderInfoPosition>(NEXT));
} }
void onToggleRenderTime() {
Options::setVideoRenderInfoShowTime(!Options::video().render_info_show_time);
}
void onExit() { void onExit() {
if (exit_window_s > 0.0F) { if (exit_window_s > 0.0F) {
quit_requested = true; quit_requested = true;
return; return;
} }
exit_window_s = EXIT_WINDOW_S; exit_window_s = static_cast<float>(UiConfig::get().exitMs()) / 1000.0F;
Notification::notify(Locale::get().text("input.press_again_exit"), Notification::EXIT); Notification::notify(Locale::get().text("input.press_again_exit"), Notification::exitPreset());
} }
} // namespace } // namespace
@@ -171,6 +181,8 @@ namespace GlobalInputs {
onToggleShader(); onToggleShader();
} else if (SC == KeyConfig::get().scancode("toggle_vsync")) { } else if (SC == KeyConfig::get().scancode("toggle_vsync")) {
onToggleVSync(); onToggleVSync();
} else if (SC == KeyConfig::get().scancode("screenshot")) {
onScreenshot();
} else if (SC == KeyConfig::get().scancode("next_shader")) { } else if (SC == KeyConfig::get().scancode("next_shader")) {
onNextShader(); onNextShader();
} else if (SC == KeyConfig::get().scancode("next_preset")) { } else if (SC == KeyConfig::get().scancode("next_preset")) {
@@ -179,8 +191,6 @@ namespace GlobalInputs {
onNextScalingMode(); onNextScalingMode();
} else if (SC == KeyConfig::get().scancode("toggle_render_info")) { } else if (SC == KeyConfig::get().scancode("toggle_render_info")) {
onCycleRenderInfo(); onCycleRenderInfo();
} else if (SC == KeyConfig::get().scancode("toggle_render_time")) {
onToggleRenderTime();
} else if (SC == KeyConfig::get().scancode("exit")) { } else if (SC == KeyConfig::get().scancode("exit")) {
onExit(); onExit();
} }
+27 -17
View File
@@ -5,12 +5,13 @@
#include <string> #include <string>
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include "utils/defaults.hpp" #include "core/ui/ui_config.hpp"
// Temes per a les notificacions de pantalla. Un "preset" captura els dos // 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 // colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
// en mil·lisegons. Per a llançar una notificació amb un preset concret, // en mil·lisegons. Els valors concrets viuen a data/ui.yaml (carregat per
// cridar Notification::notify(text, preset); si s'omet, s'usa DEFAULT. // UiConfig); per llançar una notificació amb un preset concret, cridar
// Notification::notify(text, preset). Sense preset s'usa el DEFAULT.
namespace Notification { namespace Notification {
struct Preset { struct Preset {
@@ -19,27 +20,36 @@ namespace Notification {
int time_ms; int time_ms;
}; };
// Tema per defecte: text clar sobre fons fosc. Els valors concrets // Preset per defecte: text clar sobre fons fosc. Es construeix en cada
// viuen a `Defaults::UI` — tocar l'aparença global es fa allà. // crida a partir d'UiConfig perquè els valors no són constexpr: viuen a
constexpr Preset DEFAULT{ // data/ui.yaml i es carreguen a l'arrencada.
.text_color = Defaults::UI::NOTIFICATION_TEXT, [[nodiscard]] inline auto defaultPreset() -> Preset {
.bg_color = Defaults::UI::NOTIFICATION_BG, return {
.time_ms = Defaults::UI::NOTIFICATION_TIME_MS, .text_color = UiConfig::get().notificationText(),
.bg_color = UiConfig::get().notificationBg(),
.time_ms = UiConfig::get().notificationMs(),
}; };
}
// Preset per al missatge de confirmació d'eixida (ESC doble). Color // Preset per al missatge de confirmació d'eixida (ESC doble). Color
// d'accent per a distingir-lo d'una notificació informativa; la durada // d'accent per distingir-lo d'una notificació informativa; la durada
// coincideix amb la finestra durant la qual la segona pulsació d'ESC // coincideix amb la finestra durant la qual la segona pulsació d'ESC
// confirma el tancament — veure GlobalInputs::onExit. // confirma el tancament — veure GlobalInputs::onExit.
constexpr Preset EXIT{ [[nodiscard]] inline auto exitPreset() -> Preset {
.text_color = Defaults::UI::EXIT_TEXT, return {
.bg_color = Defaults::UI::EXIT_BG, .text_color = UiConfig::get().exitText(),
.time_ms = Defaults::UI::EXIT_TIME_MS, .bg_color = UiConfig::get().exitBg(),
.time_ms = UiConfig::get().exitMs(),
}; };
}
// Helper de conveniència: aplica un preset (per defecte DEFAULT) cridant // Helper de conveniència: aplica un preset (per defecte el DEFAULT)
// la API crua de Notifier. La direcció game → core és la correcta. // cridant la API crua de Notifier. La direcció game → core és correcta.
inline void notify(const std::string& text, const Preset& preset = DEFAULT) { inline void notify(const std::string& text) {
const auto P = defaultPreset();
Notifier::get().notify(text, P.text_color, P.bg_color, P.time_ms);
}
inline void notify(const std::string& text, const Preset& preset) {
Notifier::get().notify(text, preset.text_color, preset.bg_color, preset.time_ms); Notifier::get().notify(text, preset.text_color, preset.bg_color, preset.time_ms);
} }
+62
View File
@@ -0,0 +1,62 @@
#include "core/rendering/screenshot.hpp"
#include <chrono>
#include <cstdint>
#include <ctime>
#include <filesystem>
#include <string>
#include <vector>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "external/stb_image_write.h"
namespace Screenshot {
namespace {
// Crea la carpeta `screenshots/` si no existeix i torna la ruta
// completa al fitxer. El paràmetre `filename` rep el nom nu (sense
// directori) per poder mostrar-lo a la notificació.
auto generateFilePath(std::string& filename) -> std::string {
std::filesystem::create_directories("screenshots");
const auto NOW = std::chrono::system_clock::now();
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW);
const std::tm* tm = std::localtime(&TIME);
char timestamp[20];
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm);
filename = std::string("scr_") + timestamp + ".png";
return "screenshots/" + filename;
}
// Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write
// espera RGBA en ordre byte (little-endian: R al byte baix, A al byte alt).
auto writePng(std::uint32_t* buffer, int width, int height, const std::string& filepath) -> bool {
const int TOTAL = width * height;
for (int i = 0; i < TOTAL; ++i) {
const std::uint32_t P = buffer[i];
const std::uint32_t A = (P >> 24) & 0xFF;
const std::uint32_t R = (P >> 16) & 0xFF;
const std::uint32_t G = (P >> 8) & 0xFF;
const std::uint32_t B = P & 0xFF;
buffer[i] = R | (G << 8) | (B << 16) | (A << 24);
}
// NOLINTNEXTLINE(clang-analyzer-optin.portability.UnixAPI) — fals positiu dins de stb_image_write
return stbi_write_png(filepath.c_str(), width, height, 4, buffer, width * 4) != 0;
}
} // namespace
auto save(const std::uint32_t* buffer, int width, int height) -> std::string {
// Còpia local per no tocar el canvas original durant la conversió.
std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height));
std::string filename;
const std::string FILEPATH = generateFilePath(filename);
return writePng(copy.data(), width, height, FILEPATH) ? filename : "";
}
} // namespace Screenshot
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
#include <string>
// Volca un buffer ARGB8888 a un fitxer PNG dins de la carpeta `screenshots/`
// (relativa al CWD). El nom es genera amb un timestamp i la funció retorna
// el nom del fitxer creat, o una cadena buida si l'escriptura ha fallat.
namespace Screenshot {
auto save(const std::uint32_t* buffer, int width, int height) -> std::string;
} // namespace Screenshot
+24 -28
View File
@@ -25,6 +25,7 @@
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp" #include "game/debug_config.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/game_scene.hpp" #include "game/scenes/game_scene.hpp"
@@ -32,7 +33,6 @@
#include "game/scenes/logo_scene.hpp" #include "game/scenes/logo_scene.hpp"
#include "game/scenes/title_scene.hpp" #include "game/scenes/title_scene.hpp"
#include "game/ui/service_menu.hpp" #include "game/ui/service_menu.hpp"
#include "game/ui/ui_palette.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
Director::Director() { Director::Director() {
@@ -97,15 +97,19 @@ Director::Director() {
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame. // Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
KeyConfig::get().load("data/input/keys.yaml"); KeyConfig::get().load("data/input/keys.yaml");
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
// data/ui.yaml cau al valor compilat a Defaults::UI.
UiConfig::get().load("data/ui.yaml");
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els // Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els // overlays perquè tots els reaprofiten sense duplicar la lògica. Els
// noms de font viuen a Defaults::UI::Font (un únic lloc per canviar-los). // noms de font viuen al fitxer data/ui.yaml via UiConfig.
Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal"); Resource::Cache::get().loadPalette("ui", "data/palettes/uzebox.pal");
const std::string HUD_FONT_GIF_KEY = std::string(Defaults::UI::Font::HUD_NAME) + ".gif"; const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif";
Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, Defaults::UI::Font::HUD_GIF); Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, UiConfig::get().hudFont().gif);
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui")); Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
const std::string PLACEHOLDER_FONT_GIF_KEY = std::string(Defaults::UI::Font::PLACEHOLDER_NAME) + ".gif"; const std::string PLACEHOLDER_FONT_GIF_KEY = UiConfig::get().placeholderFont().name + ".gif";
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, Defaults::UI::Font::PLACEHOLDER_GIF); Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, UiConfig::get().placeholderFont().gif);
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui")); Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
// Música de la piràmide 1 (primer i únic track de moment). Carreguem ací // Música de la piràmide 1 (primer i únic track de moment). Carreguem ací
@@ -126,41 +130,33 @@ Director::Director() {
// i la injectem als consumidors (Notifier, RenderInfo) per a que cap // i la injectem als consumidors (Notifier, RenderInfo) per a que cap
// d'ells hagi de conèixer els noms dels recursos concrets. // d'ells hagi de conèixer els noms dels recursos concrets.
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY); auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
auto hud_text = std::make_shared<Text>(hud_font_surface, Defaults::UI::Font::HUD_FNT); auto hud_text = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
hud_text->setDefaultKerning(Defaults::UI::Font::HUD_KERNING); hud_text->setDefaultKerning(UiConfig::get().hudFont().kerning);
const auto& ui_palette = Resource::Cache::get().getPalette("ui"); const auto& ui_palette = Resource::Cache::get().getPalette("ui");
RenderInfo::get().init(Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW), hud_text); RenderInfo::get().init(Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()), hud_text);
Notifier::get().init(hud_text, ui_palette); Notifier::get().init(hud_text, ui_palette);
// Font dels placeholders (LOGO / TITLE / INTRO). És una font distinta a // Font dels placeholders (LOGO / TITLE / INTRO). És una font distinta a
// la de HUD perquè els placeholders desapareixeran amb les seues escenes // la de HUD perquè els placeholders desapareixeran amb les seues escenes
// finals; mentrestant tenen estètica pròpia. // finals; mentrestant tenen estètica pròpia.
auto placeholder_font_surface = Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY); auto placeholder_font_surface = Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY);
auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, Defaults::UI::Font::PLACEHOLDER_FNT); auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, UiConfig::get().placeholderFont().fnt);
placeholder_text->setDefaultKerning(Defaults::UI::Font::PLACEHOLDER_KERNING); placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
// Font del menú de servei (F9). El títol va amb la variant `display` // Font del menú de servei (F9). El títol va amb la variant `display`
// i els items amb `condensed-display` (més compacte perquè caben més // i els items amb `condensed-display` (més compacte perquè caben més
// files). Els dos noms i kernings viuen a `Defaults::UI::Font`. // files). Els dos noms i kernings viuen a data/ui.yaml via UiConfig.
auto load_menu_font = [&](const char* name, const char* gif, const char* fnt, int kerning) { auto load_menu_font = [&](const UiConfig::Font& font) {
const std::string KEY = std::string(name) + ".gif"; const std::string KEY = font.name + ".gif";
Resource::Cache::get().loadSurface(KEY, gif); Resource::Cache::get().loadSurface(KEY, font.gif);
Resource::Cache::get().getSurface(KEY)->setPalette(Resource::Cache::get().getPalette("ui")); Resource::Cache::get().getSurface(KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
auto text = std::make_shared<Text>(Resource::Cache::get().getSurface(KEY), fnt); auto text = std::make_shared<Text>(Resource::Cache::get().getSurface(KEY), font.fnt);
text->setDefaultKerning(kerning); text->setDefaultKerning(font.kerning);
return text; return text;
}; };
auto menu_title_text = load_menu_font( auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont());
Defaults::UI::Font::MENU_TITLE_NAME, auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont());
Defaults::UI::Font::MENU_TITLE_GIF,
Defaults::UI::Font::MENU_TITLE_FNT,
Defaults::UI::Font::MENU_TITLE_KERNING);
auto menu_items_text = load_menu_font(
Defaults::UI::Font::MENU_ITEMS_NAME,
Defaults::UI::Font::MENU_ITEMS_GIF,
Defaults::UI::Font::MENU_ITEMS_FNT,
Defaults::UI::Font::MENU_ITEMS_KERNING);
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette); ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
// Overlay hook: Screen::present() cridarà aquesta lambda al principi, // Overlay hook: Screen::present() cridarà aquesta lambda al principi,
+139
View File
@@ -0,0 +1,139 @@
#include "core/ui/ui_config.hpp"
#include <iostream>
#include <string>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp"
namespace {
// Lector de camp amb fallback: si la clau existeix al node, escriu a
// `dst` el valor convertit; si no, deixa `dst` com estava (que al
// constructor ja hem inicialitzat amb Defaults::UI).
template <typename T>
void readField(const fkyaml::node& node, const char* key, T& dst) {
if (node.contains(key)) { dst = node[key].get_value<T>(); }
}
// Variant per a Uint8 — fkyaml no serialitza a Uint8 directament, cal
// passar per int i encaixar al rang 0..255 (valors fora de rang es
// trunquen i s'avisen).
void readColorIndex(const fkyaml::node& node, const char* key, Uint8& dst) {
if (!node.contains(key)) { return; }
const int VALUE = node[key].get_value<int>();
if (VALUE < 0 || VALUE > 255) {
std::cerr << "UiConfig: índex '" << key << "'=" << VALUE << " fora de rang 0..255 — ignorat\n";
return;
}
dst = static_cast<Uint8>(VALUE);
}
void readFont(const fkyaml::node& node, const char* key, UiConfig::Font& dst) {
if (!node.contains(key)) { return; }
const auto& f = node[key];
readField(f, "name", dst.name);
readField(f, "gif", dst.gif);
readField(f, "fnt", dst.fnt);
readField(f, "kerning", dst.kerning);
}
} // namespace
UiConfig::UiConfig()
: render_info_text_(Defaults::UI::RENDER_INFO_TEXT),
render_info_shadow_(Defaults::UI::RENDER_INFO_SHADOW),
canvas_bg_(Defaults::UI::CANVAS_BG),
notification_text_(Defaults::UI::NOTIFICATION_TEXT),
notification_bg_(Defaults::UI::NOTIFICATION_BG),
exit_text_(Defaults::UI::EXIT_TEXT),
exit_bg_(Defaults::UI::EXIT_BG),
menu_bg_(Defaults::UI::MENU_BG),
menu_border_(Defaults::UI::MENU_BORDER),
menu_title_(Defaults::UI::MENU_TITLE),
menu_label_(Defaults::UI::MENU_LABEL),
menu_value_(Defaults::UI::MENU_VALUE),
menu_cursor_(Defaults::UI::MENU_CURSOR),
menu_disabled_(Defaults::UI::MENU_DISABLED),
hud_bg_(Defaults::UI::HUD_BG),
hud_label_(Defaults::UI::HUD_LABEL),
hud_value_(Defaults::UI::HUD_VALUE),
hud_flash_(Defaults::UI::HUD_FLASH),
notification_ms_(Defaults::UI::NOTIFICATION_TIME_MS),
exit_ms_(Defaults::UI::EXIT_TIME_MS),
hud_entry_ms_(Defaults::UI::HUD_ENTRY_MS),
hud_flash_ms_(Defaults::UI::HUD_FLASH_MS),
hud_shake_ms_(Defaults::UI::HUD_SHAKE_MS),
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},
menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING} {
}
auto UiConfig::get() -> UiConfig& {
static UiConfig instance_;
return instance_;
}
void UiConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) {
std::cerr << "UiConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n";
return;
}
try {
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (yaml.contains("colors")) {
const auto& c = yaml["colors"];
readColorIndex(c, "render_info_text", render_info_text_);
readColorIndex(c, "render_info_shadow", render_info_shadow_);
readColorIndex(c, "canvas_bg", canvas_bg_);
readColorIndex(c, "notification_text", notification_text_);
readColorIndex(c, "notification_bg", notification_bg_);
readColorIndex(c, "exit_text", exit_text_);
readColorIndex(c, "exit_bg", exit_bg_);
readColorIndex(c, "menu_bg", menu_bg_);
readColorIndex(c, "menu_border", menu_border_);
readColorIndex(c, "menu_title", menu_title_);
readColorIndex(c, "menu_label", menu_label_);
readColorIndex(c, "menu_value", menu_value_);
readColorIndex(c, "menu_cursor", menu_cursor_);
readColorIndex(c, "menu_disabled", menu_disabled_);
readColorIndex(c, "hud_bg", hud_bg_);
readColorIndex(c, "hud_label", hud_label_);
readColorIndex(c, "hud_value", hud_value_);
readColorIndex(c, "hud_flash", hud_flash_);
}
if (yaml.contains("timings")) {
const auto& t = yaml["timings"];
readField(t, "notification_ms", notification_ms_);
readField(t, "exit_ms", exit_ms_);
}
if (yaml.contains("fonts")) {
const auto& f = yaml["fonts"];
readFont(f, "hud", hud_font_);
readFont(f, "placeholder", placeholder_font_);
readFont(f, "menu_title", menu_title_font_);
readFont(f, "menu_items", menu_items_font_);
}
if (yaml.contains("hud")) {
const auto& h = yaml["hud"];
readField(h, "bg_enabled", hud_bg_enabled_);
readField(h, "entry_ms", hud_entry_ms_);
readField(h, "flash_ms", hud_flash_ms_);
readField(h, "shake_ms", hud_shake_ms_);
}
std::cout << "UiConfig: carregat des de " << file_path << '\n';
} catch (const std::exception& e) {
std::cerr << "UiConfig: error parsejant " << file_path << ": " << e.what() << '\n';
}
}
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
// Configuració del tema UI (índexs de paleta, timings, fonts i flags)
// carregada des de data/ui.yaml. Cada camp s'inicialitza amb el valor
// de Defaults::UI com a fallback: si el fitxer falta o li manca una
// clau, el joc s'executa amb el tema compilat.
//
// Patró idèntic a Locale i KeyConfig: Meyers singleton + load(path).
// No hi ha hot-reload — els canvis al YAML requereixen reiniciar.
class UiConfig {
public:
struct Font {
std::string name;
std::string gif;
std::string fnt;
int kerning;
};
static auto get() -> UiConfig&;
UiConfig(const UiConfig&) = delete;
UiConfig(UiConfig&&) = delete;
auto operator=(const UiConfig&) -> UiConfig& = delete;
auto operator=(UiConfig&&) -> UiConfig& = delete;
void load(const std::string& file_path);
// Colors — índexs sobre la paleta "ui" (Resource::Cache).
[[nodiscard]] auto renderInfoText() const -> Uint8 { return render_info_text_; }
[[nodiscard]] auto renderInfoShadow() const -> Uint8 { return render_info_shadow_; }
[[nodiscard]] auto canvasBg() const -> Uint8 { return canvas_bg_; }
[[nodiscard]] auto notificationText() const -> Uint8 { return notification_text_; }
[[nodiscard]] auto notificationBg() const -> Uint8 { return notification_bg_; }
[[nodiscard]] auto exitText() const -> Uint8 { return exit_text_; }
[[nodiscard]] auto exitBg() const -> Uint8 { return exit_bg_; }
[[nodiscard]] auto menuBg() const -> Uint8 { return menu_bg_; }
[[nodiscard]] auto menuBorder() const -> Uint8 { return menu_border_; }
[[nodiscard]] auto menuTitle() const -> Uint8 { return menu_title_; }
[[nodiscard]] auto menuLabel() const -> Uint8 { return menu_label_; }
[[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; }
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
[[nodiscard]] auto hudLabel() const -> Uint8 { return hud_label_; }
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; }
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
// 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_; }
// Fonts de UI.
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
[[nodiscard]] auto placeholderFont() const -> const Font& { return placeholder_font_; }
[[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; }
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
// Flags visuals.
[[nodiscard]] auto hudBgEnabled() const -> bool { return hud_bg_enabled_; }
private:
UiConfig();
~UiConfig() = default;
Uint8 render_info_text_;
Uint8 render_info_shadow_;
Uint8 canvas_bg_;
Uint8 notification_text_;
Uint8 notification_bg_;
Uint8 exit_text_;
Uint8 exit_bg_;
Uint8 menu_bg_;
Uint8 menu_border_;
Uint8 menu_title_;
Uint8 menu_label_;
Uint8 menu_value_;
Uint8 menu_cursor_;
Uint8 menu_disabled_;
Uint8 hud_bg_;
Uint8 hud_label_;
Uint8 hud_value_;
Uint8 hud_flash_;
int notification_ms_;
int exit_ms_;
int hud_entry_ms_;
int hud_flash_ms_;
int hud_shake_ms_;
Font hud_font_;
Font placeholder_font_;
Font menu_title_font_;
Font menu_items_font_;
bool hud_bg_enabled_{false};
};
+1724
View File
File diff suppressed because it is too large. Load diff
+11 -11
View File
@@ -23,6 +23,7 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/entities/entity.hpp" #include "game/entities/entity.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
@@ -30,7 +31,6 @@
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/ui/hud.hpp" #include "game/ui/hud.hpp"
#include "game/ui/ui_palette.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
@@ -144,11 +144,11 @@ GameScene::GameScene(Config config)
// la paleta "ui" ja precarregades pel Director — no toca disc des // la paleta "ui" ja precarregades pel Director — no toca disc des
// d'aquí. Subscrit al bus per reaccionar a `player_hit`, // d'aquí. Subscrit al bus per reaccionar a `player_hit`,
// `level_complete`, etc. amb shake/flash i futurs canvis de mode. // `level_complete`, etc. amb shake/flash i futurs canvis de mode.
const std::string HUD_FONT_GIF_KEY = std::string(Defaults::UI::Font::HUD_NAME) + ".gif"; const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif";
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY); auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
auto hud_font = std::make_shared<Text>(hud_font_surface, Defaults::UI::Font::HUD_FNT); auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
hud_font->setDefaultKerning(Defaults::UI::Font::HUD_KERNING); hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, Resource::Cache::get().getPalette("ui")); hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font);
// Música de fons en bucle infinit. El track ja està al Resource::Cache // Música de fons en bucle infinit. El track ja està al Resource::Cache
// (preload al Director), així que playMusic només mou el punter cap a // (preload al Director), així que playMusic només mou el punter cap a
@@ -159,10 +159,10 @@ GameScene::GameScene(Config config)
// Font de l'overlay de pausa: reutilitzem la Surface ja cachejada al // Font de l'overlay de pausa: reutilitzem la Surface ja cachejada al
// Director (PLACEHOLDER_NAME + ".gif"). Text només parseja el .fnt a // Director (PLACEHOLDER_NAME + ".gif"). Text només parseja el .fnt a
// l'arrencada de l'escena, un sol cop. // l'arrencada de l'escena, un sol cop.
const std::string PAUSE_FONT_KEY = std::string(Defaults::UI::Font::PLACEHOLDER_NAME) + ".gif"; const std::string PAUSE_FONT_KEY = UiConfig::get().placeholderFont().name + ".gif";
auto pause_surface = Resource::Cache::get().getSurface(PAUSE_FONT_KEY); 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_ = std::make_shared<Text>(pause_surface, UiConfig::get().placeholderFont().fnt);
pause_font_->setDefaultKerning(Defaults::UI::Font::PLACEHOLDER_KERNING); pause_font_->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
} }
GameScene::~GameScene() = default; GameScene::~GameScene() = default;
@@ -252,12 +252,12 @@ void GameScene::update(int delta_ms) {
// sola i quan es despausa ja està al seu lloc, igual que el Notifier. // sola i quan es despausa ja està al seu lloc, igual que el Notifier.
if (hud_) { hud_->update(delta_ms); } if (hud_) { hud_->update(delta_ms); }
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
Notifier::get().update(delta_ms); Notifier::get().update(delta_ms);
} }
void GameScene::render() { void GameScene::render() {
const auto BG_ARGB = Resource::Cache::get().getPalette("ui")[UiColor::CANVAS_BG]; const auto BG_ARGB = Resource::Cache::get().getPalette("ui")[UiConfig::get().canvasBg()];
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BG_ARGB); Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BG_ARGB);
// Mapa visual: textura prerenderitzada del tilemap. Els sarcòfags // Mapa visual: textura prerenderitzada del tilemap. Els sarcòfags
@@ -334,7 +334,7 @@ void GameScene::render() {
Y, Y,
TEXT, TEXT,
PAUSE_KERNING, PAUSE_KERNING,
Defaults::UI::NOTIFICATION_TEXT, UiConfig::get().notificationText(),
STROKE_DIST, STROKE_DIST,
OUTLINE_WHITE); OUTLINE_WHITE);
} }
+4 -4
View File
@@ -14,9 +14,9 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/placeholder_input.hpp" #include "game/scenes/placeholder_input.hpp"
#include "game/ui/ui_palette.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
@@ -56,7 +56,7 @@ void IntroScene::update(int delta_ms) {
} }
Notifier::get().update(delta_ms); Notifier::get().update(delta_ms);
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
} }
void IntroScene::render() { void IntroScene::render() {
@@ -70,13 +70,13 @@ void IntroScene::render() {
GameCanvas::CENTER_Y + NAME_Y_OFFSET, GameCanvas::CENTER_Y + NAME_Y_OFFSET,
SCENE_NAME, SCENE_NAME,
1, 1,
Defaults::UI::NOTIFICATION_TEXT); UiConfig::get().notificationText());
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
GameCanvas::CENTER_X, GameCanvas::CENTER_X,
GameCanvas::CENTER_Y + TIMER_Y_OFFSET, GameCanvas::CENTER_Y + TIMER_Y_OFFSET,
std::to_string(SECS_LEFT), std::to_string(SECS_LEFT),
1, 1,
Defaults::UI::NOTIFICATION_TEXT); UiConfig::get().notificationText());
} }
Notifier::get().render(); Notifier::get().render();
+4 -4
View File
@@ -14,9 +14,9 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/placeholder_input.hpp" #include "game/scenes/placeholder_input.hpp"
#include "game/ui/ui_palette.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
@@ -57,7 +57,7 @@ void LogoScene::update(int delta_ms) {
} }
Notifier::get().update(delta_ms); Notifier::get().update(delta_ms);
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
} }
void LogoScene::render() { void LogoScene::render() {
@@ -71,13 +71,13 @@ void LogoScene::render() {
GameCanvas::CENTER_Y + NAME_Y_OFFSET, GameCanvas::CENTER_Y + NAME_Y_OFFSET,
SCENE_NAME, SCENE_NAME,
1, 1,
Defaults::UI::NOTIFICATION_TEXT); UiConfig::get().notificationText());
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
GameCanvas::CENTER_X, GameCanvas::CENTER_X,
GameCanvas::CENTER_Y + TIMER_Y_OFFSET, GameCanvas::CENTER_Y + TIMER_Y_OFFSET,
std::to_string(SECS_LEFT), std::to_string(SECS_LEFT),
1, 1,
Defaults::UI::NOTIFICATION_TEXT); UiConfig::get().notificationText());
} }
Notifier::get().render(); Notifier::get().render();
+4 -4
View File
@@ -14,9 +14,9 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/placeholder_input.hpp" #include "game/scenes/placeholder_input.hpp"
#include "game/ui/ui_palette.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
@@ -56,7 +56,7 @@ void TitleScene::update(int delta_ms) {
} }
Notifier::get().update(delta_ms); Notifier::get().update(delta_ms);
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
} }
void TitleScene::render() { void TitleScene::render() {
@@ -70,13 +70,13 @@ void TitleScene::render() {
GameCanvas::CENTER_Y + NAME_Y_OFFSET, GameCanvas::CENTER_Y + NAME_Y_OFFSET,
SCENE_NAME, SCENE_NAME,
1, 1,
Defaults::UI::NOTIFICATION_TEXT); UiConfig::get().notificationText());
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
GameCanvas::CENTER_X, GameCanvas::CENTER_X,
GameCanvas::CENTER_Y + TIMER_Y_OFFSET, GameCanvas::CENTER_Y + TIMER_Y_OFFSET,
std::to_string(SECS_LEFT), std::to_string(SECS_LEFT),
1, 1,
Defaults::UI::NOTIFICATION_TEXT); UiConfig::get().notificationText());
} }
Notifier::get().render(); Notifier::get().render();
+47 -41
View File
@@ -11,22 +11,21 @@
#include <utility> #include <utility>
#include <variant> #include <variant>
#include "core/locale/locale.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "game/ui/ui_palette.hpp" #include "core/resources/resource_cache.hpp"
#include "core/ui/ui_config.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/easing.hpp" #include "utils/easing.hpp"
namespace { namespace {
// Colors HUD reaprofitats de la paleta UI: les etiquetes ("P1", "V", // Els colors del HUD viuen a data/ui.yaml (colors.hud_label/value/flash)
// "T") usen un to apagat per crear jerarquia visual respecte als // via UiConfig. Les etiquetes ("P1", "V", "T") usen un to apagat per
// valors. El flash temporal usa un to càlid (groc del títol del menú). // crear jerarquia visual respecte als valors; el flash temporal usa un
constexpr Uint8 LABEL_COLOR = UiColor::MENU_LABEL; // 47 // to càlid que contrasta amb el valor normal.
constexpr Uint8 VALUE_COLOR = UiColor::MENU_VALUE; // 197
constexpr Uint8 BG_COLOR = UiColor::MENU_BG; // 40
constexpr Uint8 FLASH_COLOR = Defaults::UI::MENU_TITLE; // 213
// Amplada nominal d'una cel·la de dígit. Imposem monoespaiat als // Amplada nominal d'una cel·la de dígit. Imposem monoespaiat als
// comptadors per evitar jitter visible quan canvien valors (Notifier // comptadors per evitar jitter visible quan canvien valors (Notifier
@@ -66,16 +65,18 @@ namespace {
} // namespace } // namespace
Hud::Hud(EventBus& bus, std::shared_ptr<Text> font, const Palette& ui_palette) Hud::Hud(EventBus& bus, std::shared_ptr<Text> font)
: bus_(bus), : bus_(bus),
font_(std::move(font)), font_(std::move(font)) {
ui_palette_(ui_palette) {
// Geometria fixa dels tres blocs. Sumen exactament GameCanvas::WIDTH. // Geometria fixa dels tres blocs. Sumen exactament GameCanvas::WIDTH.
// La durada de l'animació d'entrada ve de data/ui.yaml via UiConfig.
const int ENTRY_MS = UiConfig::get().hudEntryMs();
blocks_[0] = Block{ blocks_[0] = Block{
.x = 0, .x = 0,
.y = 0, .y = 0,
.w = LEFT_W, .w = LEFT_W,
.h = HEIGHT_PX, .h = HEIGHT_PX,
.entry_ms_left = ENTRY_MS,
.entry_offset_x = -LEFT_W, .entry_offset_x = -LEFT_W,
.entry_offset_y = 0, .entry_offset_y = 0,
}; };
@@ -84,6 +85,7 @@ Hud::Hud(EventBus& bus, std::shared_ptr<Text> font, const Palette& ui_palette)
.y = 0, .y = 0,
.w = CENTER_W, .w = CENTER_W,
.h = HEIGHT_PX, .h = HEIGHT_PX,
.entry_ms_left = ENTRY_MS,
.entry_offset_x = 0, .entry_offset_x = 0,
.entry_offset_y = -HEIGHT_PX, .entry_offset_y = -HEIGHT_PX,
}; };
@@ -92,6 +94,7 @@ Hud::Hud(EventBus& bus, std::shared_ptr<Text> font, const Palette& ui_palette)
.y = 0, .y = 0,
.w = RIGHT_W, .w = RIGHT_W,
.h = HEIGHT_PX, .h = HEIGHT_PX,
.entry_ms_left = ENTRY_MS,
.entry_offset_x = RIGHT_W, .entry_offset_x = RIGHT_W,
.entry_offset_y = 0, .entry_offset_y = 0,
}; };
@@ -113,8 +116,8 @@ Hud::Hud(EventBus& bus, std::shared_ptr<Text> font, const Palette& ui_palette)
constexpr std::size_t IDX = 0; constexpr std::size_t IDX = 0;
if (players_[IDX].lives > 0) { --players_[IDX].lives; } if (players_[IDX].lives > 0) { --players_[IDX].lives; }
auto& blk = blocks_[IDX]; // P1 → bloc esquerre auto& blk = blocks_[IDX]; // P1 → bloc esquerre
blk.flash_ms_left = FLASH_MS; blk.flash_ms_left = UiConfig::get().hudFlashMs();
blk.shake_ms_left = SHAKE_MS; blk.shake_ms_left = UiConfig::get().hudShakeMs();
blk.shake_intensity = 1.5F; blk.shake_intensity = 1.5F;
std::cout << "[HUD] player_hit dmg=" << (dmg != nullptr ? *dmg : 0) std::cout << "[HUD] player_hit dmg=" << (dmg != nullptr ? *dmg : 0)
<< " lives=" << players_[IDX].lives << '\n'; << " lives=" << players_[IDX].lives << '\n';
@@ -165,7 +168,7 @@ auto Hud::computeOffset(const Block& b) -> SDL_FPoint {
// Animació d'entrada: t va de 0 (recent iniciat) a 1 (acabada). Amb // Animació d'entrada: t va de 0 (recent iniciat) a 1 (acabada). Amb
// outCubic, el bloc arrenca ràpid i s'atura suaument al seu lloc. // outCubic, el bloc arrenca ràpid i s'atura suaument al seu lloc.
if (b.entry_ms_left > 0) { if (b.entry_ms_left > 0) {
const float T = 1.0F - (static_cast<float>(b.entry_ms_left) / static_cast<float>(ENTRY_MS)); const float T = 1.0F - (static_cast<float>(b.entry_ms_left) / static_cast<float>(UiConfig::get().hudEntryMs()));
const float E = Easing::outCubic(T); const float E = Easing::outCubic(T);
const float REMAIN = 1.0F - E; // 1 al principi → 0 al final const float REMAIN = 1.0F - E; // 1 al principi → 0 al final
off.x += static_cast<float>(b.entry_offset_x) * REMAIN; off.x += static_cast<float>(b.entry_offset_x) * REMAIN;
@@ -174,7 +177,7 @@ auto Hud::computeOffset(const Block& b) -> SDL_FPoint {
// Shake: amplitud decau linealment amb el temps restant. // Shake: amplitud decau linealment amb el temps restant.
if (b.shake_ms_left > 0) { if (b.shake_ms_left > 0) {
const float DECAY = static_cast<float>(b.shake_ms_left) / static_cast<float>(SHAKE_MS); const float DECAY = static_cast<float>(b.shake_ms_left) / static_cast<float>(UiConfig::get().hudShakeMs());
const auto JITTER = shakeOffset(b.shake_intensity * DECAY, b.shake_ms_left); const auto JITTER = shakeOffset(b.shake_intensity * DECAY, b.shake_ms_left);
off.x += JITTER.x; off.x += JITTER.x;
off.y += JITTER.y; off.y += JITTER.y;
@@ -184,6 +187,14 @@ auto Hud::computeOffset(const Block& b) -> SDL_FPoint {
} }
void Hud::render() const { void Hud::render() const {
// Backdrop de la franja del HUD. Es pinta abans dels tres blocs perquè,
// quan un bloc es mou (entry slide o shake), la zona que deixa al
// descobert segueix mostrant el mateix color — en lloc del tilemap de
// sota, que causaria un trencament visual.
if (UiConfig::get().hudBgEnabled()) {
const auto& pal = Resource::Cache::get().getPalette("ui");
Screen::get().fillRect(0, 0, LEFT_W + CENTER_W + RIGHT_W, HEIGHT_PX, pal[UiConfig::get().hudBg()]);
}
renderBlock(BlockId::LEFT); renderBlock(BlockId::LEFT);
renderBlock(BlockId::CENTER); renderBlock(BlockId::CENTER);
renderBlock(BlockId::RIGHT); renderBlock(BlockId::RIGHT);
@@ -196,16 +207,12 @@ void Hud::renderBlock(BlockId id) const {
const int X = b.x + static_cast<int>(std::round(OFF.x)); const int X = b.x + static_cast<int>(std::round(OFF.x));
const int Y = b.y + static_cast<int>(std::round(OFF.y)); const int Y = b.y + static_cast<int>(std::round(OFF.y));
// Fons sòlid del bloc (mateixa convenció que `Notifier::render`):
// ARGB directe al canvas, sense tocar surfaces paletades.
Screen::get().fillRect(X, Y, b.w, b.h, ui_palette_[BG_COLOR]);
switch (b.mode) { switch (b.mode) {
case Mode::IDLE: case Mode::IDLE:
if (id == BlockId::LEFT) { if (id == BlockId::LEFT) {
renderIdleSide(X, Y, b.w, b, 0, "P1"); renderIdleSide(X, Y, b.w, b, 0, Locale::get().text("hud.p1"));
} else if (id == BlockId::RIGHT) { } else if (id == BlockId::RIGHT) {
renderIdleSide(X, Y, b.w, b, 1, "P2"); renderIdleSide(X, Y, b.w, b, 1, Locale::get().text("hud.p2"));
} else { } else {
renderIdleCenter(X, Y, b.w); renderIdleCenter(X, Y, b.w);
} }
@@ -218,24 +225,23 @@ void Hud::renderBlock(BlockId id) const {
} }
} }
void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const char* label) const { void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const std::string& label) const {
const auto& p = players_[static_cast<std::size_t>(player_index)]; const auto& p = players_[static_cast<std::size_t>(player_index)];
const Uint8 SCORE_C = (blk.flash_ms_left > 0) ? FLASH_COLOR : VALUE_COLOR; 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 LBL{label};
const std::string SCORE = formatScore(p.score); const std::string SCORE = formatScore(p.score);
const int LBL_W = font_->length(LBL); const int LBL_W = font_->length(label);
const int SC_W = font_->lengthMonoDigits(SCORE, MONO_DIGIT_W); 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 + LABEL_GAP + SC_W;
int xx = x + ((w - LINE1_W) / 2); int xx = x + ((w - LINE1_W) / 2);
font_->writeColored(xx, y + LINE1_Y, LBL, LABEL_COLOR); font_->writeColored(xx, y + LINE1_Y, label, UiConfig::get().hudLabel());
xx += LBL_W + LABEL_GAP; xx += LBL_W + LABEL_GAP;
font_->writeMonoDigits(xx, y + LINE1_Y, SCORE, SCORE_C, MONO_DIGIT_W); font_->writeMonoDigits(xx, y + LINE1_Y, SCORE, SCORE_C, MONO_DIGIT_W);
// --- Línia 2: "V <vides> T <tresors>" centrada --- // --- Línia 2: "<vides> <n> <tresors> <n>" centrada ---
const std::string V_LBL{"V"}; const std::string V_LBL = Locale::get().text("hud.lives");
const std::string T_LBL{"T"}; const std::string T_LBL = Locale::get().text("hud.treasures");
const std::string V_VAL = std::to_string(std::clamp(p.lives, 0, 9)); const std::string V_VAL = std::to_string(std::clamp(p.lives, 0, 9));
const std::string T_VAL = formatTwoDigits(p.treasures); const std::string T_VAL = formatTwoDigits(p.treasures);
const int V_LW = font_->length(V_LBL); const int V_LW = font_->length(V_LBL);
@@ -244,35 +250,35 @@ void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index
const int T_VW = font_->lengthMonoDigits(T_VAL, MONO_DIGIT_W); 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 + LABEL_GAP + V_VW + FIELD_GAP + T_LW + LABEL_GAP + T_VW;
xx = x + ((w - LINE2_W) / 2); xx = x + ((w - LINE2_W) / 2);
font_->writeColored(xx, y + LINE2_Y, V_LBL, LABEL_COLOR); font_->writeColored(xx, y + LINE2_Y, V_LBL, UiConfig::get().hudLabel());
xx += V_LW + LABEL_GAP; xx += V_LW + LABEL_GAP;
font_->writeMonoDigits(xx, y + LINE2_Y, V_VAL, VALUE_COLOR, MONO_DIGIT_W); font_->writeMonoDigits(xx, y + LINE2_Y, V_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
xx += V_VW + FIELD_GAP; xx += V_VW + FIELD_GAP;
font_->writeColored(xx, y + LINE2_Y, T_LBL, LABEL_COLOR); font_->writeColored(xx, y + LINE2_Y, T_LBL, UiConfig::get().hudLabel());
xx += T_LW + LABEL_GAP; xx += T_LW + LABEL_GAP;
font_->writeMonoDigits(xx, y + LINE2_Y, T_VAL, VALUE_COLOR, MONO_DIGIT_W); font_->writeMonoDigits(xx, y + LINE2_Y, T_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
} }
void Hud::renderIdleCenter(int x, int y, int w) const { void Hud::renderIdleCenter(int x, int y, int w) const {
// --- Línia 1: "PIRAMIDE <n>" centrada --- // --- Línia 1: "<piramide> <n>" centrada ---
const std::string PIR_LBL{"PIRAMIDE"}; const std::string PIR_LBL = Locale::get().text("hud.pyramid");
const std::string PIR_VAL = std::to_string(pyramid_); const std::string PIR_VAL = std::to_string(pyramid_);
const int PL_W = font_->length(PIR_LBL); const int PL_W = font_->length(PIR_LBL);
const int PV_W = font_->lengthMonoDigits(PIR_VAL, MONO_DIGIT_W); 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 + LABEL_GAP + PV_W;
int xx = x + ((w - LINE1_W) / 2); int xx = x + ((w - LINE1_W) / 2);
font_->writeColored(xx, y + LINE1_Y, PIR_LBL, LABEL_COLOR); font_->writeColored(xx, y + LINE1_Y, PIR_LBL, UiConfig::get().hudLabel());
xx += PL_W + LABEL_GAP; xx += PL_W + LABEL_GAP;
font_->writeMonoDigits(xx, y + LINE1_Y, PIR_VAL, VALUE_COLOR, MONO_DIGIT_W); font_->writeMonoDigits(xx, y + LINE1_Y, PIR_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
// --- Línia 2: "SALA <r>" centrada --- // --- Línia 2: "<sala> <r>" centrada ---
const std::string SALA_LBL{"SALA"}; const std::string SALA_LBL = Locale::get().text("hud.room");
const std::string SALA_VAL = std::to_string(room_); const std::string SALA_VAL = std::to_string(room_);
const int SL_W = font_->length(SALA_LBL); const int SL_W = font_->length(SALA_LBL);
const int SV_W = font_->lengthMonoDigits(SALA_VAL, MONO_DIGIT_W); 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 + LABEL_GAP + SV_W;
xx = x + ((w - LINE2_W) / 2); xx = x + ((w - LINE2_W) / 2);
font_->writeColored(xx, y + LINE2_Y, SALA_LBL, LABEL_COLOR); font_->writeColored(xx, y + LINE2_Y, SALA_LBL, UiConfig::get().hudLabel());
xx += SL_W + LABEL_GAP; xx += SL_W + LABEL_GAP;
font_->writeMonoDigits(xx, y + LINE2_Y, SALA_VAL, VALUE_COLOR, MONO_DIGIT_W); font_->writeMonoDigits(xx, y + LINE2_Y, SALA_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
} }
+6 -15
View File
@@ -5,9 +5,9 @@
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string>
#include <vector> #include <vector>
#include "core/rendering/surface.hpp" // Palette
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
class Text; class Text;
@@ -41,7 +41,7 @@ class Hud {
WIN, WIN,
EMPTY }; EMPTY };
Hud(EventBus& bus, std::shared_ptr<Text> font, const Palette& ui_palette); Hud(EventBus& bus, std::shared_ptr<Text> font);
~Hud(); ~Hud();
Hud(const Hud&) = delete; Hud(const Hud&) = delete;
@@ -89,15 +89,6 @@ class Hud {
static constexpr int LINE1_Y = 4; static constexpr int LINE1_Y = 4;
static constexpr int LINE2_Y = 13; static constexpr int LINE2_Y = 13;
// Durada de l'animació d'entrada — els tres blocs alhora, amb easing
// outCubic. 400 ms és prou per a percebre el moviment sense fer
// esperar massa al jugador.
static constexpr int ENTRY_MS = 400;
// Durades per defecte d'efectes transitoris.
static constexpr int FLASH_MS = 250;
static constexpr int SHAKE_MS = 300;
// Vides inicials per jugador (placeholder fins que GameScene ho // Vides inicials per jugador (placeholder fins que GameScene ho
// configure des del Config del nivell). // configure des del Config del nivell).
static constexpr int DEFAULT_LIVES = 3; static constexpr int DEFAULT_LIVES = 3;
@@ -112,8 +103,9 @@ class Hud {
int h{0}; int h{0};
// Animació d'entrada: ms restants fins a quedar-se a la posició // Animació d'entrada: ms restants fins a quedar-se a la posició
// final. Quan val 0 el bloc està quiet. // final. Quan val 0 el bloc està quiet. El Hud ctor l'inicialitza
int entry_ms_left{ENTRY_MS}; // amb la durada viva a data/ui.yaml (hud.entry_ms).
int entry_ms_left{0};
// Vector inicial des d'on entra: {-LEFT_W,0} per al bloc esquerre, // Vector inicial des d'on entra: {-LEFT_W,0} per al bloc esquerre,
// {0,-HEIGHT_PX} per al central i {+RIGHT_W,0} per al dret. // {0,-HEIGHT_PX} per al central i {+RIGHT_W,0} per al dret.
int entry_offset_x{0}; int entry_offset_x{0};
@@ -143,7 +135,7 @@ class Hud {
// Subrutines del mode `Idle` per als tres blocs. Reben el rectangle // Subrutines del mode `Idle` per als tres blocs. Reben el rectangle
// ja desplaçat (x, y) on han d'escriure. // ja desplaçat (x, y) on han d'escriure.
void renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const char* label) const; void renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const std::string& label) const;
void renderIdleCenter(int x, int y, int w) const; void renderIdleCenter(int x, int y, int w) const;
// Calcula l'offset transitori (entrada + shake) en píxels per a un // Calcula l'offset transitori (entrada + shake) en píxels per a un
@@ -155,7 +147,6 @@ class Hud {
std::vector<EventBus::SubId> subs_; std::vector<EventBus::SubId> subs_;
std::shared_ptr<Text> font_; std::shared_ptr<Text> font_;
Palette ui_palette_{};
std::array<Block, 3> blocks_; std::array<Block, 3> blocks_;
std::array<PlayerStats, 2> players_; std::array<PlayerStats, 2> players_;
+11 -11
View File
@@ -14,9 +14,9 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/options_apply.hpp" #include "game/options_apply.hpp"
#include "game/ui/ui_palette.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "version.h" #include "version.h"
@@ -844,8 +844,8 @@ auto ServiceMenu::targetBoxHeight(const Page& page) const -> int {
} }
void ServiceMenu::renderBox(int box_x, int box_y, int box_w, int box_h, float eased) const { void ServiceMenu::renderBox(int box_x, int box_y, int box_w, int box_h, float eased) const {
const std::uint32_t BG_ARGB = ui_palette_[UiColor::MENU_BG]; const std::uint32_t BG_ARGB = ui_palette_[UiConfig::get().menuBg()];
const std::uint32_t BORDER_ARGB = ui_palette_[UiColor::MENU_BORDER]; const std::uint32_t BORDER_ARGB = ui_palette_[UiConfig::get().menuBorder()];
const auto ALPHA = static_cast<std::uint8_t>(std::lround(BG_ALPHA * eased)); const auto ALPHA = static_cast<std::uint8_t>(std::lround(BG_ALPHA * eased));
// Fons semi-transparent (el joc queda visible apagat per darrere). // Fons semi-transparent (el joc queda visible apagat per darrere).
@@ -877,7 +877,7 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
cursor_y, cursor_y,
Locale::get().text(page.title_key), Locale::get().text(page.title_key),
TITLE_KERNING, TITLE_KERNING,
UiColor::MENU_TITLE); UiConfig::get().menuTitle());
cursor_y += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM; cursor_y += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM;
if (!page.subtitle.empty()) { if (!page.subtitle.empty()) {
@@ -886,11 +886,11 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
cursor_y, cursor_y,
page.subtitle, page.subtitle,
TITLE_KERNING, TITLE_KERNING,
UiColor::MENU_TITLE); UiConfig::get().menuTitle());
cursor_y += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM; cursor_y += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM;
} }
Screen::get().fillRect(box_x + PAD_X, cursor_y, box_w - (2 * PAD_X), 1, ui_palette_[UiColor::MENU_BORDER]); Screen::get().fillRect(box_x + PAD_X, cursor_y, box_w - (2 * PAD_X), 1, ui_palette_[UiConfig::get().menuBorder()]);
cursor_y += 1 + GAP_Y; cursor_y += 1 + GAP_Y;
bool first_item = true; bool first_item = true;
@@ -907,14 +907,14 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
: Locale::get().text(item.label_key); : Locale::get().text(item.label_key);
const std::string VALUE = formatValue(item); const std::string VALUE = formatValue(item);
Uint8 label_color = UiColor::MENU_LABEL; Uint8 label_color = UiConfig::get().menuLabel();
if (item.kind == Item::Kind::LABEL) { if (item.kind == Item::Kind::LABEL) {
// Mateix to que el títol de la pàgina: les LABELs actuen com // Mateix to que el títol de la pàgina: les LABELs actuen com
// a subtítol-divisor (p.ex. "Jugador 1" a Teclat, "ver.X.Y" a // a subtítol-divisor (p.ex. "Jugador 1" a Teclat, "ver.X.Y" a
// Sistema) i han de tenir la mateixa presència visual. // Sistema) i han de tenir la mateixa presència visual.
label_color = UiColor::MENU_TITLE; label_color = UiConfig::get().menuTitle();
} else if (SELECTED) { } else if (SELECTED) {
label_color = UiColor::MENU_CURSOR; label_color = UiConfig::get().menuCursor();
} }
int label_x = LABEL_X; int label_x = LABEL_X;
@@ -931,7 +931,7 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
cursor_y, cursor_y,
"> ", "> ",
ITEM_KERNING, ITEM_KERNING,
UiColor::MENU_CURSOR); UiConfig::get().menuCursor());
} }
items_font_->writeDX(Text::COLOR_FLAG, items_font_->writeDX(Text::COLOR_FLAG,
@@ -951,7 +951,7 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
cursor_y, cursor_y,
VALUE, VALUE,
ITEM_KERNING, ITEM_KERNING,
SELECTED ? UiColor::MENU_CURSOR : UiColor::MENU_VALUE); SELECTED ? UiConfig::get().menuCursor() : UiConfig::get().menuValue());
} }
cursor_y += ITEM_H; cursor_y += ITEM_H;
-22
View File
@@ -1,22 +0,0 @@
#pragma once
#include "utils/defaults.hpp"
// Alias semàntics als índexs de paleta UI (viuen a `Defaults::UI`). Canviar
// l'aparença d'un component només requereix tocar-los allà; ací només
// reexportem amb els noms que consumeix el codi del joc.
namespace UiColor {
constexpr auto RENDER_INFO_TEXT = Defaults::UI::RENDER_INFO_TEXT;
constexpr auto RENDER_INFO_SHADOW = Defaults::UI::RENDER_INFO_SHADOW;
constexpr auto CANVAS_BG = Defaults::UI::CANVAS_BG;
constexpr auto MENU_BG = Defaults::UI::MENU_BG;
constexpr auto MENU_BORDER = Defaults::UI::MENU_BORDER;
constexpr auto MENU_TITLE = Defaults::UI::MENU_TITLE;
constexpr auto MENU_LABEL = Defaults::UI::MENU_LABEL;
constexpr auto MENU_VALUE = Defaults::UI::MENU_VALUE;
constexpr auto MENU_CURSOR = Defaults::UI::MENU_CURSOR;
constexpr auto MENU_DISABLED = Defaults::UI::MENU_DISABLED;
} // namespace UiColor
+34 -19
View File
@@ -84,11 +84,11 @@ namespace Defaults {
constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones). constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones).
} // namespace Player } // namespace Player
// Índexs de paleta (sega-cube.pal) i temps en ms per als components // Fallbacks del tema d'UI: índexs de paleta, timings (ms) i fonts.
// de UI. Centralitzats ací perquè un "tema" viu en un únic lloc: tocar // El tema en viu és a data/ui.yaml, carregat per UiConfig a
// els colors o durades de l'overlay/notificador no requereix buscar // l'arrencada; aquests valors només s'apliquen si el fitxer falta o
// pel codi. Si algun dia es volen temes múltiples, es pot moure a YAML // li manca alguna clau. Ací només cal tocar-los si es vol canviar el
// sense més impacte que dos accessors extra. // fallback "de fàbrica".
namespace UI { namespace UI {
// Fonts d'UI centralitzades: un únic lloc per conèixer la carpeta // Fonts d'UI centralitzades: un únic lloc per conèixer la carpeta
// (data/fonts/), el nom-clau al Resource::Cache i els fitxers .gif / // (data/fonts/), el nom-clau al Resource::Cache i els fitxers .gif /
@@ -128,32 +128,47 @@ namespace Defaults {
} // namespace Font } // namespace Font
// Overlay de RenderInfo (F12). // Overlay de RenderInfo (F12).
constexpr Uint8 RENDER_INFO_TEXT = 47; constexpr Uint8 RENDER_INFO_TEXT = 58;
constexpr Uint8 RENDER_INFO_SHADOW = 34; constexpr Uint8 RENDER_INFO_SHADOW = 16;
// Fons del canvas (zona fora del tilemap en letterbox/integer). // Fons del canvas (zona fora del tilemap en letterbox/integer).
constexpr Uint8 CANVAS_BG = 65; constexpr Uint8 CANVAS_BG = 72;
// Preset per defecte del Notifier. // Preset per defecte del Notifier.
constexpr Uint8 NOTIFICATION_TEXT = 197; constexpr Uint8 NOTIFICATION_TEXT = 232;
constexpr Uint8 NOTIFICATION_BG = 96; constexpr Uint8 NOTIFICATION_BG = 64;
constexpr int NOTIFICATION_TIME_MS = 4000; constexpr int NOTIFICATION_TIME_MS = 4000;
// Preset per a la confirmació d'eixida (ESC doble). La durada ha // Preset per a la confirmació d'eixida (ESC doble). La durada ha
// de coincidir amb la del Notifier perquè el missatge i la finestra // de coincidir amb la del Notifier perquè el missatge i la finestra
// de confirmació expiren alhora (veure GlobalInputs::onExit). // de confirmació expiren alhora (veure GlobalInputs::onExit).
constexpr Uint8 EXIT_TEXT = 88; constexpr Uint8 EXIT_TEXT = 71;
constexpr Uint8 EXIT_BG = 40; constexpr Uint8 EXIT_BG = 2;
constexpr int EXIT_TIME_MS = 4000; constexpr int EXIT_TIME_MS = 4000;
// Índexs de paleta per al menú de servei (F9). // Índexs de paleta per al menú de servei (F9).
constexpr Uint8 MENU_BG = 40; // Fons de la caixa. constexpr Uint8 MENU_BG = 2; // Fons de la caixa.
constexpr Uint8 MENU_BORDER = 197; // Vora. constexpr Uint8 MENU_BORDER = 232; // Vora.
constexpr Uint8 MENU_TITLE = 213; // Títol i subtítol. constexpr Uint8 MENU_TITLE = 237; // Títol i subtítol.
constexpr Uint8 MENU_LABEL = 47; // Etiquetes dels items. constexpr Uint8 MENU_LABEL = 58; // Etiquetes dels items.
constexpr Uint8 MENU_VALUE = 197; // Valor actual. constexpr Uint8 MENU_VALUE = 232; // Valor actual.
constexpr Uint8 MENU_CURSOR = 88; // Item seleccionat. constexpr Uint8 MENU_CURSOR = 71; // Item seleccionat.
constexpr Uint8 MENU_DISABLED = 34; // Label en gris quan no aplica. constexpr Uint8 MENU_DISABLED = 16; // Label en gris quan no aplica.
// Índexs de paleta per al marcador de joc (HUD de la GameScene).
// Arranca reciclant els colors del menú per coherència visual;
// separant els noms a constants pròpies, tocar-los a posteriori
// no afecta el menú.
constexpr Uint8 HUD_BG = 0; // Fons de cada bloc.
constexpr Uint8 HUD_LABEL = 58; // Etiquetes ("P1", "V", "T").
constexpr Uint8 HUD_VALUE = 187; // Valors (score, vides, tresors).
constexpr Uint8 HUD_FLASH = 237; // Color temporal del valor en flash.
// Timings d'animació del HUD. L'entrada són els tres blocs lliscant
// a la seua posició; flash i shake acompanyen un `player_hit`.
constexpr int HUD_ENTRY_MS = 400;
constexpr int HUD_FLASH_MS = 250;
constexpr int HUD_SHAKE_MS = 300;
} // namespace UI } // namespace UI
#ifdef _DEBUG #ifdef _DEBUG