# Pla d'implementació — Menú de servei Pla per a portar el menú de servei de `../aee` (`source/core/rendering/menu.cpp`) a `aee_arcade`, 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_arcade` | | ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- | | 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` + `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 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 ()` — 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_arcade`: - 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_arcade` é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(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.