Merge feat/control-remapping — redefinició de tecles i botons del mando
This commit is contained in:
commit
6f6aa182f7
16 files changed
+1170
-81
No files matched your search
@@ -74,6 +74,8 @@ set(APP_SOURCES
|
|||||||
source/core/input/global_inputs.cpp
|
source/core/input/global_inputs.cpp
|
||||||
source/core/input/input_state.cpp
|
source/core/input/input_state.cpp
|
||||||
source/core/input/gamepad_manager.cpp
|
source/core/input/gamepad_manager.cpp
|
||||||
|
source/core/input/gamepad_bindings.cpp
|
||||||
|
source/core/input/define_buttons.cpp
|
||||||
source/core/input/mouse.cpp
|
source/core/input/mouse.cpp
|
||||||
source/core/rendering/screen.cpp
|
source/core/rendering/screen.cpp
|
||||||
source/core/rendering/canvas.cpp
|
source/core/rendering/canvas.cpp
|
||||||
|
|||||||
@@ -71,8 +71,13 @@ menu:
|
|||||||
move_down: "Avall"
|
move_down: "Avall"
|
||||||
move_left: "Esquerra"
|
move_left: "Esquerra"
|
||||||
move_right: "Dreta"
|
move_right: "Dreta"
|
||||||
|
jump: "Saltar"
|
||||||
|
attack: "Atacar"
|
||||||
|
service: "Opcions"
|
||||||
gamepad_p1: "Comandament P1"
|
gamepad_p1: "Comandament P1"
|
||||||
gamepad_p2: "Comandament P2"
|
gamepad_p2: "Comandament P2"
|
||||||
|
define_buttons_p1: "Definir botons P1"
|
||||||
|
define_buttons_p2: "Definir botons P2"
|
||||||
zoom: "Zoom"
|
zoom: "Zoom"
|
||||||
screen: "Pantalla completa"
|
screen: "Pantalla completa"
|
||||||
vsync: "VSync"
|
vsync: "VSync"
|
||||||
@@ -104,3 +109,7 @@ menu:
|
|||||||
render_info_bottom: "Baix"
|
render_info_bottom: "Baix"
|
||||||
gamepad_none: "Cap"
|
gamepad_none: "Cap"
|
||||||
gamepad_disconnected: "Desconnectat"
|
gamepad_disconnected: "Desconnectat"
|
||||||
|
press_key: "Polsa una tecla"
|
||||||
|
press_button: "Polsa un botó"
|
||||||
|
define_buttons_complete: "Configuració completa"
|
||||||
|
define_buttons_no_pad: "Cap mando assignat"
|
||||||
@@ -57,6 +57,7 @@ menu:
|
|||||||
system: "System"
|
system: "System"
|
||||||
labels:
|
labels:
|
||||||
player_1: "Player 1"
|
player_1: "Player 1"
|
||||||
|
player_2: "Player 2"
|
||||||
gamepad_unavailable: "Not available"
|
gamepad_unavailable: "Not available"
|
||||||
items:
|
items:
|
||||||
video_submenu: "Video"
|
video_submenu: "Video"
|
||||||
@@ -69,6 +70,13 @@ menu:
|
|||||||
move_down: "Down"
|
move_down: "Down"
|
||||||
move_left: "Left"
|
move_left: "Left"
|
||||||
move_right: "Right"
|
move_right: "Right"
|
||||||
|
jump: "Jump"
|
||||||
|
attack: "Attack"
|
||||||
|
service: "Options"
|
||||||
|
gamepad_p1: "Gamepad P1"
|
||||||
|
gamepad_p2: "Gamepad P2"
|
||||||
|
define_buttons_p1: "Define buttons P1"
|
||||||
|
define_buttons_p2: "Define buttons P2"
|
||||||
zoom: "Zoom"
|
zoom: "Zoom"
|
||||||
screen: "Fullscreen"
|
screen: "Fullscreen"
|
||||||
vsync: "VSync"
|
vsync: "VSync"
|
||||||
@@ -98,3 +106,9 @@ menu:
|
|||||||
render_info_off: "Off"
|
render_info_off: "Off"
|
||||||
render_info_top: "Top"
|
render_info_top: "Top"
|
||||||
render_info_bottom: "Bottom"
|
render_info_bottom: "Bottom"
|
||||||
|
gamepad_none: "None"
|
||||||
|
gamepad_disconnected: "Disconnected"
|
||||||
|
press_key: "Press a key"
|
||||||
|
press_button: "Press a button"
|
||||||
|
define_buttons_complete: "Configuration complete"
|
||||||
|
define_buttons_no_pad: "No gamepad assigned"
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
#include "core/input/define_buttons.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/text.hpp"
|
||||||
|
#include "core/ui/ui_config.hpp"
|
||||||
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Threshold per als triggers L2/R2 — equivalent a CC (16384 ≈ 50%).
|
||||||
|
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||||
|
|
||||||
|
// Geometria de l'overlay (canvas lògic 320×240).
|
||||||
|
constexpr int BOX_WIDTH = 240;
|
||||||
|
constexpr int PAD_Y = 8;
|
||||||
|
constexpr int LINE_GAP = 4;
|
||||||
|
constexpr Uint8 BG_ALPHA = 230;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void DefineButtons::init(std::shared_ptr<Text> title_font,
|
||||||
|
std::shared_ptr<Text> items_font,
|
||||||
|
const Palette& ui_palette) {
|
||||||
|
title_font_ = std::move(title_font);
|
||||||
|
items_font_ = std::move(items_font);
|
||||||
|
ui_palette_ = ui_palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DefineButtons::enable(SDL_Gamepad* pad, std::string name, std::string path) -> bool {
|
||||||
|
if (pad == nullptr) { return false; }
|
||||||
|
pad_ = pad;
|
||||||
|
pad_name_ = std::move(name);
|
||||||
|
pad_path_ = std::move(path);
|
||||||
|
enabled_ = true;
|
||||||
|
finished_ = false;
|
||||||
|
closing_ = false;
|
||||||
|
message_shown_ = false;
|
||||||
|
message_timer_ms_ = 0;
|
||||||
|
l2_was_pressed_ = false;
|
||||||
|
r2_was_pressed_ = false;
|
||||||
|
resetSequence();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::disable() {
|
||||||
|
enabled_ = false;
|
||||||
|
finished_ = false;
|
||||||
|
closing_ = false;
|
||||||
|
message_shown_ = false;
|
||||||
|
message_timer_ms_ = 0;
|
||||||
|
l2_was_pressed_ = false;
|
||||||
|
r2_was_pressed_ = false;
|
||||||
|
pad_ = nullptr;
|
||||||
|
pad_name_.clear();
|
||||||
|
pad_path_.clear();
|
||||||
|
sequence_.clear();
|
||||||
|
index_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::resetSequence() {
|
||||||
|
// Ordre: ATTACK, JUMP, SERVICE — replica l'estructura de
|
||||||
|
// `clearButtons()` de coffee_crisis_arcade_edition (seqüència ordenada
|
||||||
|
// d'accions amb la SERVICE al final) adaptada al joc actual. La label
|
||||||
|
// de SERVICE va a una clau pròpia (`menu.items.service`) per no
|
||||||
|
// reutilitzar la del submenú dels mandos.
|
||||||
|
sequence_.clear();
|
||||||
|
sequence_.push_back({"menu.items.attack", GamepadBindings::Action::ATTACK, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||||
|
sequence_.push_back({"menu.items.jump", GamepadBindings::Action::JUMP, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||||
|
sequence_.push_back({"menu.items.service", GamepadBindings::Action::SERVICE, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||||
|
index_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DefineButtons::isInUse(int button) const -> bool {
|
||||||
|
return std::ranges::any_of(sequence_, [button](const ButtonAssignment& a) {
|
||||||
|
return a.button == button;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DefineButtons::isReadyToClose() const -> bool {
|
||||||
|
// Llest per tancar quan: la seqüència ha acabat, ja s'ha mostrat el
|
||||||
|
// missatge, està en mode tancament, i el timer ha expirat. Replica el
|
||||||
|
// patró de `coffee_crisis_arcade_edition::DefineButtons::isReadyToClose`,
|
||||||
|
// simplificat perquè ací no tenim animació de WindowMessage.
|
||||||
|
return finished_ && message_shown_ && closing_ && message_timer_ms_ <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::update(int delta_ms) {
|
||||||
|
if (!enabled_) { return; }
|
||||||
|
if (finished_ && message_shown_) {
|
||||||
|
message_timer_ms_ -= delta_ms;
|
||||||
|
if (message_timer_ms_ <= 0 && !closing_) {
|
||||||
|
closing_ = true;
|
||||||
|
message_timer_ms_ = 0; // anchor a 0 per a `isReadyToClose`.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::handleEvent(const SDL_Event& ev) {
|
||||||
|
if (!enabled_) { return; }
|
||||||
|
switch (ev.type) {
|
||||||
|
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||||
|
handleButtonDown(ev.gbutton);
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||||
|
onButtonUp();
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
||||||
|
handleAxisMotion(ev.gaxis);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::assignAndAdvance(int button) {
|
||||||
|
if (finished_ || isInUse(button)) { return; }
|
||||||
|
sequence_.at(index_).button = button;
|
||||||
|
if (index_ + 1 < sequence_.size()) {
|
||||||
|
++index_;
|
||||||
|
} else {
|
||||||
|
finished_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::processTrigger(int virtual_button, bool& was_pressed, bool now) {
|
||||||
|
if (now && !was_pressed) { assignAndAdvance(virtual_button); }
|
||||||
|
if (!now && was_pressed) { onButtonUp(); }
|
||||||
|
was_pressed = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::handleButtonDown(const SDL_GamepadButtonEvent& ev) {
|
||||||
|
// Filtra: només acceptem events del mando que estem configurant.
|
||||||
|
// (Per l'instance_id no podem comparar directament; obtenim el handle
|
||||||
|
// i el comparem.)
|
||||||
|
const SDL_Gamepad* source = SDL_GetGamepadFromID(ev.which);
|
||||||
|
if (source != pad_) { return; }
|
||||||
|
assignAndAdvance(static_cast<int>(ev.button));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::handleAxisMotion(const SDL_GamepadAxisEvent& ev) {
|
||||||
|
const SDL_Gamepad* source = SDL_GetGamepadFromID(ev.which);
|
||||||
|
if (source != pad_) { return; }
|
||||||
|
|
||||||
|
// Triggers L2/R2 com a "botons" amb edge-detect (transició inactiu →
|
||||||
|
// actiu). Replica `doControllerAxisMotion` de
|
||||||
|
// coffee_crisis_arcade_edition.
|
||||||
|
const bool NOW = ev.value > TRIGGER_THRESHOLD;
|
||||||
|
if (ev.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
|
||||||
|
processTrigger(GamepadBindings::TRIGGER_L2_AS_BUTTON, l2_was_pressed_, NOW);
|
||||||
|
} else if (ev.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||||
|
processTrigger(GamepadBindings::TRIGGER_R2_AS_BUTTON, r2_was_pressed_, NOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::onButtonUp() {
|
||||||
|
if (!finished_ || message_shown_) { return; }
|
||||||
|
persistAndShowComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::persistAndShowComplete() {
|
||||||
|
std::unordered_map<GamepadBindings::Action, int> mapping;
|
||||||
|
for (const auto& a : sequence_) {
|
||||||
|
mapping[a.action] = a.button;
|
||||||
|
}
|
||||||
|
GamepadBindings::get().setBindings(pad_name_, pad_path_, mapping);
|
||||||
|
message_shown_ = true;
|
||||||
|
message_timer_ms_ = MESSAGE_DISPLAY_DURATION_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineButtons::render() const {
|
||||||
|
if (!enabled_ || !title_font_ || !items_font_) { return; }
|
||||||
|
|
||||||
|
const int TITLE_H = title_font_->getLineHeight();
|
||||||
|
const int ITEM_H = items_font_->getLineHeight();
|
||||||
|
const int TITLE_KERNING = title_font_->getDefaultKerning();
|
||||||
|
const int ITEM_KERNING = items_font_->getDefaultKerning();
|
||||||
|
|
||||||
|
// Calcular l'alçada total: títol + 2 línies (instrucció + label).
|
||||||
|
int box_h = (PAD_Y * 2) + TITLE_H + LINE_GAP + (ITEM_H * 2) + LINE_GAP;
|
||||||
|
int box_x = (GameCanvas::WIDTH - BOX_WIDTH) / 2;
|
||||||
|
int box_y = (GameCanvas::HEIGHT - box_h) / 2;
|
||||||
|
|
||||||
|
const auto BG_ARGB = ui_palette_[UiConfig::get().menuBg()];
|
||||||
|
const auto BORDER_ARGB = ui_palette_[UiConfig::get().menuBorder()];
|
||||||
|
Screen::get().blendRect(box_x, box_y, BOX_WIDTH, box_h, BG_ARGB, BG_ALPHA);
|
||||||
|
Screen::get().fillRect(box_x, box_y, BOX_WIDTH, 1, BORDER_ARGB);
|
||||||
|
Screen::get().fillRect(box_x, box_y + box_h - 1, BOX_WIDTH, 1, BORDER_ARGB);
|
||||||
|
Screen::get().fillRect(box_x, box_y, 1, box_h, BORDER_ARGB);
|
||||||
|
Screen::get().fillRect(box_x + BOX_WIDTH - 1, box_y, 1, box_h, BORDER_ARGB);
|
||||||
|
|
||||||
|
int cursor_y = box_y + PAD_Y;
|
||||||
|
const int CENTER_X = box_x + (BOX_WIDTH / 2);
|
||||||
|
|
||||||
|
// Títol: nom del mando.
|
||||||
|
const std::string TITLE = pad_name_;
|
||||||
|
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
CENTER_X,
|
||||||
|
cursor_y,
|
||||||
|
TITLE,
|
||||||
|
TITLE_KERNING,
|
||||||
|
UiConfig::get().menuTitle());
|
||||||
|
cursor_y += TITLE_H + LINE_GAP;
|
||||||
|
|
||||||
|
if (message_shown_) {
|
||||||
|
// Pantalla final: missatge de configuració completa.
|
||||||
|
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
CENTER_X,
|
||||||
|
cursor_y,
|
||||||
|
Locale::get().text("menu.values.define_buttons_complete"),
|
||||||
|
ITEM_KERNING,
|
||||||
|
UiConfig::get().menuValue());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instrucció + acció pendent. Replica visualment el WindowMessage de
|
||||||
|
// coffee_crisis_arcade_edition.
|
||||||
|
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
CENTER_X,
|
||||||
|
cursor_y,
|
||||||
|
Locale::get().text("menu.values.press_button"),
|
||||||
|
ITEM_KERNING,
|
||||||
|
UiConfig::get().menuLabel());
|
||||||
|
cursor_y += ITEM_H + LINE_GAP;
|
||||||
|
|
||||||
|
if (index_ < sequence_.size()) {
|
||||||
|
const auto& current = sequence_[index_];
|
||||||
|
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
CENTER_X,
|
||||||
|
cursor_y,
|
||||||
|
Locale::get().text(current.label_key),
|
||||||
|
ITEM_KERNING,
|
||||||
|
UiConfig::get().menuCursor());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/input/gamepad_bindings.hpp"
|
||||||
|
#include "core/rendering/surface.hpp" // Palette
|
||||||
|
|
||||||
|
class Text;
|
||||||
|
|
||||||
|
// Sub-mòdul de captura seqüencial de botons per a un mando concret.
|
||||||
|
// Replica `DefineButtons` de coffee_crisis_arcade_edition: l'usuari obre el
|
||||||
|
// menú de servei, escull "Definir botons P1" / "P2", i el sub-mòdul li
|
||||||
|
// demana un per un els botons per a cada acció (JUMP → ATTACK → SERVICE).
|
||||||
|
//
|
||||||
|
// Cicle de vida:
|
||||||
|
// 1. `enable(pad, name, path)`: arrenca la seqüència. `index_` = 0.
|
||||||
|
// 2. Cada SDL_EVENT_GAMEPAD_BUTTON_DOWN (o transició de L2/R2 via
|
||||||
|
// AXIS_MOTION) que NO duplique cap botó ja assignat dins la sessió
|
||||||
|
// avança `index_`. Si `index_` arriba a la mida de la seqüència,
|
||||||
|
// `finished_` es posa a true i, al pròxim BUTTON_UP, escrivim el mapeig
|
||||||
|
// complet a `GamepadBindings`, mostrem "Configuració completa" durant
|
||||||
|
// `MESSAGE_DISPLAY_DURATION_MS`, i posem `closing_`. Quan tot l'overlay
|
||||||
|
// ja no és visible, `isReadyToClose()` torna `true` i el caller ens
|
||||||
|
// fa `disable()`.
|
||||||
|
//
|
||||||
|
// El layer runtime de l'aplicació viu a `GamepadBindings`: aquesta classe
|
||||||
|
// només decideix què s'escriu i quan.
|
||||||
|
class DefineButtons {
|
||||||
|
public:
|
||||||
|
DefineButtons() = default;
|
||||||
|
~DefineButtons() = default;
|
||||||
|
DefineButtons(const DefineButtons&) = delete;
|
||||||
|
DefineButtons(DefineButtons&&) = delete;
|
||||||
|
auto operator=(const DefineButtons&) -> DefineButtons& = delete;
|
||||||
|
auto operator=(DefineButtons&&) -> DefineButtons& = delete;
|
||||||
|
|
||||||
|
// Injecta les fonts i la paleta UI; cridat des de `ServiceMenu::init`
|
||||||
|
// perquè comparteixin tipografia i colors de tema.
|
||||||
|
void init(std::shared_ptr<Text> title_font,
|
||||||
|
std::shared_ptr<Text> items_font,
|
||||||
|
const Palette& ui_palette);
|
||||||
|
|
||||||
|
// Arranca la sessió per al mando indicat. `pad` ha de ser un handle
|
||||||
|
// viu obtingut de `GamepadManager`. Retorna false si `pad` és nullptr.
|
||||||
|
auto enable(SDL_Gamepad* pad, std::string name, std::string path) -> bool;
|
||||||
|
|
||||||
|
// Atura la sessió (sense persistir) i neteja l'estat.
|
||||||
|
void disable();
|
||||||
|
|
||||||
|
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||||
|
[[nodiscard]] auto isReadyToClose() const -> bool;
|
||||||
|
|
||||||
|
// Avança el timer de tancament (ms) — replica el `update` de
|
||||||
|
// coffee_crisis_arcade_edition. No fa res si no està actiu.
|
||||||
|
void update(int delta_ms);
|
||||||
|
|
||||||
|
// Pinta l'overlay sobre el canvas actual (centrat). No fa res si no
|
||||||
|
// està actiu.
|
||||||
|
void render() const;
|
||||||
|
|
||||||
|
// Punt d'entrada d'events: BUTTON_DOWN / BUTTON_UP / AXIS_MOTION del
|
||||||
|
// mando objectiu. La resta s'ignoren.
|
||||||
|
void handleEvent(const SDL_Event& ev);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int MESSAGE_DISPLAY_DURATION_MS = 2000;
|
||||||
|
|
||||||
|
struct ButtonAssignment {
|
||||||
|
std::string label_key;
|
||||||
|
GamepadBindings::Action action;
|
||||||
|
int button; // SDL_GamepadButton o trigger virtual; INVALID = sense assignar.
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reinicialitza la seqüència ordenada (JUMP → ATTACK → SERVICE) amb
|
||||||
|
// tots els botons sense assignar. Cridat a `enable`.
|
||||||
|
void resetSequence();
|
||||||
|
|
||||||
|
// Comprova si un botó ja ha estat assignat dins de la sessió actual.
|
||||||
|
// Replica `checkButtonNotInUse` / `checkTriggerNotInUse`.
|
||||||
|
[[nodiscard]] auto isInUse(int button) const -> bool;
|
||||||
|
|
||||||
|
void handleButtonDown(const SDL_GamepadButtonEvent& ev);
|
||||||
|
void handleAxisMotion(const SDL_GamepadAxisEvent& ev);
|
||||||
|
|
||||||
|
// Helper de la lògica comuna a button-down i als triggers L2/R2:
|
||||||
|
// assigna `button` a l'acció pendent i avança l'índex (o marca
|
||||||
|
// finished_ si era l'última). No fa res si `button` ja està en ús.
|
||||||
|
void assignAndAdvance(int button);
|
||||||
|
|
||||||
|
// Tracta una transició de trigger (L2 o R2) com a botó virtual amb
|
||||||
|
// edge-detect. `virtual_button` és `TRIGGER_L?_AS_BUTTON`,
|
||||||
|
// `was_pressed` és l'estat anterior (mutat per referència).
|
||||||
|
void processTrigger(int virtual_button, bool& was_pressed, bool now);
|
||||||
|
|
||||||
|
// Crida `onSequenceEnd()` quan l'usuari deixa anar el botó després
|
||||||
|
// d'haver completat la seqüència — replica la branca de
|
||||||
|
// `SDL_EVENT_GAMEPAD_BUTTON_UP` a `DefineButtons::handleEvents`.
|
||||||
|
void onButtonUp();
|
||||||
|
|
||||||
|
// Persistix les assignacions a `GamepadBindings` i posa `message_shown_`.
|
||||||
|
void persistAndShowComplete();
|
||||||
|
|
||||||
|
SDL_Gamepad* pad_{nullptr};
|
||||||
|
std::string pad_name_;
|
||||||
|
std::string pad_path_;
|
||||||
|
|
||||||
|
std::shared_ptr<Text> title_font_;
|
||||||
|
std::shared_ptr<Text> items_font_;
|
||||||
|
Palette ui_palette_{};
|
||||||
|
|
||||||
|
std::vector<ButtonAssignment> sequence_;
|
||||||
|
std::size_t index_{0};
|
||||||
|
int message_timer_ms_{0};
|
||||||
|
|
||||||
|
bool enabled_{false};
|
||||||
|
bool finished_{false};
|
||||||
|
bool closing_{false};
|
||||||
|
bool message_shown_{false};
|
||||||
|
bool l2_was_pressed_{false};
|
||||||
|
bool r2_was_pressed_{false};
|
||||||
|
};
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
#include "core/input/gamepad_bindings.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Threshold per a considerar L2/R2 com a premuts (mateix valor que
|
||||||
|
// coffee_crisis_arcade_edition: 16384 ≈ 50% de ±32767).
|
||||||
|
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||||
|
|
||||||
|
// Taules de mapejat string ↔ valor: replica BUTTON_TO_STRING /
|
||||||
|
// STRING_TO_BUTTON / ACTION_TO_STRING / STRING_TO_ACTION del projecte
|
||||||
|
// de referència. Els noms en lowercase per llegibilitat al YAML.
|
||||||
|
using ActionPair = std::pair<GamepadBindings::Action, std::string_view>;
|
||||||
|
constexpr std::array ACTION_NAMES = {
|
||||||
|
ActionPair{GamepadBindings::Action::JUMP, std::string_view{"jump"}},
|
||||||
|
ActionPair{GamepadBindings::Action::ATTACK, std::string_view{"attack"}},
|
||||||
|
ActionPair{GamepadBindings::Action::SERVICE, std::string_view{"service"}},
|
||||||
|
};
|
||||||
|
|
||||||
|
using ButtonPair = std::pair<int, std::string_view>;
|
||||||
|
constexpr std::array BUTTON_NAMES = {
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH), std::string_view{"south"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_EAST), std::string_view{"east"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_WEST), std::string_view{"west"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_NORTH), std::string_view{"north"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_BACK), std::string_view{"back"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_GUIDE), std::string_view{"guide"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_START), std::string_view{"start"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_STICK), std::string_view{"left_stick"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_STICK), std::string_view{"right_stick"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), std::string_view{"l1"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), std::string_view{"r1"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_UP), std::string_view{"dpad_up"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_DOWN), std::string_view{"dpad_down"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT), std::string_view{"dpad_left"}},
|
||||||
|
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), std::string_view{"dpad_right"}},
|
||||||
|
ButtonPair{GamepadBindings::TRIGGER_L2_AS_BUTTON, std::string_view{"l2"}},
|
||||||
|
ButtonPair{GamepadBindings::TRIGGER_R2_AS_BUTTON, std::string_view{"r2"}},
|
||||||
|
};
|
||||||
|
|
||||||
|
auto actionToString(GamepadBindings::Action a) -> std::string_view {
|
||||||
|
for (const auto& [act, name] : ACTION_NAMES) {
|
||||||
|
if (act == a) { return name; }
|
||||||
|
}
|
||||||
|
return ACTION_NAMES.front().second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stringToAction(std::string_view s, GamepadBindings::Action& out) -> bool {
|
||||||
|
for (const auto& [act, name] : ACTION_NAMES) {
|
||||||
|
if (name == s) {
|
||||||
|
out = act;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto buttonToString(int btn) -> std::string_view {
|
||||||
|
for (const auto& [val, name] : BUTTON_NAMES) {
|
||||||
|
if (val == btn) { return name; }
|
||||||
|
}
|
||||||
|
return std::string_view{""};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stringToButton(std::string_view s, int& out) -> bool {
|
||||||
|
for (const auto& [val, name] : BUTTON_NAMES) {
|
||||||
|
if (name == s) {
|
||||||
|
out = val;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto GamepadBindings::get() -> GamepadBindings& {
|
||||||
|
static GamepadBindings instance_;
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GamepadBindings::defaultButton(Action action) -> int {
|
||||||
|
// Defaults històrics de l'aee_2026 (vegeu Player::accumulateGamepadMovement
|
||||||
|
// i les consultes del Director per al menú).
|
||||||
|
switch (action) {
|
||||||
|
case Action::JUMP:
|
||||||
|
return static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH);
|
||||||
|
case Action::ATTACK:
|
||||||
|
return static_cast<int>(SDL_GAMEPAD_BUTTON_WEST);
|
||||||
|
case Action::SERVICE:
|
||||||
|
return static_cast<int>(SDL_GAMEPAD_BUTTON_BACK);
|
||||||
|
}
|
||||||
|
return static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GamepadBindings::findConfig(const std::string& name, const std::string& path) const -> int {
|
||||||
|
if (!path.empty()) {
|
||||||
|
for (std::size_t i = 0; i < configs_.size(); ++i) {
|
||||||
|
if (configs_[i].path == path) { return static_cast<int>(i); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!name.empty()) {
|
||||||
|
for (std::size_t i = 0; i < configs_.size(); ++i) {
|
||||||
|
if (configs_[i].name == name) { return static_cast<int>(i); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GamepadBindings::getButton(const std::string& name, const std::string& path, Action action) const -> int {
|
||||||
|
const int IDX = findConfig(name, path);
|
||||||
|
if (IDX < 0) { return defaultButton(action); }
|
||||||
|
const auto& m = configs_[static_cast<std::size_t>(IDX)].bindings;
|
||||||
|
auto it = m.find(action);
|
||||||
|
return (it != m.end()) ? it->second : defaultButton(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GamepadBindings::setBindings(const std::string& name,
|
||||||
|
const std::string& path,
|
||||||
|
const std::unordered_map<Action, int>& bindings) {
|
||||||
|
int idx = findConfig(name, path);
|
||||||
|
if (idx < 0) {
|
||||||
|
configs_.push_back(Config{.name = name, .path = path, .bindings = bindings});
|
||||||
|
} else {
|
||||||
|
auto& cfg = configs_[static_cast<std::size_t>(idx)];
|
||||||
|
cfg.name = name;
|
||||||
|
cfg.path = path;
|
||||||
|
cfg.bindings = bindings;
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GamepadBindings::isPressed(SDL_Gamepad* pad, int button) -> bool {
|
||||||
|
if (pad == nullptr) { return false; }
|
||||||
|
if (button == TRIGGER_L2_AS_BUTTON) {
|
||||||
|
return SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_THRESHOLD;
|
||||||
|
}
|
||||||
|
if (button == TRIGGER_R2_AS_BUTTON) {
|
||||||
|
return SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_THRESHOLD;
|
||||||
|
}
|
||||||
|
return SDL_GetGamepadButton(pad, static_cast<SDL_GamepadButton>(button));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GamepadBindings::load(const std::string& file_path) {
|
||||||
|
file_path_ = file_path;
|
||||||
|
configs_.clear();
|
||||||
|
|
||||||
|
std::ifstream file(file_path);
|
||||||
|
if (!file) { return; }
|
||||||
|
std::stringstream buf;
|
||||||
|
buf << file.rdbuf();
|
||||||
|
const std::string CONTENT = buf.str();
|
||||||
|
if (CONTENT.empty()) { return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto yaml = fkyaml::node::deserialize(CONTENT);
|
||||||
|
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
|
||||||
|
std::cerr << "GamepadBindings: secció 'gamepads' absent o invàlida a "
|
||||||
|
<< file_path << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto& entry : yaml["gamepads"]) {
|
||||||
|
Config cfg;
|
||||||
|
if (entry.contains("name")) { cfg.name = entry["name"].get_value<std::string>(); }
|
||||||
|
if (entry.contains("path")) { cfg.path = entry["path"].get_value<std::string>(); }
|
||||||
|
if (entry.contains("bindings") && entry["bindings"].is_mapping()) {
|
||||||
|
for (const auto& kv : entry["bindings"].as_map()) {
|
||||||
|
const auto KEY = kv.first.get_value<std::string>();
|
||||||
|
const auto VAL = kv.second.get_value<std::string>();
|
||||||
|
Action action{};
|
||||||
|
int btn = 0;
|
||||||
|
if (stringToAction(KEY, action) && stringToButton(VAL, btn)) {
|
||||||
|
cfg.bindings[action] = btn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configs_.push_back(std::move(cfg));
|
||||||
|
}
|
||||||
|
std::cout << "GamepadBindings: " << configs_.size() << " mandos carregats de "
|
||||||
|
<< file_path << '\n';
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "GamepadBindings: error parsejant " << file_path << ": "
|
||||||
|
<< e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GamepadBindings::save() -> bool {
|
||||||
|
if (file_path_.empty()) { return false; }
|
||||||
|
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "# AEE 2026 - Vinculacions de mando per dispositiu.\n";
|
||||||
|
out << "# Auto-generat des del menú de servei. Editable a mà.\n";
|
||||||
|
out << "gamepads:\n";
|
||||||
|
for (const auto& cfg : configs_) {
|
||||||
|
out << " - name: \"" << cfg.name << "\"\n";
|
||||||
|
out << " path: \"" << cfg.path << "\"\n";
|
||||||
|
out << " bindings:\n";
|
||||||
|
for (const auto& [action, button] : cfg.bindings) {
|
||||||
|
out << " " << actionToString(action) << ": \""
|
||||||
|
<< buttonToString(button) << "\"\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream file(file_path_, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "GamepadBindings: no es pot escriure " << file_path_ << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file << out.str();
|
||||||
|
return file.good();
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Vinculacions remapeables d'accions del joc a botons d'un mando concret,
|
||||||
|
// indexades per `(name, path)` del mando — replica el patró
|
||||||
|
// `GamepadConfig` + `GamepadConfigManager` de coffee_crisis_arcade_edition.
|
||||||
|
//
|
||||||
|
// Persisteix en un YAML aïllat al system folder (no a `config.yaml`) perquè
|
||||||
|
// és per dispositiu, no per usuari/perfil: dos mandos diferents poden
|
||||||
|
// tenir mappings diferents i cap d'ells va lligat a un slot de jugador.
|
||||||
|
//
|
||||||
|
// El layer runtime (consultat per Player/Director cada frame) és
|
||||||
|
// directament aquesta classe; no hi ha caché a `GamepadManager::Entry`. El
|
||||||
|
// nombre d'entries persistides és típicament <10, i les accions són 3
|
||||||
|
// (JUMP/ATTACK/SERVICE), així que les consultes són trivialment ràpides.
|
||||||
|
class GamepadBindings {
|
||||||
|
public:
|
||||||
|
enum class Action : std::uint8_t {
|
||||||
|
JUMP,
|
||||||
|
ATTACK,
|
||||||
|
SERVICE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Triggers L2/R2 representats com a "botons virtuals" — replica la
|
||||||
|
// convenció de coffee_crisis_arcade_edition (`TRIGGER_L2_AS_BUTTON`,
|
||||||
|
// `TRIGGER_R2_AS_BUTTON`). El threshold viu a `isPressed`.
|
||||||
|
static constexpr int TRIGGER_L2_AS_BUTTON = 100;
|
||||||
|
static constexpr int TRIGGER_R2_AS_BUTTON = 101;
|
||||||
|
|
||||||
|
static auto get() -> GamepadBindings&;
|
||||||
|
|
||||||
|
GamepadBindings(const GamepadBindings&) = delete;
|
||||||
|
GamepadBindings(GamepadBindings&&) = delete;
|
||||||
|
auto operator=(const GamepadBindings&) -> GamepadBindings& = delete;
|
||||||
|
auto operator=(GamepadBindings&&) -> GamepadBindings& = delete;
|
||||||
|
|
||||||
|
// Fixa la ruta del fitxer persistent + el carrega. Si no existix
|
||||||
|
// encara, l'estat queda buit (les consultes retornaran defaults) i
|
||||||
|
// `save()` el crearà al primer `setBindings`.
|
||||||
|
void load(const std::string& file_path);
|
||||||
|
|
||||||
|
// Persistix l'estat actual (totes les Config conegudes). Cridat
|
||||||
|
// automàticament dins de `setBindings` — no cal cridar-la a mà.
|
||||||
|
auto save() -> bool;
|
||||||
|
|
||||||
|
// Retorna el botó (o trigger virtual 100/101) lligat a `action` per al
|
||||||
|
// mando identificat per (name, path). Si no hi ha entrada per aquest
|
||||||
|
// mando, retorna el default global. La cerca és primer per `path`
|
||||||
|
// (re-vincular el mateix mando al mateix port és prioritari) i, si no
|
||||||
|
// troba, fallback per `name` (mando connectat a un altre port USB).
|
||||||
|
[[nodiscard]] auto getButton(const std::string& name, const std::string& path, Action action) const -> int;
|
||||||
|
|
||||||
|
// Reescriu totes les vinculacions del mando (name, path). Si no hi
|
||||||
|
// havia config per aquest dispositiu, en crea una nova. Persisteix a
|
||||||
|
// disc immediatament. Replicació de `Input::saveGamepadConfigFromGamepad`.
|
||||||
|
void setBindings(const std::string& name,
|
||||||
|
const std::string& path,
|
||||||
|
const std::unordered_map<Action, int>& bindings);
|
||||||
|
|
||||||
|
// Wrapper de polling: torna `true` si el "botó" (o trigger virtual)
|
||||||
|
// està premut ara mateix al mando `pad`. Encapsula el threshold per
|
||||||
|
// a L2/R2 (~50% del rang).
|
||||||
|
[[nodiscard]] static auto isPressed(SDL_Gamepad* pad, int button) -> bool;
|
||||||
|
|
||||||
|
// Default de fàbrica per a una acció — usat tant pel fallback de
|
||||||
|
// `getButton` com per inicialitzar la seqüència del DefineButtons.
|
||||||
|
[[nodiscard]] static auto defaultButton(Action action) -> int;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Config {
|
||||||
|
std::string name;
|
||||||
|
std::string path;
|
||||||
|
std::unordered_map<Action, int> bindings;
|
||||||
|
};
|
||||||
|
|
||||||
|
GamepadBindings() = default;
|
||||||
|
~GamepadBindings() = default;
|
||||||
|
|
||||||
|
// Localitza la Config que coincideix amb (name, path); retorna -1 si
|
||||||
|
// no la troba. Match prioritari per path, fallback per nom.
|
||||||
|
[[nodiscard]] auto findConfig(const std::string& name, const std::string& path) const -> int;
|
||||||
|
|
||||||
|
std::vector<Config> configs_;
|
||||||
|
std::string file_path_;
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/audio/sound_effects_config.hpp"
|
#include "core/audio/sound_effects_config.hpp"
|
||||||
|
#include "core/input/gamepad_bindings.hpp"
|
||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "core/input/key_config.hpp"
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
@@ -82,6 +83,11 @@ Director::Director() {
|
|||||||
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
|
||||||
|
// perquè és per (name, path) del mando, no per perfil/usuari. Replica
|
||||||
|
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
|
||||||
|
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
|
||||||
|
|
||||||
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
|
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
|
||||||
// es crea amb presets per defecte (off, crt, ntsc).
|
// es crea amb presets per defecte (off, crt, ntsc).
|
||||||
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
||||||
@@ -296,12 +302,29 @@ void Director::processEvents() {
|
|||||||
if (QUIT_REQUESTED) { current_scene_.reset(); }
|
if (QUIT_REQUESTED) { current_scene_.reset(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Resol el botó SERVICE configurat per al mando origen d'aquest event.
|
||||||
|
// Retorna -1 si no en troba cap. La cerca passa per `GamepadManager`
|
||||||
|
// per obtenir el (name, path) i d'ahí a `GamepadBindings`.
|
||||||
|
auto serviceButtonForGamepadEvent(SDL_JoystickID jid) -> int {
|
||||||
|
for (const auto& info : GamepadManager::get().getAll()) {
|
||||||
|
if (info.pad == nullptr) { continue; }
|
||||||
|
if (SDL_GetJoystickID(SDL_GetGamepadJoystick(info.pad)) != jid) { continue; }
|
||||||
|
return GamepadBindings::get().getButton(info.name, info.path, GamepadBindings::Action::SERVICE);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void Director::processGamepadEvents() {
|
void Director::processGamepadEvents() {
|
||||||
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
|
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
|
||||||
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
|
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
|
||||||
// tractar el botó BACK com a toggle del menú quan està tancat (el
|
// tractar el botó SERVICE (per defecte BACK; remapejable per
|
||||||
// paral·lel de F9 per al teclat). Quan el menú està obert, el BACK
|
// `GamepadBindings`) com a toggle del menú quan està tancat (el
|
||||||
// passa pel pipeline del `ServiceMenu::handleEvent()` via
|
// paral·lel de F9 per al teclat). Quan el menú està obert, el botó
|
||||||
|
// SERVICE passa pel pipeline del `ServiceMenu::handleEvent()` via
|
||||||
// `processMenuInput()` i allà es converteix en `close()`.
|
// `processMenuInput()` i allà es converteix en `close()`.
|
||||||
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
||||||
bool consumed = false;
|
bool consumed = false;
|
||||||
@@ -309,10 +332,12 @@ void Director::processGamepadEvents() {
|
|||||||
GamepadManager::get().handleEvent(*it);
|
GamepadManager::get().handleEvent(*it);
|
||||||
consumed = true;
|
consumed = true;
|
||||||
} else if (it->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
|
} else if (it->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
|
||||||
it->gbutton.button == SDL_GAMEPAD_BUTTON_BACK &&
|
|
||||||
!ServiceMenu::get().isOpen()) {
|
!ServiceMenu::get().isOpen()) {
|
||||||
ServiceMenu::get().toggle();
|
const int SERVICE_BTN = serviceButtonForGamepadEvent(it->gbutton.which);
|
||||||
consumed = true;
|
if (SERVICE_BTN >= 0 && static_cast<int>(it->gbutton.button) == SERVICE_BTN) {
|
||||||
|
ServiceMenu::get().toggle();
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (consumed) {
|
if (consumed) {
|
||||||
it = EventBuffer::events.erase(it);
|
it = EventBuffer::events.erase(it);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/input/gamepad_bindings.hpp"
|
||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "core/input/input_state.hpp"
|
#include "core/input/input_state.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/options.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -578,6 +580,12 @@ void Player::tick(float delta_time) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresca les vinculacions des d'Options en calent: així un remap fet
|
||||||
|
// al menú de servei té efecte immediatament, sense esperar al pròxim
|
||||||
|
// GameScene::onEnter. Replica el comportament d'AEE
|
||||||
|
// (`KeyRemap::update` que llig `Options::keys_game` cada frame).
|
||||||
|
refreshControlsFromOptions();
|
||||||
|
|
||||||
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
|
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
|
||||||
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
|
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
|
||||||
// un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari
|
// un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari
|
||||||
@@ -653,6 +661,18 @@ void Player::tick(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Player::refreshControlsFromOptions() {
|
||||||
|
if (id_ != 0 && id_ != 1) { return; }
|
||||||
|
const auto& kb = (id_ == 0) ? Options::controls().keyboard_p1
|
||||||
|
: Options::controls().keyboard_p2;
|
||||||
|
controls_.up = kb.up;
|
||||||
|
controls_.down = kb.down;
|
||||||
|
controls_.left = kb.left;
|
||||||
|
controls_.right = kb.right;
|
||||||
|
controls_.jump = kb.jump;
|
||||||
|
controls_.attack = kb.attack;
|
||||||
|
}
|
||||||
|
|
||||||
auto Player::consumeJumpEdge() -> bool {
|
auto Player::consumeJumpEdge() -> bool {
|
||||||
// Edge-detection independent per teclat i mando: si qualsevol dels
|
// Edge-detection independent per teclat i mando: si qualsevol dels
|
||||||
// dos passa de no-premut a premut aquest frame, comptem com a salt.
|
// dos passa de no-premut a premut aquest frame, comptem com a salt.
|
||||||
@@ -664,7 +684,11 @@ auto Player::consumeJumpEdge() -> bool {
|
|||||||
jump_held_keyboard_ = KEY_NOW;
|
jump_held_keyboard_ = KEY_NOW;
|
||||||
|
|
||||||
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
|
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
|
||||||
const bool PAD_NOW = (pad != nullptr) && InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH);
|
const auto& slot = (id_ == 0) ? Options::controls().gamepad_p1
|
||||||
|
: Options::controls().gamepad_p2;
|
||||||
|
const int JUMP_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::JUMP);
|
||||||
|
const bool PAD_NOW = (pad != nullptr) && !InputState::isBlocked() &&
|
||||||
|
GamepadBindings::isPressed(pad, JUMP_BTN);
|
||||||
if (PAD_NOW && !jump_held_gamepad_) { edge = true; }
|
if (PAD_NOW && !jump_held_gamepad_) { edge = true; }
|
||||||
jump_held_gamepad_ = PAD_NOW;
|
jump_held_gamepad_ = PAD_NOW;
|
||||||
|
|
||||||
@@ -672,16 +696,20 @@ auto Player::consumeJumpEdge() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto Player::consumeAttackEdge() -> bool {
|
auto Player::consumeAttackEdge() -> bool {
|
||||||
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. Mando:
|
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. El botó
|
||||||
// SDL_GAMEPAD_BUTTON_WEST (X a Xbox / Quadrat a PlayStation), oposat
|
// físic ve d'`GamepadBindings`, persistit per dispositiu — l'usuari
|
||||||
// a SOUTH per evitar topades semàntiques.
|
// l'ha pogut remapejar des del menú de servei.
|
||||||
const bool* keys = InputState::getKeyboardState();
|
const bool* keys = InputState::getKeyboardState();
|
||||||
const bool KEY_NOW = keys[controls_.attack];
|
const bool KEY_NOW = keys[controls_.attack];
|
||||||
bool edge = (KEY_NOW && !attack_held_keyboard_);
|
bool edge = (KEY_NOW && !attack_held_keyboard_);
|
||||||
attack_held_keyboard_ = KEY_NOW;
|
attack_held_keyboard_ = KEY_NOW;
|
||||||
|
|
||||||
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
|
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
|
||||||
const bool PAD_NOW = (pad != nullptr) && InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST);
|
const auto& slot = (id_ == 0) ? Options::controls().gamepad_p1
|
||||||
|
: Options::controls().gamepad_p2;
|
||||||
|
const int ATK_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::ATTACK);
|
||||||
|
const bool PAD_NOW = (pad != nullptr) && !InputState::isBlocked() &&
|
||||||
|
GamepadBindings::isPressed(pad, ATK_BTN);
|
||||||
if (PAD_NOW && !attack_held_gamepad_) { edge = true; }
|
if (PAD_NOW && !attack_held_gamepad_) { edge = true; }
|
||||||
attack_held_gamepad_ = PAD_NOW;
|
attack_held_gamepad_ = PAD_NOW;
|
||||||
|
|
||||||
|
|||||||
@@ -407,6 +407,12 @@ class Player : public AnimatedSprite {
|
|||||||
float (Player::*get_pos)() const,
|
float (Player::*get_pos)() const,
|
||||||
void (Player::*set_pos)(float)) -> bool;
|
void (Player::*set_pos)(float)) -> bool;
|
||||||
|
|
||||||
|
// Refresca `controls_` des d'`Options::controls()` cada frame. Replica
|
||||||
|
// el patró d'AEE on `KeyRemap::update` reflecteix Options::keys_game al
|
||||||
|
// buffer del teclat cada tick: així un remap fet al menú de servei té
|
||||||
|
// efecte immediat sense haver de tornar a entrar a la GameScene.
|
||||||
|
void refreshControlsFromOptions();
|
||||||
|
|
||||||
int id_{-1};
|
int id_{-1};
|
||||||
Controls controls_;
|
Controls controls_;
|
||||||
|
|
||||||
|
|||||||
@@ -169,11 +169,51 @@ namespace {
|
|||||||
readField(node, "path", dst.path);
|
readField(node, "path", dst.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Llegeix una tecla del YAML pel nom (com `SDL_GetScancodeName`). Si la
|
||||||
|
// clau no hi és o és invàlida, deixa `dst` intacte per preservar el
|
||||||
|
// default. Replica la política d'AEE (`KeyConfig::applyOverrides`).
|
||||||
|
void readScancodeField(const fkyaml::node& node, const char* key, SDL_Scancode& dst) {
|
||||||
|
if (!node.contains(key)) { return; }
|
||||||
|
const auto NAME = node[key].get_value<std::string>();
|
||||||
|
const SDL_Scancode SC = SDL_GetScancodeFromName(NAME.c_str());
|
||||||
|
if (SC != SDL_SCANCODE_UNKNOWN) { dst = SC; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseKeyboard(const fkyaml::node& parent, const char* key, Options::Keyboard& dst) {
|
||||||
|
if (!parent.contains(key)) { return; }
|
||||||
|
const auto& node = parent[key];
|
||||||
|
readScancodeField(node, "up", dst.up);
|
||||||
|
readScancodeField(node, "down", dst.down);
|
||||||
|
readScancodeField(node, "left", dst.left);
|
||||||
|
readScancodeField(node, "right", dst.right);
|
||||||
|
readScancodeField(node, "jump", dst.jump);
|
||||||
|
readScancodeField(node, "attack", dst.attack);
|
||||||
|
}
|
||||||
|
|
||||||
void parseControls(const fkyaml::node& yaml) {
|
void parseControls(const fkyaml::node& yaml) {
|
||||||
if (!yaml.contains("controls")) { return; }
|
if (!yaml.contains("controls")) { return; }
|
||||||
const auto& node = yaml["controls"];
|
const auto& node = yaml["controls"];
|
||||||
parseGamepadAssignment(node, "gamepad_p1", g_controls.gamepad_p1);
|
parseGamepadAssignment(node, "gamepad_p1", g_controls.gamepad_p1);
|
||||||
parseGamepadAssignment(node, "gamepad_p2", g_controls.gamepad_p2);
|
parseGamepadAssignment(node, "gamepad_p2", g_controls.gamepad_p2);
|
||||||
|
parseKeyboard(node, "keyboard_p1", g_controls.keyboard_p1);
|
||||||
|
parseKeyboard(node, "keyboard_p2", g_controls.keyboard_p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escriu una tecla com a string del nom SDL ("Up", "W", ...). Si
|
||||||
|
// l'scancode és UNKNOWN o sense nom registrat, escriu "" — no en farem
|
||||||
|
// override en càrrega gràcies a `readScancodeField`.
|
||||||
|
void writeKeyboard(std::ostringstream& out, const char* key, const Options::Keyboard& kb) {
|
||||||
|
const auto NAME_OR_EMPTY = [](SDL_Scancode sc) -> std::string {
|
||||||
|
const char* n = SDL_GetScancodeName(sc);
|
||||||
|
return (n != nullptr) ? std::string{n} : std::string{};
|
||||||
|
};
|
||||||
|
out << " " << key << ":\n";
|
||||||
|
out << " up: \"" << NAME_OR_EMPTY(kb.up) << "\"\n";
|
||||||
|
out << " down: \"" << NAME_OR_EMPTY(kb.down) << "\"\n";
|
||||||
|
out << " left: \"" << NAME_OR_EMPTY(kb.left) << "\"\n";
|
||||||
|
out << " right: \"" << NAME_OR_EMPTY(kb.right) << "\"\n";
|
||||||
|
out << " jump: \"" << NAME_OR_EMPTY(kb.jump) << "\"\n";
|
||||||
|
out << " attack: \"" << NAME_OR_EMPTY(kb.attack) << "\"\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica les assignacions persistides al GamepadManager. Ordre garantit
|
// Aplica les assignacions persistides al GamepadManager. Ordre garantit
|
||||||
@@ -561,6 +601,32 @@ namespace Options {
|
|||||||
saveToFile();
|
saveToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setKeyboardBinding(int player_id, KeyboardAction action, SDL_Scancode scancode) {
|
||||||
|
if (player_id < 0 || player_id > 1) { return; }
|
||||||
|
auto& kb = (player_id == 0) ? g_controls.keyboard_p1 : g_controls.keyboard_p2;
|
||||||
|
switch (action) {
|
||||||
|
case KeyboardAction::UP:
|
||||||
|
kb.up = scancode;
|
||||||
|
break;
|
||||||
|
case KeyboardAction::DOWN:
|
||||||
|
kb.down = scancode;
|
||||||
|
break;
|
||||||
|
case KeyboardAction::LEFT:
|
||||||
|
kb.left = scancode;
|
||||||
|
break;
|
||||||
|
case KeyboardAction::RIGHT:
|
||||||
|
kb.right = scancode;
|
||||||
|
break;
|
||||||
|
case KeyboardAction::JUMP:
|
||||||
|
kb.jump = scancode;
|
||||||
|
break;
|
||||||
|
case KeyboardAction::ATTACK:
|
||||||
|
kb.attack = scancode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
void setConfigFile(const std::string& path) {
|
void setConfigFile(const std::string& path) {
|
||||||
g_config_file_path = path;
|
g_config_file_path = path;
|
||||||
}
|
}
|
||||||
@@ -666,6 +732,8 @@ namespace Options {
|
|||||||
out << " gamepad_p2:\n";
|
out << " gamepad_p2:\n";
|
||||||
out << " name: \"" << g_controls.gamepad_p2.name << "\"\n";
|
out << " name: \"" << g_controls.gamepad_p2.name << "\"\n";
|
||||||
out << " path: \"" << g_controls.gamepad_p2.path << "\"\n";
|
out << " path: \"" << g_controls.gamepad_p2.path << "\"\n";
|
||||||
|
writeKeyboard(out, "keyboard_p1", g_controls.keyboard_p1);
|
||||||
|
writeKeyboard(out, "keyboard_p2", g_controls.keyboard_p2);
|
||||||
|
|
||||||
return writeFile(g_config_file_path, out.str());
|
return writeFile(g_config_file_path, out.str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -114,9 +117,49 @@ namespace Options {
|
|||||||
std::string path;
|
std::string path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Tecles de joc d'un jugador (vinculacions remapeables des del menú).
|
||||||
|
// Cada camp és un `SDL_Scancode` literal; els defaults venen de
|
||||||
|
// `Defaults::KeysGame*` segons el slot (P1 fletxes, P2 WASD).
|
||||||
|
struct Keyboard {
|
||||||
|
SDL_Scancode up{SDL_SCANCODE_UNKNOWN};
|
||||||
|
SDL_Scancode down{SDL_SCANCODE_UNKNOWN};
|
||||||
|
SDL_Scancode left{SDL_SCANCODE_UNKNOWN};
|
||||||
|
SDL_Scancode right{SDL_SCANCODE_UNKNOWN};
|
||||||
|
SDL_Scancode jump{SDL_SCANCODE_UNKNOWN};
|
||||||
|
SDL_Scancode attack{SDL_SCANCODE_UNKNOWN};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Identificadors d'acció per als setters genèrics: una sola funció
|
||||||
|
// `setKeyboardBinding(player_id, action, scancode)` cobreix les 12
|
||||||
|
// possibles vinculacions sense haver de generar 12 setters individuals.
|
||||||
|
enum class KeyboardAction : std::uint8_t {
|
||||||
|
UP,
|
||||||
|
DOWN,
|
||||||
|
LEFT,
|
||||||
|
RIGHT,
|
||||||
|
JUMP,
|
||||||
|
ATTACK,
|
||||||
|
};
|
||||||
|
|
||||||
struct Controls {
|
struct Controls {
|
||||||
GamepadAssignment gamepad_p1{};
|
GamepadAssignment gamepad_p1{};
|
||||||
GamepadAssignment gamepad_p2{};
|
GamepadAssignment gamepad_p2{};
|
||||||
|
Keyboard keyboard_p1{
|
||||||
|
.up = Defaults::KeysGame::UP,
|
||||||
|
.down = Defaults::KeysGame::DOWN,
|
||||||
|
.left = Defaults::KeysGame::LEFT,
|
||||||
|
.right = Defaults::KeysGame::RIGHT,
|
||||||
|
.jump = Defaults::KeysGame::JUMP,
|
||||||
|
.attack = Defaults::KeysGame::ATTACK,
|
||||||
|
};
|
||||||
|
Keyboard keyboard_p2{
|
||||||
|
.up = Defaults::KeysGameP2::UP,
|
||||||
|
.down = Defaults::KeysGameP2::DOWN,
|
||||||
|
.left = Defaults::KeysGameP2::LEFT,
|
||||||
|
.right = Defaults::KeysGameP2::RIGHT,
|
||||||
|
.jump = Defaults::KeysGameP2::JUMP,
|
||||||
|
.attack = Defaults::KeysGameP2::ATTACK,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Getters de l'estat (read-only) ---
|
// --- Getters de l'estat (read-only) ---
|
||||||
@@ -153,6 +196,12 @@ namespace Options {
|
|||||||
// = desassignació. `player_id`: 0 = P1, 1 = P2.
|
// = desassignació. `player_id`: 0 = P1, 1 = P2.
|
||||||
void setGamepadAssignment(int player_id, const std::string& name, const std::string& path);
|
void setGamepadAssignment(int player_id, const std::string& name, const std::string& path);
|
||||||
|
|
||||||
|
// Assigna un nou scancode a una acció d'un jugador i persisteix a
|
||||||
|
// `config.yaml`. `player_id`: 0 = P1, 1 = P2. Sense validació de
|
||||||
|
// conflictes — replicant la referència AEE, el mateix scancode pot
|
||||||
|
// quedar associat a múltiples accions si l'usuari ho elegeix.
|
||||||
|
void setKeyboardBinding(int player_id, KeyboardAction action, SDL_Scancode scancode);
|
||||||
|
|
||||||
// --- API pública: càrrega/desament de fitxers ---
|
// --- API pública: càrrega/desament de fitxers ---
|
||||||
void setConfigFile(const std::string& path);
|
void setConfigFile(const std::string& path);
|
||||||
auto loadFromFile() -> bool;
|
auto loadFromFile() -> bool;
|
||||||
|
|||||||
@@ -334,24 +334,27 @@ GameScene::GameScene(Config config)
|
|||||||
if (p != nullptr) { spawnWhip(*p); }
|
if (p != nullptr) { spawnWhip(*p); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Controls per jugador: P1 amb fletxes, P2 amb WASD. Cada Player
|
// Controls per jugador: P1 amb fletxes, P2 amb WASD per defecte. Les
|
||||||
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
|
// vinculacions actives venen d'`Options::controls()`, que s'inicialitza
|
||||||
// estat global d'input.
|
// amb els defaults i pot ser sobreescrita pel menú de servei (i
|
||||||
|
// persistida a `config.yaml`).
|
||||||
|
const auto& kb_p1 = Options::controls().keyboard_p1;
|
||||||
|
const auto& kb_p2 = Options::controls().keyboard_p2;
|
||||||
const Player::Controls CONTROLS_P1{
|
const Player::Controls CONTROLS_P1{
|
||||||
.up = Defaults::KeysGame::UP,
|
.up = kb_p1.up,
|
||||||
.down = Defaults::KeysGame::DOWN,
|
.down = kb_p1.down,
|
||||||
.left = Defaults::KeysGame::LEFT,
|
.left = kb_p1.left,
|
||||||
.right = Defaults::KeysGame::RIGHT,
|
.right = kb_p1.right,
|
||||||
.jump = Defaults::KeysGame::JUMP,
|
.jump = kb_p1.jump,
|
||||||
.attack = Defaults::KeysGame::ATTACK,
|
.attack = kb_p1.attack,
|
||||||
};
|
};
|
||||||
const Player::Controls CONTROLS_P2{
|
const Player::Controls CONTROLS_P2{
|
||||||
.up = Defaults::KeysGameP2::UP,
|
.up = kb_p2.up,
|
||||||
.down = Defaults::KeysGameP2::DOWN,
|
.down = kb_p2.down,
|
||||||
.left = Defaults::KeysGameP2::LEFT,
|
.left = kb_p2.left,
|
||||||
.right = Defaults::KeysGameP2::RIGHT,
|
.right = kb_p2.right,
|
||||||
.jump = Defaults::KeysGameP2::JUMP,
|
.jump = kb_p2.jump,
|
||||||
.attack = Defaults::KeysGameP2::ATTACK,
|
.attack = kb_p2.attack,
|
||||||
};
|
};
|
||||||
// Spawn lligat a la porta: tots els jugadors apareixen davant del
|
// Spawn lligat a la porta: tots els jugadors apareixen davant del
|
||||||
// passadís de sortida. `computeSpawn` deriva les coordenades de
|
// passadís de sortida. `computeSpawn` deriva les coordenades de
|
||||||
|
|||||||
+227
-54
@@ -7,6 +7,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/input/gamepad_bindings.hpp"
|
||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "core/input/input_state.hpp"
|
#include "core/input/input_state.hpp"
|
||||||
#include "core/input/key_config.hpp"
|
#include "core/input/key_config.hpp"
|
||||||
@@ -230,6 +231,9 @@ void ServiceMenu::init(std::shared_ptr<Text> title_font,
|
|||||||
font_ = std::move(title_font);
|
font_ = std::move(title_font);
|
||||||
items_font_ = std::move(items_font);
|
items_font_ = std::move(items_font);
|
||||||
ui_palette_ = ui_palette;
|
ui_palette_ = ui_palette;
|
||||||
|
// Comparteix fonts/paleta amb el sub-mòdul de captura de botons —
|
||||||
|
// aquest pinta el seu propi overlay quan està actiu.
|
||||||
|
define_buttons_.init(font_, items_font_, ui_palette_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServiceMenu::toggle() {
|
void ServiceMenu::toggle() {
|
||||||
@@ -253,6 +257,7 @@ void ServiceMenu::close() {
|
|||||||
if (!open_) { return; }
|
if (!open_) { return; }
|
||||||
open_ = false;
|
open_ = false;
|
||||||
closing_ = true; // `open_anim_` decaurà a update; la caixa es veu mentre dura.
|
closing_ = true; // `open_anim_` decaurà a update; la caixa es veu mentre dura.
|
||||||
|
capturing_ = false;
|
||||||
// Allibera el polling de seguida — la closing animation és purament
|
// Allibera el polling de seguida — la closing animation és purament
|
||||||
// visual i no ha d'impedir que el jugador es mogui un cop ha tancat.
|
// visual i no ha d'impedir que el jugador es mogui un cop ha tancat.
|
||||||
InputState::setBlocked(false);
|
InputState::setBlocked(false);
|
||||||
@@ -266,6 +271,16 @@ auto ServiceMenu::isVisible() const -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ServiceMenu::update(int delta_ms) {
|
void ServiceMenu::update(int delta_ms) {
|
||||||
|
// El sub-mòdul de captura de botons té el seu propi cicle (timer post
|
||||||
|
// assignació). Una vegada `isReadyToClose`, el desactivem perquè el
|
||||||
|
// menú principal recuperi el control de l'input.
|
||||||
|
if (define_buttons_.isEnabled()) {
|
||||||
|
define_buttons_.update(delta_ms);
|
||||||
|
if (define_buttons_.isReadyToClose()) {
|
||||||
|
define_buttons_.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const float DT = static_cast<float>(delta_ms) / MS_PER_SECOND;
|
const float DT = static_cast<float>(delta_ms) / MS_PER_SECOND;
|
||||||
|
|
||||||
if (closing_) {
|
if (closing_) {
|
||||||
@@ -419,50 +434,125 @@ void ServiceMenu::activateCurrent(int direction) {
|
|||||||
if (direction > 0 && item.action) { item.action(); }
|
if (direction > 0 && item.action) { item.action(); }
|
||||||
break;
|
break;
|
||||||
case Item::Kind::KEY_BIND:
|
case Item::Kind::KEY_BIND:
|
||||||
// TODO: entrar en mode captura quan hi hagi `set_scancode`.
|
// Entra en mode captura: el Director redirigirà el pròxim
|
||||||
// De moment no fem res — els items sense setter ja no són
|
// KEY_DOWN cap a `captureKey()`. Només té sentit amb RETURN /
|
||||||
// seleccionables (veure `makeKeyBind`), per tant aquest case
|
// ENTER / RIGHT (direction +1) — LEFT no fa res, com a la
|
||||||
// només es trepitja si algú en fa un amb setter en el futur.
|
// resta de Kinds que requereixen confirmació explícita.
|
||||||
|
if (direction > 0 && item.set_scancode) { capturing_ = true; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Resol el botó SERVICE configurat per al mando que ha emès aquest
|
||||||
|
// event. -1 si no troba cap match (mando desconegut o desconnectat
|
||||||
|
// mentrestant).
|
||||||
|
auto serviceButtonForEvent(SDL_JoystickID jid) -> int {
|
||||||
|
for (const auto& info : GamepadManager::get().getAll()) {
|
||||||
|
if (info.pad == nullptr) { continue; }
|
||||||
|
if (SDL_GetJoystickID(SDL_GetGamepadJoystick(info.pad)) != jid) { continue; }
|
||||||
|
return GamepadBindings::get().getButton(info.name, info.path, GamepadBindings::Action::SERVICE);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ServiceMenu::handleGamepadButtonDown(const SDL_GamepadButtonEvent& ev) {
|
||||||
|
// El botó SERVICE (per defecte BACK; remapejable per dispositiu via
|
||||||
|
// `GamepadBindings`) tanca el menú sencer. Cada mando pot tenir el
|
||||||
|
// seu propi mapeig — resolem-lo a partir del joystick instance ID.
|
||||||
|
const int SERVICE_BTN = serviceButtonForEvent(ev.which);
|
||||||
|
if (SERVICE_BTN >= 0 && static_cast<int>(ev.button) == SERVICE_BTN) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (ev.button) {
|
||||||
|
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||||
|
moveCursor(-1);
|
||||||
|
break;
|
||||||
|
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||||
|
moveCursor(+1);
|
||||||
|
break;
|
||||||
|
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||||
|
activateCurrent(-1);
|
||||||
|
break;
|
||||||
|
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||||
|
case SDL_GAMEPAD_BUTTON_EAST:
|
||||||
|
case SDL_GAMEPAD_BUTTON_NORTH:
|
||||||
|
activateCurrent(+1);
|
||||||
|
break;
|
||||||
|
case SDL_GAMEPAD_BUTTON_WEST:
|
||||||
|
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||||
|
popPage();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::captureKey(SDL_Scancode sc) {
|
||||||
|
if (!capturing_) { return; }
|
||||||
|
// ESC cancel·la la captura sense assignar — replica AEE
|
||||||
|
// (`Menu::captureKey`). Qualsevol altra tecla queda assignada.
|
||||||
|
if (sc == SDL_SCANCODE_ESCAPE) {
|
||||||
|
capturing_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stack_.empty()) {
|
||||||
|
capturing_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& page = topConst();
|
||||||
|
if (page.cursor >= page.items.size()) {
|
||||||
|
capturing_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& item = page.items[page.cursor];
|
||||||
|
if (item.kind == Item::Kind::KEY_BIND && item.set_scancode) {
|
||||||
|
item.set_scancode(sc);
|
||||||
|
}
|
||||||
|
capturing_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Input ---
|
// --- Input ---
|
||||||
|
|
||||||
auto ServiceMenu::handleEvent(const SDL_Event& ev) -> bool {
|
auto ServiceMenu::handleEvent(const SDL_Event& ev) -> bool {
|
||||||
if (!open_) { return false; }
|
if (!open_) { return false; }
|
||||||
|
|
||||||
|
// Mode de captura de botons del mando: redirigeix tots els events
|
||||||
|
// del gamepad (BUTTON_DOWN/UP, AXIS_MOTION) al sub-mòdul. ESCAPE de
|
||||||
|
// teclat cancel·la la sessió i tanca el sub-overlay sense persistir.
|
||||||
|
if (define_buttons_.isEnabled()) {
|
||||||
|
if (ev.type == SDL_EVENT_KEY_DOWN && !ev.key.repeat &&
|
||||||
|
ev.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||||
|
define_buttons_.disable();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
define_buttons_.handleEvent(ev);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode de captura per a un KEY_BIND. Mentre dura, el menú consumeix
|
||||||
|
// TOTS els events: el primer KEY_DOWN s'assigna a l'item seleccionat
|
||||||
|
// (o ESC cancel·la), i la resta queden bloquejats perquè cap tecla
|
||||||
|
// s'escapi al joc. Replica el comportament d'AEE
|
||||||
|
// (`Menu::isCapturing` + `Menu::captureKey`).
|
||||||
|
if (capturing_) {
|
||||||
|
if (ev.type == SDL_EVENT_KEY_DOWN && !ev.key.repeat) {
|
||||||
|
captureKey(ev.key.scancode);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Botons del gamepad: cruceta per navegar, EAST/NORTH com a OK,
|
// Botons del gamepad: cruceta per navegar, EAST/NORTH com a OK,
|
||||||
// WEST/SOUTH com a BACK de pàgina, BACK tanca el menú sencer. Quan
|
// WEST/SOUTH com a BACK de pàgina, BACK tanca el menú sencer. Quan
|
||||||
// el menú està obert consumim TOTS els BUTTON_DOWN perquè cap botó
|
// el menú està obert consumim TOTS els BUTTON_DOWN perquè cap botó
|
||||||
// arribi als consumidors de gameplay. Paral·lel al consum total de
|
// arribi als consumidors de gameplay. Paral·lel al consum total de
|
||||||
// KEY_DOWN més avall.
|
// KEY_DOWN més avall.
|
||||||
if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||||
switch (ev.gbutton.button) {
|
handleGamepadButtonDown(ev.gbutton);
|
||||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
|
||||||
moveCursor(-1);
|
|
||||||
break;
|
|
||||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
|
||||||
moveCursor(+1);
|
|
||||||
break;
|
|
||||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
|
||||||
activateCurrent(-1);
|
|
||||||
break;
|
|
||||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
|
||||||
case SDL_GAMEPAD_BUTTON_EAST:
|
|
||||||
case SDL_GAMEPAD_BUTTON_NORTH:
|
|
||||||
activateCurrent(+1);
|
|
||||||
break;
|
|
||||||
case SDL_GAMEPAD_BUTTON_WEST:
|
|
||||||
case SDL_GAMEPAD_BUTTON_SOUTH:
|
|
||||||
popPage();
|
|
||||||
break;
|
|
||||||
case SDL_GAMEPAD_BUTTON_BACK:
|
|
||||||
close();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,37 +822,62 @@ auto ServiceMenu::buildControls() -> Page {
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto getKeyboardScancode(int player_id, Options::KeyboardAction action) -> SDL_Scancode {
|
||||||
|
const auto& kb = (player_id == 0) ? Options::controls().keyboard_p1
|
||||||
|
: Options::controls().keyboard_p2;
|
||||||
|
switch (action) {
|
||||||
|
case Options::KeyboardAction::UP:
|
||||||
|
return kb.up;
|
||||||
|
case Options::KeyboardAction::DOWN:
|
||||||
|
return kb.down;
|
||||||
|
case Options::KeyboardAction::LEFT:
|
||||||
|
return kb.left;
|
||||||
|
case Options::KeyboardAction::RIGHT:
|
||||||
|
return kb.right;
|
||||||
|
case Options::KeyboardAction::JUMP:
|
||||||
|
return kb.jump;
|
||||||
|
case Options::KeyboardAction::ATTACK:
|
||||||
|
return kb.attack;
|
||||||
|
}
|
||||||
|
return SDL_SCANCODE_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto ServiceMenu::makeKeyboardItem(const std::string& label_key, int player_id, std::uint8_t action) -> Item {
|
||||||
|
const auto KA = static_cast<Options::KeyboardAction>(action);
|
||||||
|
return makeKeyBind(
|
||||||
|
label_key,
|
||||||
|
[player_id, KA]() { return getKeyboardScancode(player_id, KA); },
|
||||||
|
[player_id, KA](SDL_Scancode sc) { Options::setKeyboardBinding(player_id, KA, sc); });
|
||||||
|
}
|
||||||
|
|
||||||
auto ServiceMenu::buildKeyboardControls() -> Page {
|
auto ServiceMenu::buildKeyboardControls() -> Page {
|
||||||
Page page;
|
Page page;
|
||||||
page.title_key = "menu.titles.keyboard";
|
page.title_key = "menu.titles.keyboard";
|
||||||
|
|
||||||
// Etiqueta divisòria per al jugador 1.
|
using KA = Options::KeyboardAction;
|
||||||
|
const auto AS_U8 = [](KA k) { return static_cast<std::uint8_t>(k); };
|
||||||
|
|
||||||
|
// Bloc del jugador 1. Ordre: moviment → atacar → saltar.
|
||||||
page.items.push_back(makeLabel(Locale::get().text("menu.labels.player_1")));
|
page.items.push_back(makeLabel(Locale::get().text("menu.labels.player_1")));
|
||||||
|
page.items.push_back(makeKeyboardItem("menu.items.move_up", 0, AS_U8(KA::UP)));
|
||||||
|
page.items.push_back(makeKeyboardItem("menu.items.move_down", 0, AS_U8(KA::DOWN)));
|
||||||
|
page.items.push_back(makeKeyboardItem("menu.items.move_left", 0, AS_U8(KA::LEFT)));
|
||||||
|
page.items.push_back(makeKeyboardItem("menu.items.move_right", 0, AS_U8(KA::RIGHT)));
|
||||||
|
page.items.push_back(makeKeyboardItem("menu.items.attack", 0, AS_U8(KA::ATTACK)));
|
||||||
|
page.items.push_back(makeKeyboardItem("menu.items.jump", 0, AS_U8(KA::JUMP)));
|
||||||
|
|
||||||
// Vinculacions actuals: les llegim directament de `Defaults::KeysGame`
|
// Bloc del jugador 2.
|
||||||
// (P1) i `Defaults::KeysGameP2` (P2). Encara no hi ha setter, per
|
|
||||||
// tant els items surten com a informatius (no seleccionables). La
|
|
||||||
// lògica de captura i persistència arribarà en una segona iteració
|
|
||||||
// amb `Options::keysGameP1()` / `keysGameP2()`.
|
|
||||||
page.items.push_back(makeKeyBind("menu.items.move_up",
|
|
||||||
[]() { return Defaults::KeysGame::UP; }));
|
|
||||||
page.items.push_back(makeKeyBind("menu.items.move_down",
|
|
||||||
[]() { return Defaults::KeysGame::DOWN; }));
|
|
||||||
page.items.push_back(makeKeyBind("menu.items.move_left",
|
|
||||||
[]() { return Defaults::KeysGame::LEFT; }));
|
|
||||||
page.items.push_back(makeKeyBind("menu.items.move_right",
|
|
||||||
[]() { return Defaults::KeysGame::RIGHT; }));
|
|
||||||
|
|
||||||
// Bloc del jugador 2: mateixa forma, llegint de KeysGameP2.
|
|
||||||
page.items.push_back(makeLabel(Locale::get().text("menu.labels.player_2")));
|
page.items.push_back(makeLabel(Locale::get().text("menu.labels.player_2")));
|
||||||
page.items.push_back(makeKeyBind("menu.items.move_up",
|
page.items.push_back(makeKeyboardItem("menu.items.move_up", 1, AS_U8(KA::UP)));
|
||||||
[]() { return Defaults::KeysGameP2::UP; }));
|
page.items.push_back(makeKeyboardItem("menu.items.move_down", 1, AS_U8(KA::DOWN)));
|
||||||
page.items.push_back(makeKeyBind("menu.items.move_down",
|
page.items.push_back(makeKeyboardItem("menu.items.move_left", 1, AS_U8(KA::LEFT)));
|
||||||
[]() { return Defaults::KeysGameP2::DOWN; }));
|
page.items.push_back(makeKeyboardItem("menu.items.move_right", 1, AS_U8(KA::RIGHT)));
|
||||||
page.items.push_back(makeKeyBind("menu.items.move_left",
|
page.items.push_back(makeKeyboardItem("menu.items.attack", 1, AS_U8(KA::ATTACK)));
|
||||||
[]() { return Defaults::KeysGameP2::LEFT; }));
|
page.items.push_back(makeKeyboardItem("menu.items.jump", 1, AS_U8(KA::JUMP)));
|
||||||
page.items.push_back(makeKeyBind("menu.items.move_right",
|
|
||||||
[]() { return Defaults::KeysGameP2::RIGHT; }));
|
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
@@ -874,6 +989,37 @@ namespace {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Acció: arranca DefineButtons per al mando assignat al jugador.
|
||||||
|
// Si no hi ha mando, no fa res — l'item ja s'amaga via `visible`.
|
||||||
|
void startDefineButtonsForPlayer(int player_id) {
|
||||||
|
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(player_id);
|
||||||
|
if (pad == nullptr) { return; }
|
||||||
|
const auto& slot = (player_id == 0)
|
||||||
|
? Options::controls().gamepad_p1
|
||||||
|
: Options::controls().gamepad_p2;
|
||||||
|
// Anem a buscar el nom/path actuals del mando físic, no els
|
||||||
|
// persistits — així podem captar correctament el mateix mando
|
||||||
|
// si l'usuari l'ha mogut de port USB i només en queda el nom.
|
||||||
|
std::string name = slot.name;
|
||||||
|
std::string path = slot.path;
|
||||||
|
const auto ALL = GamepadManager::get().getAll();
|
||||||
|
const auto IT = std::ranges::find_if(ALL,
|
||||||
|
[pad](const auto& info) { return info.pad == pad; });
|
||||||
|
if (IT != ALL.end()) {
|
||||||
|
name = IT->name;
|
||||||
|
path = IT->path;
|
||||||
|
}
|
||||||
|
ServiceMenu::get().startDefineButtons(pad, name, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto playerHasGamepad(int player_id) -> bool {
|
||||||
|
return GamepadManager::get().getForPlayer(player_id) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
auto ServiceMenu::buildGamepadControls() -> Page {
|
auto ServiceMenu::buildGamepadControls() -> Page {
|
||||||
Page page;
|
Page page;
|
||||||
page.title_key = "menu.titles.gamepad";
|
page.title_key = "menu.titles.gamepad";
|
||||||
@@ -896,9 +1042,29 @@ auto ServiceMenu::buildGamepadControls() -> Page {
|
|||||||
[](std::size_t idx) { applyPadSelection(1, idx); },
|
[](std::size_t idx) { applyPadSelection(1, idx); },
|
||||||
[](std::size_t idx) { return padLabel(1, idx); }));
|
[](std::size_t idx) { return padLabel(1, idx); }));
|
||||||
|
|
||||||
|
// Accions per a llançar `DefineButtons` per a cada slot assignat.
|
||||||
|
// Només visibles si el jugador té un mando vinculat — equivalent al
|
||||||
|
// `define_buttons_->enable(...)` que dispara l'acció a coffee_crisis.
|
||||||
|
Item define_p1 = makeAction("menu.items.define_buttons_p1",
|
||||||
|
[]() { startDefineButtonsForPlayer(0); });
|
||||||
|
define_p1.visible = []() { return playerHasGamepad(0); };
|
||||||
|
page.items.push_back(std::move(define_p1));
|
||||||
|
|
||||||
|
Item define_p2 = makeAction("menu.items.define_buttons_p2",
|
||||||
|
[]() { startDefineButtonsForPlayer(1); });
|
||||||
|
define_p2.visible = []() { return playerHasGamepad(1); };
|
||||||
|
page.items.push_back(std::move(define_p2));
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::startDefineButtons(SDL_Gamepad* pad, const std::string& name, const std::string& path) {
|
||||||
|
// Wrapper públic perquè els items del menú (factories estàtiques)
|
||||||
|
// puguin atacar el sub-mòdul sense exposar-lo. Evita haver de fer
|
||||||
|
// `define_buttons_` `public`.
|
||||||
|
define_buttons_.enable(pad, name, path);
|
||||||
|
}
|
||||||
|
|
||||||
auto ServiceMenu::buildSystem() -> Page {
|
auto ServiceMenu::buildSystem() -> Page {
|
||||||
Page page;
|
Page page;
|
||||||
page.title_key = "menu.titles.system";
|
page.title_key = "menu.titles.system";
|
||||||
@@ -1038,10 +1204,13 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
|
|||||||
first_item = false;
|
first_item = false;
|
||||||
|
|
||||||
const bool SELECTED = item.selectable && (i == page.cursor);
|
const bool SELECTED = item.selectable && (i == page.cursor);
|
||||||
|
const bool CAPTURING_THIS = capturing_ && SELECTED && (item.kind == Item::Kind::KEY_BIND);
|
||||||
const std::string LABEL_TEXT = (item.kind == Item::Kind::LABEL)
|
const std::string LABEL_TEXT = (item.kind == Item::Kind::LABEL)
|
||||||
? item.label_key
|
? item.label_key
|
||||||
: Locale::get().text(item.label_key);
|
: Locale::get().text(item.label_key);
|
||||||
const std::string VALUE = formatValue(item);
|
const std::string VALUE = CAPTURING_THIS
|
||||||
|
? Locale::get().text("menu.values.press_key")
|
||||||
|
: formatValue(item);
|
||||||
|
|
||||||
Uint8 label_color = UiConfig::get().menuLabel();
|
Uint8 label_color = UiConfig::get().menuLabel();
|
||||||
if (item.kind == Item::Kind::LABEL) {
|
if (item.kind == Item::Kind::LABEL) {
|
||||||
@@ -1118,4 +1287,8 @@ void ServiceMenu::render() {
|
|||||||
});
|
});
|
||||||
renderPage(page, BOX_X, BOX_Y, BOX_WIDTH);
|
renderPage(page, BOX_X, BOX_Y, BOX_WIDTH);
|
||||||
Rendering::popClip();
|
Rendering::popClip();
|
||||||
|
|
||||||
|
// Overlay de captura de botons al damunt del menú. Es pinta sense
|
||||||
|
// clip perquè ocupa la seua pròpia caixa centrada.
|
||||||
|
define_buttons_.render();
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,13 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/input/define_buttons.hpp"
|
||||||
#include "core/rendering/surface.hpp" // Palette
|
#include "core/rendering/surface.hpp" // Palette
|
||||||
|
|
||||||
class Text;
|
class Text;
|
||||||
@@ -56,6 +58,21 @@ class ServiceMenu {
|
|||||||
// false; és el caller qui decideix obrir-lo amb la hotkey.
|
// false; és el caller qui decideix obrir-lo amb la hotkey.
|
||||||
auto handleEvent(const SDL_Event& ev) -> bool;
|
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);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// --- Struct d'un item d'una pàgina ---
|
// --- Struct d'un item d'una pàgina ---
|
||||||
//
|
//
|
||||||
@@ -185,6 +202,11 @@ class ServiceMenu {
|
|||||||
// --- Navegació i estat ---
|
// --- Navegació i estat ---
|
||||||
void pushPage(Page page);
|
void pushPage(Page page);
|
||||||
void popPage();
|
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);
|
||||||
[[nodiscard]] auto top() -> Page&;
|
[[nodiscard]] auto top() -> Page&;
|
||||||
[[nodiscard]] auto topConst() const -> const Page&;
|
[[nodiscard]] auto topConst() const -> const Page&;
|
||||||
[[nodiscard]] static auto isItemVisible(const Item& item) -> bool;
|
[[nodiscard]] static auto isItemVisible(const Item& item) -> bool;
|
||||||
@@ -211,6 +233,14 @@ class ServiceMenu {
|
|||||||
[[nodiscard]] static auto makeShaderTypeItem() -> Item;
|
[[nodiscard]] static auto makeShaderTypeItem() -> Item;
|
||||||
[[nodiscard]] static auto makeShaderPresetItem() -> 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 ---
|
// --- Render ---
|
||||||
// "Visible" inclou l'animació de tancament: retorna true mentre la caixa
|
// "Visible" inclou l'animació de tancament: retorna true mentre la caixa
|
||||||
// encara ha de dibuixar-se, encara que open_ siga false.
|
// encara ha de dibuixar-se, encara que open_ siga false.
|
||||||
@@ -232,4 +262,11 @@ class ServiceMenu {
|
|||||||
float animated_h_{0.0F}; // Alçada suavitzada cap a `targetBoxHeight`.
|
float animated_h_{0.0F}; // Alçada suavitzada cap a `targetBoxHeight`.
|
||||||
bool transition_active_{false}; // Push/pop de pàgina en curs.
|
bool transition_active_{false}; // Push/pop de pàgina en curs.
|
||||||
float transition_progress_{0.0F}; // 0 → 1 durant la transició de pàgina.
|
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.
|
||||||
|
|
||||||
|
// 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_;
|
||||||
};
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.29.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.30.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
Reference in new issue
Block a user