feat(service_menu): pagina CONTROLS amb assignacio de pad i rebind per jugador

Afegeix submenu CONTROLS al menu de servei amb 2 items CYCLE per
seleccionar el mando assignat a cada jugador (persistit per name + path)
i 4 items ACTION per arrancar DefineInputs (teclat/mando per a P1/P2).

Tambe afegeix:
- Director: init/update/draw/destroy del singleton DefineInputs.
- GlobalEvents: routing prioritari de tots els events a DefineInputs
  mentre l'overlay esta actiu.
- Locale ca/en: claus del submenu CONTROLS i de l'overlay de rebind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 20:18:49 +02:00
parent fcf13591be
commit 34be79192c
6 changed files with 220 additions and 0 deletions
+9
View File
@@ -13,6 +13,7 @@
#include "core/audio/audio_adapter.hpp"
#include "core/defaults/window.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
@@ -178,6 +179,7 @@ Director::Director(int argc, char* argv[])
System::Notifier::init(sdl_->getRenderer());
System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get());
System::DefineInputs::init(sdl_->getRenderer());
last_ticks_ms_ = SDL_GetTicks();
}
@@ -192,6 +194,7 @@ Director::~Director() {
// l'hem de cridar nosaltres.
current_scene_.reset();
debug_overlay_.reset();
System::DefineInputs::destroy();
System::ServiceMenu::destroy();
System::Notifier::destroy();
context_.reset();
@@ -376,6 +379,9 @@ auto Director::iterate() -> SDL_AppResult {
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
menu->update(delta_time);
}
if (auto* di = System::DefineInputs::get(); di != nullptr) {
di->update(delta_time);
}
Audio::update();
// Si la swapchain no està disponible (finestra minimitzada, etc.),
@@ -392,6 +398,9 @@ auto Director::iterate() -> SDL_AppResult {
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr) {
menu->draw(); // service menu: per damunt fins i tot dels toasts
}
if (const auto* di = System::DefineInputs::get(); di != nullptr) {
di->draw(); // overlay de rebind: per damunt de tot
}
sdl_->present();
return SDL_APP_CONTINUE;
}
+17
View File
@@ -5,6 +5,7 @@
#include <iostream>
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
@@ -43,6 +44,16 @@ namespace GlobalEvents {
return true;
}
// Si l'overlay de redefinicio esta actiu, engoleix tots els events.
auto consumeIfDefineActive(const SDL_Event& event) -> bool {
auto* di = System::DefineInputs::get();
if (di == nullptr || !di->isActive()) {
return false;
}
di->handleEvent(event);
return true;
}
// Botó MENU al mando d'algun jugador → alterna el menú de servei
// (mateix comportament que F12 al teclat). Retorna true si l'event és
// un GAMEPAD_BUTTON_DOWN consumit.
@@ -83,6 +94,12 @@ namespace GlobalEvents {
std::cout << "[Input] " << event_msg << '\n';
}
// 1b. Si l'overlay de redefinicio esta actiu, engoleix tots els events
// (cap arriba al joc, al menu de servei ni als hotkeys F1-F12).
if (consumeIfDefineActive(event)) {
return true;
}
// 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
if (event.type == SDL_EVENT_QUIT) {
context.setNextScene(SceneType::EXIT);
+139
View File
@@ -15,9 +15,12 @@
#include "core/defaults/audio.hpp"
#include "core/defaults/rendering.hpp"
#include "core/defaults/service_menu.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/debug_overlay.hpp"
#include "core/system/notifier.hpp"
#include "core/system/relaunch.hpp"
#include "core/types.hpp"
#include "game/config_yaml.hpp"
@@ -77,6 +80,84 @@ namespace {
return Locale::get().text(item.label_key);
}
// ---- Helpers de la pagina CONTROLS ----
auto padDisplayName(int player_index) -> std::string {
const auto* input = Input::get();
if (input == nullptr) {
return Locale::get().text("service_menu.controls_no_pad");
}
auto pad = input->getPlayerGamepad(player_index);
if (!pad) {
return Locale::get().text("service_menu.controls_no_pad");
}
return pad->name;
}
// Index actual del pad assignat dins de la llista de mandos detectats.
// Prioritat path > name. Si no n'hi ha cap match, retorna 0.
auto findAssignedIndex(const std::vector<std::shared_ptr<Input::Gamepad>>& pads,
const Config::PlayerBindings& pcfg) -> std::size_t {
for (std::size_t i = 0; i < pads.size(); ++i) {
if (pads[i] && !pcfg.gamepad_path.empty() && pads[i]->path == pcfg.gamepad_path) {
return i;
}
}
for (std::size_t i = 0; i < pads.size(); ++i) {
if (pads[i] && !pcfg.gamepad_name.empty() && pads[i]->name == pcfg.gamepad_name) {
return i;
}
}
return 0;
}
// Avança ciclicament l'assignacio de pad d'un jugador i la persisteix.
void cyclePlayerPad(int player_index, int dir) {
auto* input = Input::get();
if (input == nullptr) {
return;
}
const auto& pads = input->getGamepads();
if (pads.empty()) {
return;
}
auto& pcfg = (player_index == 0)
? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2;
const std::size_t CURRENT = findAssignedIndex(pads, pcfg);
const std::size_t N = pads.size();
const std::size_t STEP = (dir > 0) ? 1 : (N - 1);
const std::size_t NEXT = (CURRENT + STEP) % N;
if (!pads[NEXT]) {
return;
}
pcfg.gamepad_name = pads[NEXT]->name;
pcfg.gamepad_path = pads[NEXT]->path;
if (player_index == 0) {
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
} else {
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
}
ConfigYaml::saveToFile();
}
// Arranca un rebind o avisa si el jugador no te pad. Retorna true si el
// rebind ha començat (el caller ha de tancar el menu).
auto startDefine(System::DefineInputs::Mode mode, System::DefineInputs::Player pl) -> bool {
auto* di = System::DefineInputs::get();
if (di == nullptr) {
return false;
}
if (!di->begin(mode, pl)) {
if (auto* n = System::Notifier::get(); n != nullptr) {
n->notifyWarn(Locale::get().text("define.no_gamepad"));
}
return false;
}
return true;
}
} // namespace
namespace System {
@@ -153,6 +234,7 @@ namespace System {
makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }),
makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }),
makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }),
makeSubmenu("service_menu.controls", [this] { pushPage(buildControlsPage()); }),
makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }),
};
stack_.clear();
@@ -437,6 +519,63 @@ namespace System {
return page;
}
namespace {
auto makeCyclePadItem(const char* label_key, int player_index) -> ServiceMenu::Item {
return ServiceMenu::Item{
.kind = ServiceMenu::Kind::CYCLE,
.label_key = label_key,
.label_text = {},
.selectable = true,
.on_activate = {},
.get_value_text = [player_index] { return padDisplayName(player_index); },
.on_change = [player_index](int dir) { cyclePlayerPad(player_index, dir); },
};
}
auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl, ServiceMenu* menu) -> ServiceMenu::Item {
return ServiceMenu::Item{
.kind = ServiceMenu::Kind::ACTION,
.label_key = label_key,
.label_text = {},
.selectable = true,
.on_activate = [mode, pl, menu] {
if (startDefine(mode, pl) && menu != nullptr && menu->isOpen()) {
menu->toggle();
} },
.get_value_text = {},
.on_change = {},
};
}
} // namespace
auto ServiceMenu::buildControlsPage() -> Page {
Page page;
page.title_key = "service_menu.controls";
page.items = {
makeCyclePadItem("service_menu.controls_pad_p1", 0),
makeCyclePadItem("service_menu.controls_pad_p2", 1),
makeDefineItem("service_menu.controls_define_keyboard_p1",
DefineInputs::Mode::KEYBOARD,
DefineInputs::Player::P1,
this),
makeDefineItem("service_menu.controls_define_keyboard_p2",
DefineInputs::Mode::KEYBOARD,
DefineInputs::Player::P2,
this),
makeDefineItem("service_menu.controls_define_gamepad_p1",
DefineInputs::Mode::GAMEPAD,
DefineInputs::Player::P1,
this),
makeDefineItem("service_menu.controls_define_gamepad_p2",
DefineInputs::Mode::GAMEPAD,
DefineInputs::Player::P2,
this),
};
return page;
}
auto ServiceMenu::buildSystemPage() -> Page {
Page page;
page.title_key = "service_menu.system";
+1
View File
@@ -98,6 +98,7 @@ namespace System {
[[nodiscard]] static auto buildAudioPage() -> Page;
[[nodiscard]] auto buildOptionsPage() const -> Page;
[[nodiscard]] auto buildSystemPage() -> Page;
[[nodiscard]] auto buildControlsPage() -> Page;
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
// l'usuari selecciona SI; el cursor per defecte apunta a NO.
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);