feat(controls): redefinició de tecles i botons del mando des del menú de servei

This commit is contained in:
JailDesigner committed 2026-04-29 19:54:47 +02:00
1 parent 4739354875
commit 37fdcb4751
15 files changed
+1169 -80

No files matched your search

+227 -54
View File
@@ -7,6 +7,7 @@
#include <utility>
#include "core/audio/audio.hpp"
#include "core/input/gamepad_bindings.hpp"
#include "core/input/gamepad_manager.hpp"
#include "core/input/input_state.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);
items_font_ = std::move(items_font);
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() {
@@ -253,6 +257,7 @@ void ServiceMenu::close() {
if (!open_) { return; }
open_ = false;
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
// visual i no ha d'impedir que el jugador es mogui un cop ha tancat.
InputState::setBlocked(false);
@@ -266,6 +271,16 @@ auto ServiceMenu::isVisible() const -> bool {
}
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;
if (closing_) {
@@ -419,50 +434,125 @@ void ServiceMenu::activateCurrent(int direction) {
if (direction > 0 && item.action) { item.action(); }
break;
case Item::Kind::KEY_BIND:
// TODO: entrar en mode captura quan hi hagi `set_scancode`.
// De moment no fem res — els items sense setter ja no són
// seleccionables (veure `makeKeyBind`), per tant aquest case
// només es trepitja si algú en fa un amb setter en el futur.
// Entra en mode captura: el Director redirigirà el pròxim
// KEY_DOWN cap a `captureKey()`. Només té sentit amb RETURN /
// ENTER / RIGHT (direction +1) — LEFT no fa res, com a la
// resta de Kinds que requereixen confirmació explícita.
if (direction > 0 && item.set_scancode) { capturing_ = true; }
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 ---
auto ServiceMenu::handleEvent(const SDL_Event& ev) -> bool {
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,
// 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ó
// arribi als consumidors de gameplay. Paral·lel al consum total de
// KEY_DOWN més avall.
if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
switch (ev.gbutton.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;
case SDL_GAMEPAD_BUTTON_BACK:
close();
break;
default:
break;
}
handleGamepadButtonDown(ev.gbutton);
return true;
}
@@ -732,37 +822,62 @@ auto ServiceMenu::buildControls() -> 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 {
Page page;
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(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`
// (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.
// Bloc del jugador 2.
page.items.push_back(makeLabel(Locale::get().text("menu.labels.player_2")));
page.items.push_back(makeKeyBind("menu.items.move_up",
[]() { return Defaults::KeysGameP2::UP; }));
page.items.push_back(makeKeyBind("menu.items.move_down",
[]() { return Defaults::KeysGameP2::DOWN; }));
page.items.push_back(makeKeyBind("menu.items.move_left",
[]() { return Defaults::KeysGameP2::LEFT; }));
page.items.push_back(makeKeyBind("menu.items.move_right",
[]() { return Defaults::KeysGameP2::RIGHT; }));
page.items.push_back(makeKeyboardItem("menu.items.move_up", 1, AS_U8(KA::UP)));
page.items.push_back(makeKeyboardItem("menu.items.move_down", 1, AS_U8(KA::DOWN)));
page.items.push_back(makeKeyboardItem("menu.items.move_left", 1, AS_U8(KA::LEFT)));
page.items.push_back(makeKeyboardItem("menu.items.move_right", 1, AS_U8(KA::RIGHT)));
page.items.push_back(makeKeyboardItem("menu.items.attack", 1, AS_U8(KA::ATTACK)));
page.items.push_back(makeKeyboardItem("menu.items.jump", 1, AS_U8(KA::JUMP)));
return page;
}
@@ -874,6 +989,37 @@ 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 {
Page page;
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) { 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;
}
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 {
Page page;
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;
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)
? 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();
if (item.kind == Item::Kind::LABEL) {
@@ -1118,4 +1287,8 @@ void ServiceMenu::render() {
});
renderPage(page, BOX_X, BOX_Y, BOX_WIDTH);
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();
}