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

+33 -5
View File
@@ -9,6 +9,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/rendering/screen.hpp"
@@ -16,6 +17,7 @@
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "game/entities/event_bus.hpp"
#include "game/options.hpp"
#include "utils/defaults.hpp"
namespace {
@@ -578,6 +580,12 @@ void Player::tick(float delta_time) {
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
// 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
@@ -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 {
// Edge-detection independent per teclat i mando: si qualsevol dels
// 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;
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; }
jump_held_gamepad_ = PAD_NOW;
@@ -672,16 +696,20 @@ auto Player::consumeJumpEdge() -> bool {
}
auto Player::consumeAttackEdge() -> bool {
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. Mando:
// SDL_GAMEPAD_BUTTON_WEST (X a Xbox / Quadrat a PlayStation), oposat
// a SOUTH per evitar topades semàntiques.
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. El botó
// físic ve d'`GamepadBindings`, persistit per dispositiu — l'usuari
// l'ha pogut remapejar des del menú de servei.
const bool* keys = InputState::getKeyboardState();
const bool KEY_NOW = keys[controls_.attack];
bool edge = (KEY_NOW && !attack_held_keyboard_);
attack_held_keyboard_ = KEY_NOW;
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; }
attack_held_gamepad_ = PAD_NOW;
+6
View File
@@ -407,6 +407,12 @@ class Player : public AnimatedSprite {
float (Player::*get_pos)() const,
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};
Controls controls_;
+68
View File
@@ -169,11 +169,51 @@ namespace {
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) {
if (!yaml.contains("controls")) { return; }
const auto& node = yaml["controls"];
parseGamepadAssignment(node, "gamepad_p1", g_controls.gamepad_p1);
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
@@ -561,6 +601,32 @@ namespace Options {
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) {
g_config_file_path = path;
}
@@ -666,6 +732,8 @@ namespace Options {
out << " gamepad_p2:\n";
out << " name: \"" << g_controls.gamepad_p2.name << "\"\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());
}
+49
View File
@@ -1,5 +1,8 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <vector>
@@ -114,9 +117,49 @@ namespace Options {
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 {
GamepadAssignment gamepad_p1{};
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) ---
@@ -153,6 +196,12 @@ namespace Options {
// = desassignació. `player_id`: 0 = P1, 1 = P2.
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 ---
void setConfigFile(const std::string& path);
auto loadFromFile() -> bool;
+18 -15
View File
@@ -334,24 +334,27 @@ GameScene::GameScene(Config config)
if (p != nullptr) { spawnWhip(*p); }
});
// Controls per jugador: P1 amb fletxes, P2 amb WASD. Cada Player
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
// estat global d'input.
// Controls per jugador: P1 amb fletxes, P2 amb WASD per defecte. Les
// vinculacions actives venen d'`Options::controls()`, que s'inicialitza
// 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{
.up = Defaults::KeysGame::UP,
.down = Defaults::KeysGame::DOWN,
.left = Defaults::KeysGame::LEFT,
.right = Defaults::KeysGame::RIGHT,
.jump = Defaults::KeysGame::JUMP,
.attack = Defaults::KeysGame::ATTACK,
.up = kb_p1.up,
.down = kb_p1.down,
.left = kb_p1.left,
.right = kb_p1.right,
.jump = kb_p1.jump,
.attack = kb_p1.attack,
};
const Player::Controls CONTROLS_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,
.up = kb_p2.up,
.down = kb_p2.down,
.left = kb_p2.left,
.right = kb_p2.right,
.jump = kb_p2.jump,
.attack = kb_p2.attack,
};
// Spawn lligat a la porta: tots els jugadors apareixen davant del
// passadís de sortida. `computeSpawn` deriva les coordenades de
+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();
}
+37
View File
@@ -3,11 +3,13 @@
#include <SDL3/SDL.h>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "core/input/define_buttons.hpp"
#include "core/rendering/surface.hpp" // Palette
class Text;
@@ -56,6 +58,21 @@ class ServiceMenu {
// false; és el caller qui decideix obrir-lo amb la hotkey.
auto handleEvent(const SDL_Event& ev) -> bool;
// True mentre estem esperant que l'usuari pulse una tecla per assignar
// a l'item KEY_BIND seleccionat. El Director redirigeix els KEY_DOWN
// a `captureKey` mentre dura — replica `Menu::isCapturing` d'AEE.
[[nodiscard]] auto isCapturing() const -> bool { return capturing_; }
// Aplica l'scancode capturat a l'item seleccionat (si encara és KEY_BIND
// i té setter). ESCAPE cancel·la sense assignar.
void captureKey(SDL_Scancode sc);
// Arranca el sub-mòdul `DefineButtons` per al mando indicat. El menú
// queda "obert" però els events del mando es redirigeixen al sub-mòdul
// fins que acabi la seqüència o l'usuari pulse ESC. Replica el patró
// `define_buttons_->enable(...)` de coffee_crisis_arcade_edition.
void startDefineButtons(SDL_Gamepad* pad, const std::string& name, const std::string& path);
private:
// --- Struct d'un item d'una pàgina ---
//
@@ -185,6 +202,11 @@ class ServiceMenu {
// --- Navegació i estat ---
void pushPage(Page page);
void popPage();
// Tractament del bloc d'events de gamepad (BUTTON_DOWN). Extret a
// mètode propi per a mantenir la complexitat cognitiva d'`handleEvent`
// sota el llindar de clang-tidy.
void handleGamepadButtonDown(const SDL_GamepadButtonEvent& ev);
[[nodiscard]] auto top() -> Page&;
[[nodiscard]] auto topConst() const -> const Page&;
[[nodiscard]] static auto isItemVisible(const Item& item) -> bool;
@@ -211,6 +233,14 @@ class ServiceMenu {
[[nodiscard]] static auto makeShaderTypeItem() -> Item;
[[nodiscard]] static auto makeShaderPresetItem() -> Item;
// Fàbrica genèrica per a un KEY_BIND vinculat a una acció del jugador
// `player_id`. Llegeix d'`Options::controls().keyboard_p*` i delega
// la mutació a `Options::setKeyboardBinding(...)` (persisteix).
[[nodiscard]] static auto makeKeyboardItem(
const std::string& label_key,
int player_id,
std::uint8_t action) -> Item;
// --- Render ---
// "Visible" inclou l'animació de tancament: retorna true mentre la caixa
// encara ha de dibuixar-se, encara que open_ siga false.
@@ -232,4 +262,11 @@ class ServiceMenu {
float animated_h_{0.0F}; // Alçada suavitzada cap a `targetBoxHeight`.
bool transition_active_{false}; // Push/pop de pàgina en curs.
float transition_progress_{0.0F}; // 0 → 1 durant la transició de pàgina.
bool capturing_{false}; // True mentre esperem una tecla per a un KEY_BIND.
// 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_;
};