#pragma once #include #include #include #include #include #include #include #include #include "core/input/define_buttons.hpp" #include "core/rendering/surface.hpp" // Palette #include "game/scenes/scene_flow.hpp" class Text; // Menú de servei (F9). Caixa centrada amb títol i items navegables per // fletxes + Retorn; Retrocés / Escape tanca la pàgina actual o el menú. // És accessible només dins de `GameScene` (els placeholders LOGO/TITLE/INTRO // duren 5 s i no tenen sentit com a entrada al menú; veure plan §11). // // La pila de pàgines (`stack_`) permet submenús; la pàgina de dalt és la // que rep input i es dibuixa. `pushPage` apila, `popPage` treu la de dalt; // en tornar a quedar amb una sola pàgina, backspace/escape tanca el menú. class ServiceMenu { public: static auto get() -> ServiceMenu&; ServiceMenu(const ServiceMenu&) = delete; ServiceMenu(ServiceMenu&&) = delete; auto operator=(const ServiceMenu&) -> ServiceMenu& = delete; auto operator=(ServiceMenu&&) -> ServiceMenu& = delete; // Injecta les dues fonts del menú i la paleta UI. Ha de cridar-se una // sola vegada (des de Director) abans de qualsevol `render()` o // `toggle()`. Cada font pot portar el seu kerning pre-configurat // (via `Text::setDefaultKerning`). void init(std::shared_ptr title_font, std::shared_ptr items_font, const Palette& ui_palette); // "Obert" = el menú accepta input. Torna `false` durant l'animació de // tancament (la caixa es veu però ja no respon). [[nodiscard]] auto isOpen() const -> bool { return open_; } void toggle(); void close(); // Avança animacions d'obertura, tancament i alçada. Ha de cridar-se // cada frame des del Director (no des de cada escena). void update(int delta_ms); void render(); // Retorna true si el menú ha consumit l'esdeveniment. Quan el menú està // obert, consumeix tots els KEY_DOWN perquè les hotkeys globals (F1–F8, // F11, F12) no es disparin per sota. Quan està tancat sempre retorna // false; és el caller qui decideix obrir-lo amb la hotkey. auto handleEvent(const SDL_Event& ev) -> bool; // True mentre estem esperant que l'usuari pulse una tecla per assignar // a l'item KEY_BIND seleccionat. El Director redirigeix els KEY_DOWN // a `captureKey` mentre dura — replica `Menu::isCapturing` d'AEE. [[nodiscard]] auto isCapturing() const -> bool { return capturing_; } // Aplica l'scancode capturat a l'item seleccionat (si encara és KEY_BIND // i té setter). ESCAPE cancel·la sense assignar. void captureKey(SDL_Scancode sc); // Arranca el sub-mòdul `DefineButtons` per al mando indicat. El menú // queda "obert" però els events del mando es redirigeixen al sub-mòdul // fins que acabi la seqüència o l'usuari pulse ESC. Replica el patró // `define_buttons_->enable(...)` de coffee_crisis_arcade_edition. void startDefineButtons(SDL_Gamepad* pad, const std::string& name, const std::string& path); // Bústia de transició d'escena des del menú: alguns items (p.ex. // "Reiniciar" a Sistema) no es poden expressar com a `ExitRequest` de // l'escena actual perquè el menú és global i no té handle a l'escena. // Director consulta `takePendingSceneRequest()` cada frame i, si hi ha // valor, força la transició — el mateix camí que per a `ExitRequest` // d'una escena, però el dispara el menú directament. void requestSceneTransition(ExitRequest req) { pending_request_ = req; } [[nodiscard]] auto takePendingSceneRequest() -> std::optional { auto out = pending_request_; pending_request_.reset(); return out; } private: // --- Struct d'un item d'una pàgina --- // // La unió de totes les "variants" (toggle / cycle / ...) en un sol // struct amb `std::function` nominalment opcionals simplifica la // construcció dels items (nomes s'omplen els slots del Kind rellevant) // a canvi d'una mica d'overhead. Amb una dotzena d'items no és un // problema de rendiment. struct Item { enum class Kind : std::uint8_t { LABEL, // Text pur, no interacciona; ideal per a versions/notes. TOGGLE, // ON / OFF amb bool getter/setter CYCLE, // Índex dins d'una llista; L=prev, R=next (wrap) INT_RANGE, // L/R descent/ascent per step, clamp a [min,max] SUBMENU, // RETURN/R: push la pàgina construïda per `pushSub` ACTION, // RETURN: dispara `action` KEY_BIND, // Mostra el nom d'una tecla; RETURN captura-ne de nova. }; Kind kind{Kind::TOGGLE}; // Per a tots els Kind menys LABEL, `label_key` és una clau de Locale. // Per a LABEL, és text literal (no passa per Locale) perquè típicament // porta valors dinàmics (versió, hash de commit, etc.). std::string label_key; std::function get_bool; std::function set_bool; std::function get_index; std::function get_count; std::function set_index; std::function value_label; std::function get_int; std::function set_int; int min_value{0}; int max_value{0}; int step{1}; std::function int_display; std::function push_sub; std::function action; // Per a KEY_BIND: getter que retorna l'scancode actual i setter // opcional que el muta. Si `set_scancode` és buit, l'item es veu // però la captura queda deshabilitada (placeholder). std::function get_scancode; std::function set_scancode; // Si està buit, l'item sempre és visible. Si retorna false, l'item // es salta tant al render com a la navegació (cursor salta). std::function visible; // Si `false`, el cursor no pot aterrar-hi: es salta a l'hora de // navegar amb ↑/↓. Default `true` per la gran majoria d'items; les // etiquetes LABEL el deixen `false` perquè no tenen interacció. bool selectable{true}; }; struct Page { std::string title_key; // "menu.titles.video" etc. std::string subtitle; // Text ja format (buit si no aplica). std::vector items; std::size_t cursor{0}; // Si `true`, els items es dibuixen centrats dins de la caixa i el // cursor es col·loca immediatament a l'esquerra de l'etiqueta. // Adequat per a pàgines que només tenen botons d'acció (p.ex. Sistema). bool center_items{false}; }; ServiceMenu() = default; ~ServiceMenu() = default; // --- Factories d'Items --- // // Cada factory omple els slots rellevants del Kind i deixa la resta // default-construïts. Redueix el soroll al lloc d'ús i, el més important, // elimina el warning `-Wmissing-designated-field-initializers` que // apareixeria amb designated-initializers parcials. [[nodiscard]] static auto makeToggle( std::string label_key, std::function getter, std::function setter, std::function visible = {}) -> Item; [[nodiscard]] static auto makeCycle( std::string label_key, std::function get_index, std::function get_count, std::function set_index, std::function value_label, std::function visible = {}) -> Item; [[nodiscard]] static auto makeIntRange( std::string label_key, std::function get_int, std::function set_int, int min_value, int max_value, int step, std::function int_display = {}, std::function visible = {}) -> Item; [[nodiscard]] static auto makeSubmenu( std::string label_key, std::function push_sub) -> Item; [[nodiscard]] static auto makeAction( std::string label_key, std::function action) -> Item; // Etiqueta no seleccionable. `text` és el literal a mostrar (no passa // per Locale), perquè l'ús principal és mostrar versions o hashos que // ja es calculen a codi. [[nodiscard]] static auto makeLabel( std::string text, std::function visible = {}) -> Item; // Item de vinculació de tecla. Mostra el nom de l'scancode al costat // dret. Si `setter` és buit, l'item surt com a informatiu (no captura). // Quan el setter sigui operatiu, RETURN entrarà en mode captura i la // pròxima tecla que es premi sobreescriurà la vinculació. [[nodiscard]] static auto makeKeyBind( std::string label_key, std::function getter, std::function setter = {}) -> Item; // --- Navegació i estat --- void pushPage(Page page); void popPage(); // Tractament del bloc d'events de gamepad (BUTTON_DOWN). Extret a // mètode propi per a mantenir la complexitat cognitiva d'`handleEvent` // sota el llindar de clang-tidy. void handleGamepadButtonDown(const SDL_GamepadButtonEvent& ev); // Navegació via sticks analògics. Treballa per edge-detection: cada // eix recorda en quina "zona" està (negativa / neutra / positiva) i // sols dispara una acció quan creua de neutra a un extrem, igual que // un botó del DPAD. Aplicada tant al stick esquerre com al dret. void handleGamepadAxisMotion(const SDL_GamepadAxisEvent& ev); // Sons d'UI: SELECT acompanya navegació (amunt/avall) i back; ACCEPT // acompanya modificació d'opció (esquerra/dreta/retorn). Els noms de // recurs venen d'`UiConfig` i el preload el fa el Director. static void playSelectSound(); static void playAcceptSound(); [[nodiscard]] auto top() -> Page&; [[nodiscard]] auto topConst() const -> const Page&; [[nodiscard]] static auto isItemVisible(const Item& item) -> bool; void moveCursor(int direction); // +1 / -1, salta items no visibles void activateCurrent(int direction); // Aplica l'acció segons el Kind static void activateCycle(const Item& item, int direction); static void activateIntRange(const Item& item, int direction); // --- Constructors de pàgines (viuen a .cpp) --- // Són static perquè no necessiten estat d'instància: tots els getters // i setters que capturen dins dels items accedeixen via `Options::` o // `ServiceMenu::get()` directament. [[nodiscard]] static auto buildRoot() -> Page; [[nodiscard]] static auto buildVideo() -> Page; [[nodiscard]] static auto buildAudio() -> Page; [[nodiscard]] static auto buildControls() -> Page; [[nodiscard]] static auto buildKeyboardControls() -> Page; [[nodiscard]] static auto buildGamepadControls() -> Page; [[nodiscard]] static auto buildSystem() -> Page; // Helpers privats d'items de vídeo: aïllen la lògica de // `shader_type` i `preset` fora de `buildVideo` perquè no exploti la // complexitat cognitiva del constructor de pàgines. [[nodiscard]] static auto makeShaderTypeItem() -> Item; [[nodiscard]] static auto makeShaderPresetItem() -> Item; // Fàbrica genèrica per a un KEY_BIND vinculat a una acció del jugador // `player_id`. Llegeix d'`Options::controls().keyboard_p*` i delega // la mutació a `Options::setKeyboardBinding(...)` (persisteix). [[nodiscard]] static auto makeKeyboardItem( const std::string& label_key, int player_id, std::uint8_t action) -> Item; // --- Render --- // "Visible" inclou l'animació de tancament: retorna true mentre la caixa // encara ha de dibuixar-se, encara que open_ siga false. [[nodiscard]] auto isVisible() const -> bool; [[nodiscard]] auto targetBoxHeight(const Page& page) const -> int; void renderBox(int box_x, int box_y, int box_w, int box_h, float eased) const; void renderPage(const Page& page, int box_x, int box_y, int box_w) const; [[nodiscard]] static auto formatValue(const Item& item) -> std::string; // --- Estat --- std::shared_ptr font_; // Títol del menú (`ari-w9500-display`). std::shared_ptr items_font_; // Items del menú (`ari-w9500-condensed-display`). Palette ui_palette_{}; std::vector stack_; bool open_{false}; bool closing_{false}; // `true` entre `close()` i fi de l'animació. float open_anim_{0.0F}; // 0 = invisible, 1 = totalment obert. float animated_h_{0.0F}; // Alçada suavitzada cap a `targetBoxHeight`. bool transition_active_{false}; // Push/pop de pàgina en curs. float transition_progress_{0.0F}; // 0 → 1 durant la transició de pàgina. bool capturing_{false}; // True mentre esperem una tecla per a un KEY_BIND. // Estat per eix del stick per a edge-detection: -1, 0, +1 segons la // zona on viu el valor actual. Un canvi de zona dispara una acció // (només quan entra a un extrem); reset a 0 quan torna al centre. std::int8_t lx_zone_{0}; std::int8_t ly_zone_{0}; std::int8_t rx_zone_{0}; std::int8_t ry_zone_{0}; // Sub-mòdul de captura de botons del mando. Quan està actiu absorbeix // tots els events de mando (BUTTON_DOWN/UP, AXIS_MOTION) i, en acabar, // persisteix la nova configuració via `GamepadBindings`. Replica el // patró `define_buttons_` de coffee_crisis_arcade_edition. DefineButtons define_buttons_; // Sol·licitud de canvi d'escena pendent (vore `requestSceneTransition`). // La consumeix Director al final del frame; si l'item no força la // transició, queda buida. std::optional pending_request_; };