docs: pla d'implementació del menú de servei
This commit is contained in:
1 parent
eb07e7e564
commit
e4a8b4cd3e
1 file changed
+472
@@ -0,0 +1,472 @@
|
|||||||
|
# Pla d'implementació — Menú de servei
|
||||||
|
|
||||||
|
Pla per a portar el menú de servei de `../aee` (`source/core/rendering/menu.cpp`)
|
||||||
|
a `aee_2026`, adaptat a l'arquitectura actual (Scenes + Director instanciat +
|
||||||
|
`Options` amb setters persistents + `Screen` ARGB + `Text` bitmap). Document de
|
||||||
|
referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi
|
||||||
|
encara.
|
||||||
|
|
||||||
|
Objectiu visual: caixa centrada amb vora i títol, navegable amb fletxes +
|
||||||
|
Retorn + Retrocés, oberta i tancada amb F12 (hotkey `menu_toggle`), retolada amb
|
||||||
|
la font `ari-w9500-display`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Referència a `../aee`
|
||||||
|
|
||||||
|
Tot el que segueix és la base; la còpia mental serveix per a no reinventar res:
|
||||||
|
|
||||||
|
- [source/core/rendering/menu.hpp](../../aee/source/core/rendering/menu.hpp) — API
|
||||||
|
pública (`init/destroy/toggle/close/isOpen/isVisible/render/handleKey`).
|
||||||
|
- [source/core/rendering/menu.cpp](../../aee/source/core/rendering/menu.cpp) —
|
||||||
|
implementació completa (676 línies). Conté:
|
||||||
|
- Struct `Item` i `Page` amb `ItemKind::{Toggle, Cycle, IntRange, Submenu,
|
||||||
|
KeyBind, Action}`.
|
||||||
|
- Animació d'obertura/tancament (`OPEN_SPEED`, `CLOSE_SPEED`,
|
||||||
|
`HEIGHT_RATE`) i transició entre pàgines (`pushPage/popPage`).
|
||||||
|
- Dibuix manual sobre `Uint32* pixel_data` amb `blendRect/fillRect/drawBorder`.
|
||||||
|
- `buildRoot/buildVideo/buildAudio/buildControls/buildGame/buildSystem`.
|
||||||
|
- Claus de `locale/ca.yaml` sota `menu.{titles, items, values}` — es poden
|
||||||
|
reutilitzar gairebé literalment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Diferències d'arquitectura rellevants
|
||||||
|
|
||||||
|
| Punt | `../aee` | `aee_2026` |
|
||||||
|
| ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||||
|
| Overlay | `Overlay::render()` dibuixa Menú + Notifier al buffer | No hi ha Overlay. Cada Scene crida `Notifier::render()` i `RenderInfo::render()` |
|
||||||
|
| Director | Singleton `Director::get()` amb `requestQuit/requestRestart` | Classe instanciada; sortida via `SDL_EVENT_QUIT` a `EventBuffer` |
|
||||||
|
| `Options` | Membres `inline` mutats directament | Getters const-ref + setters que persisteixen automàticament |
|
||||||
|
| Setter → subsistema | Dins del mètode (ex. `Screen::toggleVSync`) | El caller crida `Options::setX(v)` **i** `Screen::setX(v)` per separat |
|
||||||
|
| Remapping tecles | `KeyConfig::scancodePtr(id)` retorna mutable | `KeyConfig` actual no té `scancodePtr` — veure limitació a §5 |
|
||||||
|
| Render | Pixel-level (blend manual sobre `Uint32*`) | `Screen::fillRect(argb)` + `Text::writeDX`; ARGB cru via `getCanvasArgb()` |
|
||||||
|
| Font | `8bithud.fnt` via `Text` | `Text` via `Resource::Cache` (paletat sobre `ui`) |
|
||||||
|
|
||||||
|
Conseqüències per al disseny:
|
||||||
|
|
||||||
|
1. El menú ha d'integrar-se en el pipeline de render de cada Scene, no via
|
||||||
|
Overlay. Pot ser tan simple com una línia al final de cada `render()`
|
||||||
|
abans de `Screen::get().present()`, al costat de Notifier/RenderInfo.
|
||||||
|
2. El menú intercepta input **abans** que `GlobalInputs::update()` processi
|
||||||
|
les hotkeys (si és obert, només F12 i les fletxes arriben al menú; la
|
||||||
|
resta d'esdeveniments es consumeixen perquè no activin F1–F11).
|
||||||
|
3. Els setters de `Options::` ja persisteixen. El menú crida
|
||||||
|
`Options::setX(v); Screen::setX(v);` com fa `global_inputs.cpp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ubicació i mòduls nous
|
||||||
|
|
||||||
|
```
|
||||||
|
source/game/ui/service_menu.hpp (nou)
|
||||||
|
source/game/ui/service_menu.cpp (nou)
|
||||||
|
data/fonts/ari-w9500-display.{fnt,gif} (ja existeixen)
|
||||||
|
data/locale/ca.yaml (afegir clau arrel `menu:`)
|
||||||
|
data/locale/en.yaml (afegir clau arrel `menu:`)
|
||||||
|
data/input/keys.yaml (canviar `toggle_render_info` → `menu_toggle` en F12, o moure F12 a menu i reubicar render_info)
|
||||||
|
```
|
||||||
|
|
||||||
|
Raonament:
|
||||||
|
|
||||||
|
- Viu a `game/ui/` perquè el contingut de les pàgines toca `Options::` i
|
||||||
|
`Screen::` del joc; no és un primitiu de renderització com `Text` o
|
||||||
|
`Notifier`.
|
||||||
|
- Exposa l'API com a **singleton** (`ServiceMenu::get()`) per coherència amb
|
||||||
|
`Notifier` i `RenderInfo`.
|
||||||
|
- `init()` rep `std::shared_ptr<Text>` + `Palette ui_palette`, igual que
|
||||||
|
`Notifier::init()`. El Director construeix la font display i la injecta.
|
||||||
|
|
||||||
|
Esquelet de l'API:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// source/game/ui/service_menu.hpp
|
||||||
|
class ServiceMenu {
|
||||||
|
public:
|
||||||
|
static auto get() -> ServiceMenu&;
|
||||||
|
void init(std::shared_ptr<Text> font, const Palette& ui_palette);
|
||||||
|
|
||||||
|
[[nodiscard]] auto isOpen() const -> bool; // accepta input
|
||||||
|
[[nodiscard]] auto isVisible() const -> bool; // dibuixa caixa (també tancant-se)
|
||||||
|
|
||||||
|
void toggle();
|
||||||
|
void close();
|
||||||
|
|
||||||
|
void update(int delta_ms); // avança animacions
|
||||||
|
void render(); // dibuixa sobre el canvas de Screen
|
||||||
|
|
||||||
|
// Retorna true si el menú ha consumit l'esdeveniment (tecla empassada).
|
||||||
|
auto handleEvent(const SDL_Event& ev) -> bool;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Hotkey i integració amb `GlobalInputs`
|
||||||
|
|
||||||
|
Decisió: F12 passa a ser `menu_toggle` i el cicle de l'overlay d'info es mou
|
||||||
|
a una altra tecla (per ex. F10) o es manté a F12 i s'obre el menú amb una
|
||||||
|
altra (per ex. F9). Opció recomanada:
|
||||||
|
|
||||||
|
- `menu_toggle` → **F9** (tecla nova, no conflictiva).
|
||||||
|
- `toggle_render_info` es queda a F12 (hàbit actual).
|
||||||
|
|
||||||
|
Motius: evita trencar la memòria muscular existent i deixa F12 — una tecla
|
||||||
|
familiar per a debug — lliure per a la seua funció actual.
|
||||||
|
|
||||||
|
Afegir a `data/input/keys.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: menu_toggle
|
||||||
|
code: "F9"
|
||||||
|
desc: "Obri/tanca el menú de servei"
|
||||||
|
```
|
||||||
|
|
||||||
|
Flux d'input (escena GameScene::update):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Abans de GlobalInputs::update, donar prioritat al menú.
|
||||||
|
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
||||||
|
if (ServiceMenu::get().isOpen() && ServiceMenu::get().handleEvent(*it)) {
|
||||||
|
it = EventBuffer::events.erase(it);
|
||||||
|
} else if (it->type == SDL_EVENT_KEY_DOWN &&
|
||||||
|
it->key.scancode == KeyConfig::get().scancode("menu_toggle")) {
|
||||||
|
ServiceMenu::get().toggle();
|
||||||
|
it = EventBuffer::events.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GlobalInputs::update(delta_ms);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Mentre el menú està obert, les hotkeys (F1–F8, F11, F12) queden bloquejades
|
||||||
|
perquè els KEY_DOWN els consumeix el menú.
|
||||||
|
- Escape dins del menú ↔ close (o cancel·lació de captura de tecla, si es
|
||||||
|
porta el KeyBind — veure §5).
|
||||||
|
- F9 sempre passa el toggle (encara que el menú estiga obert).
|
||||||
|
|
||||||
|
Alternativa més neta a mig termini: un `InputRouter` centralitzat al Director
|
||||||
|
amb prioritats (Menu → GlobalInputs → Scene). De moment, cadascuna de les
|
||||||
|
Scenes (`LogoScene`, `TitleScene`, `IntroScene`, `GameScene`) crida el bloc
|
||||||
|
d'input-del-menú al principi de `update()`, igual que ja fan amb
|
||||||
|
`GlobalInputs::update`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Contingut de les pàgines (adaptat a `Options` actuals)
|
||||||
|
|
||||||
|
### Root
|
||||||
|
|
||||||
|
- Vídeo → submenú
|
||||||
|
- Àudio → submenú
|
||||||
|
- Sistema → submenú
|
||||||
|
|
||||||
|
_No hi ha Controls ni Game:_ `Options::` actual no exposa remapping de tecles
|
||||||
|
de joc (les `KeysGame` són constants de `Defaults::`), ni opcions "de joc"
|
||||||
|
equivalents a `use_new_logo` / `show_preload`. Si en el futur apareixen, es
|
||||||
|
pot afegir sense reescriure res.
|
||||||
|
|
||||||
|
### Vídeo
|
||||||
|
|
||||||
|
Items, cadascun mapa el setter+notificador com `global_inputs.cpp`:
|
||||||
|
|
||||||
|
| Label | Tipus | Origen | Handler |
|
||||||
|
| --------------------- | -------- | ------------------------------------ | ---------------------------------------------------------------------------------------- |
|
||||||
|
| `menu.items.zoom` | IntRange | `Options::window().zoom` | `Options::setWindowZoom(v); Screen::get().setWindowZoom(v)` (rang 1–6, clamp) |
|
||||||
|
| `menu.items.screen` | Toggle | `Options::window().fullscreen` | `Options::setWindowFullscreen(v); Screen::get().setFullscreen(v)` |
|
||||||
|
| `menu.items.vsync` | Toggle | `Options::video().vsync` | `Options::setVideoVSync(v); Screen::get().setVSync(v)` |
|
||||||
|
| `menu.items.scaling_mode` | Cycle | `Options::video().scaling_mode` | `Options::setVideoScalingMode(m); Screen::get().setScalingMode(m)` |
|
||||||
|
| `menu.items.shader` | Toggle | `Options::video().shader.enabled` | `Options::setShaderEnabled(v)` + `reapplyShader()` (copiar helper de `global_inputs.cpp`)|
|
||||||
|
| `menu.items.shader_type` | Cycle | `Options::video().shader.current_shader` | `Options::setActiveShaderName("postfx"/"crtpi")` + `reapplyShader()` |
|
||||||
|
| `menu.items.preset` | Cycle | preset actiu segons shader | `Options::setPostFXPreset(s)` o `setCrtPiPreset(s)` + `reapplyShader()` |
|
||||||
|
| `menu.items.render_info` | Cycle | `Options::video().render_info_position` | `Options::setVideoRenderInfoPosition(p)` |
|
||||||
|
| `menu.items.uptime` | Toggle | `Options::video().render_info_show_time` | `Options::setVideoRenderInfoShowTime(v)` (sols visible si render_info ≠ OFF) |
|
||||||
|
|
||||||
|
Claus `visible` (oculta quan no apliquen):
|
||||||
|
|
||||||
|
- `shader_type`, `preset` → només si `shader.enabled == true`.
|
||||||
|
- `uptime` → només si `render_info_position != OFF`.
|
||||||
|
|
||||||
|
### Àudio
|
||||||
|
|
||||||
|
| Label | Tipus | Origen | Handler |
|
||||||
|
| ---------------------------- | -------- | ---------------------------------- | ------------------------------------------------------------------------------------ |
|
||||||
|
| `menu.items.master_enable` | Toggle | `Options::audio().enabled` | Caldrà afegir `Options::setAudioEnabled(v)` (no hi és avui) + `Audio::applyConfig()` |
|
||||||
|
| `menu.items.master_volume` | IntRange | `Options::audio().volume` | `Options::setAudioVolume(v)` (afegir) + `Audio::applyConfig()` |
|
||||||
|
| `menu.items.music` | Toggle | `Options::audio().music.enabled` | `Options::setAudioMusicEnabled(v)` (afegir) + `Audio::applyConfig()` |
|
||||||
|
| `menu.items.music_volume` | IntRange | `Options::audio().music.volume` | `Options::setAudioMusicVolume(v)` (afegir) + `Audio::applyConfig()` |
|
||||||
|
| `menu.items.sounds` | Toggle | `Options::audio().sound.enabled` | `Options::setAudioSoundEnabled(v)` (afegir) + `Audio::applyConfig()` |
|
||||||
|
| `menu.items.sounds_volume` | IntRange | `Options::audio().sound.volume` | `Options::setAudioSoundVolume(v)` (afegir) + `Audio::applyConfig()` |
|
||||||
|
|
||||||
|
**Dependència prèvia:** `Options::` no té setters d'àudio. Cal afegir-los
|
||||||
|
seguint el mateix patró que `setVideoVSync` (muten namespace intern +
|
||||||
|
`saveToFile()`) i exposar `Audio::applyConfig(Audio::Config)` o
|
||||||
|
`Audio::setVolumes(...)` — depenent del que ja existeixi a `core/audio/`.
|
||||||
|
|
||||||
|
Les escales de volum són 0.0–1.0 amb pas 0.05 (5 %), com a `../aee`.
|
||||||
|
|
||||||
|
### Sistema
|
||||||
|
|
||||||
|
Subtítol: `v<VERSION> (<GIT_HASH>)` — usar el `version.h` generat per CMake
|
||||||
|
(ja existeix a `source/version.h.in`).
|
||||||
|
|
||||||
|
| Label | Tipus | Handler |
|
||||||
|
| --------------------- | ------ | -------------------------------------------------------------------------------- |
|
||||||
|
| `menu.items.exit_game`| Action | Injectar un `SDL_EVENT_QUIT` a `EventBuffer::events` perquè `Director::processEvents` el vegi |
|
||||||
|
|
||||||
|
_Sense `Restart`_ de moment: Director no té un pas de reinici en calent, i
|
||||||
|
simular-lo a partir del director actual implica destruir l'escena i
|
||||||
|
reconstruir el flux — fora d'abast d'aquest menú. Deixar-ho com a TODO si
|
||||||
|
es necessita.
|
||||||
|
|
||||||
|
### Limitació: pàgina Controls diferida
|
||||||
|
|
||||||
|
El menú de `../aee` té una pàgina Controls (`buildControls`) que permet
|
||||||
|
remappar `up/down/left/right` i la tecla de menú. A `aee_2026`:
|
||||||
|
|
||||||
|
- Les tecles de joc són `Defaults::KeysGame::{UP,DOWN,LEFT,RIGHT}` — constants
|
||||||
|
`constexpr`, no mutables.
|
||||||
|
- `KeyConfig` actual no exposa `scancodePtr(id)`.
|
||||||
|
|
||||||
|
Opcions, de menys a més invasiu:
|
||||||
|
|
||||||
|
1. **No portar Controls** en aquesta iteració (recomanat per a la primera
|
||||||
|
versió).
|
||||||
|
2. Migrar `KeysGame` a `Options::` + afegir `KeyConfig::scancodePtr(id)` per
|
||||||
|
a hotkeys. Requereix tocar `Player::Controls`, el config.yaml i keys.yaml.
|
||||||
|
|
||||||
|
Si s'opta per (2), la pàgina Controls s'afegeix exactament igual que a
|
||||||
|
`../aee`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Renderitzat
|
||||||
|
|
||||||
|
Dos camins segons nivell d'ambició:
|
||||||
|
|
||||||
|
### A. Simple — sòlid opac (MVP)
|
||||||
|
|
||||||
|
- Caixa centrada: `Screen::get().fillRect(x, y, w, h, ui_palette_[idx_fons])`.
|
||||||
|
- Vora: 4 `fillRect` d'1 px amb color `ui_palette_[idx_vora]`.
|
||||||
|
- Text: `Text::writeDX(CENTER_FLAG | COLOR_FLAG | SHADOW_FLAG, ...)`.
|
||||||
|
- Cap blend; el joc queda totalment ocult darrere de la caixa (equivalent a
|
||||||
|
`../aee` però amb `BG_ALPHA = 255`).
|
||||||
|
|
||||||
|
Afegir dos índexs nous a `utils/defaults.hpp → UI::`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
constexpr Uint8 MENU_BG = /* triar paleta sega-cube */;
|
||||||
|
constexpr Uint8 MENU_BORDER = /* triar */;
|
||||||
|
constexpr Uint8 MENU_TITLE = /* blanc */;
|
||||||
|
constexpr Uint8 MENU_LABEL = /* gris clar */;
|
||||||
|
constexpr Uint8 MENU_VALUE = /* cian/groc */;
|
||||||
|
constexpr Uint8 MENU_CURSOR = /* destacat */;
|
||||||
|
constexpr Uint8 MENU_EMPTY = /* gris */;
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Semi-transparent (opcional, iteració 2)
|
||||||
|
|
||||||
|
Calca el `blendRect` de `../aee`: expandir `Screen::` amb un `blendRect(x,
|
||||||
|
y, w, h, argb, alpha)` que opere sobre `canvas_.data()`. El canvas és
|
||||||
|
`std::uint32_t*` format ABGR (equivalent a ARGB little-endian). Amb això, el
|
||||||
|
fons del menú pot ser `alpha=220` i es veu el joc apagat per darrere.
|
||||||
|
|
||||||
|
Mida fixa recomanada per al MVP: `BOX_W = 220`, altura dinàmica (igual que
|
||||||
|
`../aee`). Canvas lògic d'`aee_2026` és `GameCanvas::WIDTH`/`HEIGHT` —
|
||||||
|
confirmar mida real (mateixos 320×200?) a `utils/defines.hpp` abans de
|
||||||
|
hardcoded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Font `ari-w9500-display`
|
||||||
|
|
||||||
|
Ja és al disc i ja té el metadatfnt corresponent
|
||||||
|
([data/fonts/ari-w9500-display.fnt](../data/fonts/ari-w9500-display.fnt),
|
||||||
|
11 px d'altura, 12 px de box). És paletable sobre `ui` com les altres
|
||||||
|
fonts bitmap.
|
||||||
|
|
||||||
|
Al `Director::Director()`, afegir després del bloc de fonts HUD/Placeholder:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
constexpr const char* MENU_FONT_NAME = "ari-w9500-display";
|
||||||
|
constexpr const char* MENU_FONT_GIF = "data/fonts/ari-w9500-display.gif";
|
||||||
|
constexpr const char* MENU_FONT_FNT = "data/fonts/ari-w9500-display.fnt";
|
||||||
|
|
||||||
|
const std::string MENU_FONT_GIF_KEY = std::string(MENU_FONT_NAME) + ".gif";
|
||||||
|
Resource::Cache::get().loadSurface(MENU_FONT_GIF_KEY, MENU_FONT_GIF);
|
||||||
|
Resource::Cache::get().getSurface(MENU_FONT_GIF_KEY)->setPalette(
|
||||||
|
Resource::Cache::get().getPalette("ui"));
|
||||||
|
auto menu_font_surface = Resource::Cache::get().getSurface(MENU_FONT_GIF_KEY);
|
||||||
|
auto menu_text = std::make_shared<Text>(menu_font_surface, MENU_FONT_FNT);
|
||||||
|
|
||||||
|
ServiceMenu::get().init(menu_text, ui_palette);
|
||||||
|
```
|
||||||
|
|
||||||
|
Centralitzar les constants a `Defaults::UI::Font::MENU_{NAME,GIF,FNT}` perquè
|
||||||
|
encaixi amb el memo `project_ui_fonts.md` ("tot a `Defaults::UI::Font`").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Locale
|
||||||
|
|
||||||
|
Copiar les claus de `../aee/data/locale/ca.yaml` sota `menu:` (titles, items,
|
||||||
|
values) a **ca.yaml** i **en.yaml**. El primer commit pot incloure només `ca`;
|
||||||
|
`en` val la pena fer-lo a la vegada perquè és traducció directa.
|
||||||
|
|
||||||
|
Valors a eliminar de la còpia (no apliquen ací):
|
||||||
|
|
||||||
|
- `menu.items.{use_new_logo, show_title_credits, show_preload}` (no hi ha
|
||||||
|
pàgina Game).
|
||||||
|
- `menu.items.{move_up, move_down, move_left, move_right, menu_key,
|
||||||
|
restart, aspect_4_3, texture_filter, internal_resolution, supersampling}`
|
||||||
|
(no portats a l'MVP — veure §5 i les decisions de pàgina Vídeo).
|
||||||
|
|
||||||
|
Claus noves que no tenia `../aee` — cap per a l'MVP; si s'afegeixen pàgines
|
||||||
|
en futures iteracions, traduir a mesura.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Fases d'implementació
|
||||||
|
|
||||||
|
Cada fase és un commit. Ordre perquè cada pas compila i s'executa.
|
||||||
|
|
||||||
|
### Fase 1 — setters d'àudio a `Options::`
|
||||||
|
|
||||||
|
_Preparatori; sense això la pàgina Àudio no pot mutar res._
|
||||||
|
|
||||||
|
- Afegir `setAudioEnabled`, `setAudioVolume`, `setAudioMusicEnabled`,
|
||||||
|
`setAudioMusicVolume`, `setAudioSoundEnabled`, `setAudioSoundVolume` a
|
||||||
|
`options.hpp/.cpp`. Patró exacte de `setVideoVSync`: muten estat intern +
|
||||||
|
`saveToFile()`.
|
||||||
|
- Exposar un mètode de reconfiguració en calent a `core/audio/`
|
||||||
|
(`Audio::applyConfig` o similar) si no hi és ja.
|
||||||
|
- Smoke test: `xvfb-run` 5 s, joc arrenca i música sona.
|
||||||
|
|
||||||
|
### Fase 2 — hotkey i esquelet `ServiceMenu`
|
||||||
|
|
||||||
|
- Afegir `menu_toggle` a `data/input/keys.yaml` amb F9.
|
||||||
|
- Crear `service_menu.{hpp,cpp}` amb API mínima: `init`, `toggle`, `close`,
|
||||||
|
`isOpen`, `isVisible`, `update`, `render` (render de caixa buida amb
|
||||||
|
títol fix "Opcions"), `handleEvent` (només F9, Escape, Backspace).
|
||||||
|
- Al Director: construir font `ari-w9500-display`, inicialitzar
|
||||||
|
`ServiceMenu::get()`.
|
||||||
|
- A `GameScene::update()`: bloc d'input abans de `GlobalInputs::update`
|
||||||
|
(veure §4). A `GameScene::render()`: `ServiceMenu::get().render()` abans
|
||||||
|
de `Screen::present()`. **No** replicar encara a Logo/Title/Intro — el
|
||||||
|
menú només té sentit quan l'usuari juga.
|
||||||
|
- Smoke test: F9 obri caixa centrada, Escape/F9 la tanca.
|
||||||
|
|
||||||
|
### Fase 3 — framework de pàgines
|
||||||
|
|
||||||
|
- Portar `Item`, `Page`, `ItemKind`, `stack_`, `pushPage`/`popPage`,
|
||||||
|
`nextVisibleCursor`, `isVisible(Item)`, `handleKey` de `../aee` (quasi
|
||||||
|
literalment).
|
||||||
|
- Portar `renderPageContent` simplificat (sense transicions ni clipping
|
||||||
|
avançat al principi): render de títol, línia, items amb cursor i valor.
|
||||||
|
- Afegir les claus `menu.titles.root` i `menu.values.*` a `ca.yaml`/`en.yaml`.
|
||||||
|
- Implementar `buildRoot()` amb tres entrades Submenu buides (es naveguen
|
||||||
|
però totes porten a pàgines sense items visibles, text "(Buit)").
|
||||||
|
- Smoke test: navegació amunt/avall, enter a submenú, backspace torna.
|
||||||
|
|
||||||
|
### Fase 4 — Pàgina Vídeo
|
||||||
|
|
||||||
|
- Implementar `buildVideo()` amb els items de §5.
|
||||||
|
- Copiar el helper `reapplyShader()` de `global_inputs.cpp` a un lloc
|
||||||
|
compartit (p. ex. `game/options_apply.{hpp,cpp}`) perquè el cridin
|
||||||
|
tant `global_inputs` com el menú, sense duplicar la lògica.
|
||||||
|
- Afegir claus `menu.items.{zoom,screen,vsync,scaling_mode,shader,shader_type,preset,render_info,uptime}`.
|
||||||
|
- Smoke test: obrir menú, toggle vsync, canvi d'escala, comprovar que la
|
||||||
|
notificació NO apareix (és el menú qui fa el canvi, no les hotkeys).
|
||||||
|
|
||||||
|
### Fase 5 — Pàgina Àudio
|
||||||
|
|
||||||
|
- `buildAudio()` amb els 6 items. Helpers `volPct`/`stepVolume` copiats de
|
||||||
|
`../aee`.
|
||||||
|
- Claus `menu.items.{master_enable,master_volume,music,music_volume,sounds,sounds_volume}`.
|
||||||
|
- Smoke test: baixar volum de música, pujar volum de sons.
|
||||||
|
|
||||||
|
### Fase 6 — Pàgina Sistema
|
||||||
|
|
||||||
|
- `buildSystem()` amb subtítol de versió i `exit_game` que injecta
|
||||||
|
`SDL_EVENT_QUIT`.
|
||||||
|
- Smoke test: obrir sistema, exit_game tanca el joc net.
|
||||||
|
|
||||||
|
### Fase 7 (opcional) — polit
|
||||||
|
|
||||||
|
- Animació d'obertura/tancament (porta les constants `OPEN_SPEED`,
|
||||||
|
`CLOSE_SPEED`, `HEIGHT_RATE`, `TRANSITION_SPEED` de `../aee`).
|
||||||
|
- `blendRect` a `Screen::` per a fons semi-transparent.
|
||||||
|
- Replicar el bloc d'input del menú a `Title/Intro/Logo` si s'acorda que el
|
||||||
|
menú hi és accessible també.
|
||||||
|
- Traduccions EN si no es van fer abans.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Tests
|
||||||
|
|
||||||
|
- **Smoke (xvfb-run 5 s)** després de cada fase — memòria `feedback_smoke_test_mandatory`.
|
||||||
|
- Unitaris amb Catch2 si el framework està muntat (veure `tests/` actual):
|
||||||
|
- `ServiceMenu::toggle()` idempotent després de `close()`.
|
||||||
|
- `nextVisibleCursor` salta items ocults.
|
||||||
|
- `handleEvent` retorna `true` per a KEY_DOWN quan està obert, `false`
|
||||||
|
quan està tancat.
|
||||||
|
- Manual:
|
||||||
|
- Obrir F9 → tancar F9 → obrir de nou (no leak d'estat).
|
||||||
|
- Entrar a Vídeo → Shader off → verificar que `shader_type` i `preset`
|
||||||
|
desapareixen (anim o salt).
|
||||||
|
- Tancar el joc, re-obrir, verificar que `config.yaml` conserva els
|
||||||
|
canvis (els fa `Options::setX`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Riscos i punts oberts
|
||||||
|
|
||||||
|
- **Input routing als altres Scenes.** Si l'usuari vol obrir el menú
|
||||||
|
durant el LOGO/TITLE/INTRO, cal replicar el bloc d'input. Decisió: no.
|
||||||
|
Motiu: són placeholders amb 5 s de durada; obrir un menú allà és
|
||||||
|
overkill. Anotar-ho a la capçalera de `service_menu.hpp`.
|
||||||
|
- **`EventBuffer::events.erase(it)`** invalida els iteradors — mà amb el
|
||||||
|
loop. Patró amb `it = events.erase(it)` ↔ `++it` és segur.
|
||||||
|
- **Blend rect vs. GPU pipeline.** Si el shader PostFX reprocessa el
|
||||||
|
canvas, el menú ha d'estar pintat *abans* del shader (i ja ho està,
|
||||||
|
perquè el canvas ARGB va al shader). Res a fer; tot passa per
|
||||||
|
`Screen::present()` al final.
|
||||||
|
- **Captura de tecla (`KeyBind`).** No s'implementa aquesta iteració
|
||||||
|
(§5). Si es porta, l'event router ha de distingir "menú obert i
|
||||||
|
capturant" — el menú consumeix **tota** tecla, no només Enter/Backspace/etc.
|
||||||
|
- **`ari-w9500-display` és paletada.** Ha de compartir la paleta `ui` amb
|
||||||
|
la resta d'overlays. Verificar que el GIF només té la tinta a l'índex
|
||||||
|
`Text::INK_COLOR_INDEX = 1`; si no, el font_gen l'ha de regenerar (veure
|
||||||
|
memòria `reference_font_generator`).
|
||||||
|
- **F9 lliure?** Abans d'agafar F9 per al `menu_toggle`, confirmar que
|
||||||
|
cap altra hotkey hi és mapejada en builds futures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Resum d'arxius a tocar
|
||||||
|
|
||||||
|
**Nous:**
|
||||||
|
|
||||||
|
- `source/game/ui/service_menu.hpp`
|
||||||
|
- `source/game/ui/service_menu.cpp`
|
||||||
|
- (opcional) `source/game/options_apply.{hpp,cpp}` per compartir
|
||||||
|
`reapplyShader` amb `global_inputs.cpp`.
|
||||||
|
|
||||||
|
**Modificats:**
|
||||||
|
|
||||||
|
- `source/utils/defaults.hpp` — afegir índexs `UI::MENU_*` + `UI::Font::MENU_*`.
|
||||||
|
- `source/game/options.hpp/.cpp` — setters d'àudio (fase 1).
|
||||||
|
- `source/core/system/director.cpp` — carregar font menú + `ServiceMenu::get().init(...)`.
|
||||||
|
- `source/game/scenes/game_scene.cpp` — bloc d'input del menú a `update`,
|
||||||
|
render del menú a `render` abans de `present`.
|
||||||
|
- `data/input/keys.yaml` — afegir `menu_toggle: F9`.
|
||||||
|
- `data/locale/ca.yaml`, `data/locale/en.yaml` — secció `menu:`.
|
||||||
|
- `CMakeLists.txt` o `source/CMakeLists.txt` — afegir el nou `service_menu.cpp`.
|
||||||
|
|
||||||
|
**Sense tocar per a l'MVP:**
|
||||||
|
|
||||||
|
- `source/core/input/key_config.*` (no cal `scancodePtr`).
|
||||||
|
- `source/core/audio/*` si ja exposa configuració en calent; si no, mínima
|
||||||
|
adició.
|
||||||
|
- LogoScene/TitleScene/IntroScene.
|
||||||
Reference in new issue
Block a user