315 lines
14 KiB
C++
315 lines
14 KiB
C++
#pragma once
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <cstddef>
|
||
#include <cstdint>
|
||
#include <functional>
|
||
#include <memory>
|
||
#include <optional>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#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<Text> title_font,
|
||
std::shared_ptr<Text> 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<ExitRequest> {
|
||
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<bool()> get_bool;
|
||
std::function<void(bool)> set_bool;
|
||
|
||
std::function<std::size_t()> get_index;
|
||
std::function<std::size_t()> get_count;
|
||
std::function<void(std::size_t)> set_index;
|
||
std::function<std::string(std::size_t)> value_label;
|
||
|
||
std::function<int()> get_int;
|
||
std::function<void(int)> set_int;
|
||
int min_value{0};
|
||
int max_value{0};
|
||
int step{1};
|
||
std::function<std::string(int)> int_display;
|
||
|
||
std::function<void()> push_sub;
|
||
std::function<void()> 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<SDL_Scancode()> get_scancode;
|
||
std::function<void(SDL_Scancode)> 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<bool()> 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<Item> 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<bool()> getter,
|
||
std::function<void(bool)> setter,
|
||
std::function<bool()> visible = {}) -> Item;
|
||
|
||
[[nodiscard]] static auto makeCycle(
|
||
std::string label_key,
|
||
std::function<std::size_t()> get_index,
|
||
std::function<std::size_t()> get_count,
|
||
std::function<void(std::size_t)> set_index,
|
||
std::function<std::string(std::size_t)> value_label,
|
||
std::function<bool()> visible = {}) -> Item;
|
||
|
||
[[nodiscard]] static auto makeIntRange(
|
||
std::string label_key,
|
||
std::function<int()> get_int,
|
||
std::function<void(int)> set_int,
|
||
int min_value,
|
||
int max_value,
|
||
int step,
|
||
std::function<std::string(int)> int_display = {},
|
||
std::function<bool()> visible = {}) -> Item;
|
||
|
||
[[nodiscard]] static auto makeSubmenu(
|
||
std::string label_key,
|
||
std::function<void()> push_sub) -> Item;
|
||
|
||
[[nodiscard]] static auto makeAction(
|
||
std::string label_key,
|
||
std::function<void()> 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<bool()> 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<SDL_Scancode()> getter,
|
||
std::function<void(SDL_Scancode)> 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 buildKeyboardControlsForPlayer(int player_id) -> Page;
|
||
[[nodiscard]] static auto buildGamepadControls() -> Page;
|
||
[[nodiscard]] static auto buildSystem() -> Page;
|
||
[[nodiscard]] static auto buildTweaks() -> 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<Text> font_; // Títol del menú (`ari-w9500-display`).
|
||
std::shared_ptr<Text> items_font_; // Items del menú (`ari-w9500-condensed-display`).
|
||
Palette ui_palette_{};
|
||
std::vector<Page> 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<ExitRequest> pending_request_;
|
||
};
|