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
@@ -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;
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
Reference in new issue
Block a user