3dcf5c3a99
El cycle anterior fallava al desasignar perque Input::resolvePlayerGamepad tenia un fallback per slot que reasignava gamepads_[player_index] quan name+path eren buits. Això el contradeia el slot "SENSE MANDO" del cycle: el YAML quedava buit pero el runtime seguia lligant el mando. Treure el fallback i moure l'autoassignacio inicial al boot (nomes si tots dos jugadors venen buits) restaura la semàntica: buit vol dir buit. Sobre el fix, redissenyem la UX dels items MANDO P1/P2: ja no son CYCLE sino SUBMENU que obrin una pàgina-llista (estil RESOLUCIÓ) amb tots els mandos detectats. Cada item porta sufix (P1)/(P2) nomes si el mando el te l'altre jugador, perque sapigues que assignar-lo li'l "robarà". L'ultim item es "SENSE MANDO" per a desassignar explícitament. La lògica de swap automatic en conflicte queda extreta a assignPadToPlayer i es reutilitza des de la picker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
7.2 KiB
C++
169 lines
7.2 KiB
C++
// service_menu.hpp - Menu de servei (singleton)
|
|
// © 2026 JailDesigner
|
|
//
|
|
// Overlay de configuracio global accessible amb F12 des de qualsevol escena
|
|
// (LOGO, TITLE, GAME). Captura tots els KEY_DOWN excepte F1-F12 i ESC, que
|
|
// continuen arribant a GlobalEvents. Mentre esta obert, GameScene::update()
|
|
// fa early return per pausar el joc; LOGO i TITLE continuen renderitzant-se
|
|
// sota el menu.
|
|
//
|
|
// Arquitectura inspirada en aee_arcade service_menu.{hpp,cpp}: pila de
|
|
// pagines amb cursor, animacio open/close amb easing easeOutQuad i clipping
|
|
// del contingut mentre la caixa creix/decreix.
|
|
//
|
|
// API singleton equivalent a Notifier: init() al startup amb un renderer,
|
|
// get() retorna el punter, destroy() al teardown.
|
|
|
|
#pragma once
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/graphics/vector_text.hpp"
|
|
#include "core/rendering/render_context.hpp"
|
|
|
|
class SDLManager;
|
|
|
|
namespace System {
|
|
|
|
class DebugOverlay;
|
|
|
|
class ServiceMenu {
|
|
public:
|
|
// Tipus d'item de menu. En aquesta iteracio nomes s'usen SUBMENU i
|
|
// LABEL; la resta queden reservats per a iteracions futures (toggles
|
|
// de vsync/zoom, picker d'idioma, restart, exit...).
|
|
enum class Kind : std::uint8_t {
|
|
LABEL, // No interactiu, nomes es dibuixa
|
|
TOGGLE, // bool flip — reservat
|
|
CYCLE, // index amb modul — reservat
|
|
INT_RANGE, // step ± — reservat
|
|
SUBMENU, // pushPage en activar — usat
|
|
ACTION // call al lambda en activar — reservat
|
|
};
|
|
|
|
struct Item {
|
|
Kind kind = Kind::LABEL;
|
|
std::string label_key; // Clau de locale (s'ignora si label_text no esta buit)
|
|
std::string label_text; // Text literal (no locale). Util per a labels que no necessiten traduccio (resolucions, etc.)
|
|
bool selectable = true;
|
|
// SUBMENU / ACTION: callback en ENTER / RIGHT.
|
|
std::function<void()> on_activate;
|
|
// TOGGLE / CYCLE / INT_RANGE: text del valor actual (renderitzat a la dreta).
|
|
std::function<std::string()> get_value_text;
|
|
// TOGGLE / CYCLE / INT_RANGE: callback amb +1 (RIGHT/ENTER) o -1 (LEFT).
|
|
std::function<void(int)> on_change;
|
|
};
|
|
|
|
struct Page {
|
|
std::string title_key;
|
|
// Subtitol opcional, renderitzat sota el titol amb tipografia mes
|
|
// petita i color apagat. Es una funcio perque pot ser dinamic
|
|
// (versio+hash, etc.). Si esta buit, no es renderitza.
|
|
std::function<std::string()> subtitle_provider;
|
|
std::vector<Item> items;
|
|
std::size_t cursor = 0;
|
|
};
|
|
|
|
// Inicialitza el singleton amb el renderer global, l'SDLManager (video
|
|
// toggles: fullscreen, vsync, AA, postfx, zoom) i el DebugOverlay
|
|
// (toggle del HUD de debug a OPCIONS). Tots propietat del Director.
|
|
static void init(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
|
static void destroy();
|
|
[[nodiscard]] static auto get() -> ServiceMenu*;
|
|
|
|
// F12: alterna obrir/tancar amb animacio.
|
|
void toggle();
|
|
[[nodiscard]] auto isOpen() const -> bool;
|
|
|
|
void update(float delta_time);
|
|
void draw() const;
|
|
|
|
// Processa events de navegacio. Retorna true si l'event s'ha consumit.
|
|
// Accepta:
|
|
// - SDL_EVENT_KEY_DOWN: UP/DOWN/ENTER/RIGHT/LEFT/BACKSPACE.
|
|
// - SDL_EVENT_GAMEPAD_BUTTON_DOWN: DPAD per nav, FIRE = ENTER,
|
|
// ACCELERATE = BACK. La resta de botons s'ignoren.
|
|
// - SDL_EVENT_GAMEPAD_AXIS_MOTION: stick X/Y amb edge-detect.
|
|
auto handleEvent(const SDL_Event& event) -> bool;
|
|
|
|
private:
|
|
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
|
|
|
// Sub-handlers de handleEvent. Privats, no son part de l'API publica.
|
|
auto handleKeyDown(const SDL_Event& event) -> bool;
|
|
auto handleGamepadButton(const SDL_Event& event) -> bool;
|
|
auto handleGamepadAxis(const SDL_Event& event) -> bool;
|
|
|
|
void buildRootPage();
|
|
[[nodiscard]] auto buildVideoPage() -> Page;
|
|
[[nodiscard]] auto buildResolutionPage() const -> Page;
|
|
[[nodiscard]] static auto buildAudioPage() -> Page;
|
|
[[nodiscard]] auto buildOptionsPage() const -> Page;
|
|
[[nodiscard]] auto buildSystemPage() -> Page;
|
|
[[nodiscard]] auto buildControlsPage() -> Page;
|
|
// Llista de mandos detectats per a un jugador. Cada item assigna el
|
|
// pad triat (amb swap automatic si l'altre jugador ja el tenia).
|
|
// L'ultim item es "SENSE MANDO" per a desasignar.
|
|
[[nodiscard]] static auto buildPadPickerPage(int player_index) -> Page;
|
|
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
|
|
// l'usuari selecciona SI; el cursor per defecte apunta a NO.
|
|
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
|
|
void pushPage(Page page);
|
|
void popPage();
|
|
void moveCursor(int direction);
|
|
void activateCurrent();
|
|
// RIGHT (direction=+1) / LEFT (direction=-1). Per a TOGGLE/CYCLE/INT_RANGE
|
|
// crida on_change. Per a SUBMENU/ACTION nomes +1 (entra/activa).
|
|
void changeValue(int direction);
|
|
|
|
// Alçada objectiu de la caixa per a la pagina superior (sense animacio).
|
|
[[nodiscard]] auto computeTargetHeight() const -> float;
|
|
|
|
// Ample objectiu de la caixa per a la pagina superior (sense animacio).
|
|
// Pren com a base BOX_WIDTH_MIN i s'eixampla si algun text no hi cap.
|
|
[[nodiscard]] auto computeTargetWidth() const -> float;
|
|
|
|
// Y (top) de l'item index dins una caixa col·locada a box_y. Si la
|
|
// pagina te subtitol, els items es desplacen cap avall.
|
|
[[nodiscard]] static auto computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float;
|
|
|
|
Rendering::Renderer* renderer_;
|
|
SDLManager* sdl_;
|
|
DebugOverlay* debug_overlay_;
|
|
Graphics::VectorText text_;
|
|
|
|
std::vector<Page> stack_;
|
|
bool open_ = false;
|
|
bool closing_ = false;
|
|
float open_anim_ = 0.0F; // 0..1 raw (sense easing)
|
|
float animated_h_ = 0.0F; // Alçada animada amb smoothing exponencial
|
|
float animated_w_ = 0.0F; // Ample animat (eixampla segons contingut)
|
|
|
|
// Estat del highlight (rectangle del cursor). Es lerpa cap a l'item
|
|
// actiu amb ease-out exponencial; quan el cursor "salta" (open o
|
|
// push/pop de pagina), s'enganxa directament al nou objectiu.
|
|
float highlight_y_ = 0.0F;
|
|
float highlight_h_ = 0.0F;
|
|
bool highlight_snap_ = true;
|
|
|
|
// Edge-detect de stick analogic per a navegacio. Una sola activacio
|
|
// per direccio: cal tornar a centre (sota el llindar) per disparar
|
|
// una altra. Compartit entre tots els pads — qualsevol jugador pot
|
|
// navegar el menu.
|
|
bool stick_left_held_ = false;
|
|
bool stick_right_held_ = false;
|
|
bool stick_up_held_ = false;
|
|
bool stick_down_held_ = false;
|
|
|
|
static std::unique_ptr<ServiceMenu> instance;
|
|
};
|
|
|
|
} // namespace System
|