feat(controls): redefinició de tecles i botons del mando des del menú de servei
This commit is contained in:
1 parent
4739354875
commit
37fdcb4751
15 files changed
+1169
-80
No files matched your search
+227
-54
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user