Merge branch 'feature/sistema-gestio-inputs': sistema de gestio d'inputs
Modul DefineInputs per redefinir teclat i mando amb overlay modal, pagina CONTROLS al menu de servei (picker de mando amb llista, swap automatic en conflicte, slot SENSE MANDO, rebind per jugador), so accept en cada captura, navegacio del menu amb dpad/stick i triggers L2/R2, glyphs ( ) i / al charset, autoassignacio de mando al primer arranque, i diversos fixes de pipeline d'events.
This commit is contained in:
@@ -14,6 +14,8 @@ notification:
|
||||
postfx_on: "POSTPROCESSAT ACTIU"
|
||||
postfx_off: "POSTPROCESSAT INACTIU"
|
||||
locale_switched: "IDIOMA: {lang}"
|
||||
gamepad_connected: "{name} CONNECTAT"
|
||||
gamepad_disconnected: "{name} DESCONNECTAT"
|
||||
|
||||
language:
|
||||
ca: "CATALA"
|
||||
@@ -82,3 +84,29 @@ service_menu:
|
||||
# Valors comuns
|
||||
value_on: "ACTIU"
|
||||
value_off: "INACTIU"
|
||||
# Items del submenu CONTROLS
|
||||
controls_pad_p1: "MANDO JUGADOR 1"
|
||||
controls_pad_p2: "MANDO JUGADOR 2"
|
||||
controls_no_pad: "SENSE MANDO"
|
||||
controls_define_keyboard_p1: "REDEFINIR TECLES P1"
|
||||
controls_define_keyboard_p2: "REDEFINIR TECLES P2"
|
||||
controls_define_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||
controls_define_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||
|
||||
# Overlay modal de redefinicio (DefineInputs)
|
||||
define:
|
||||
title_keyboard_p1: "REDEFINIR TECLES P1"
|
||||
title_keyboard_p2: "REDEFINIR TECLES P2"
|
||||
title_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||
title_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||
press_key: "PREMEU UNA TECLA"
|
||||
press_button: "PREMEU UN BOTO"
|
||||
complete: "CONFIGURACIO COMPLETA"
|
||||
no_gamepad: "CAP MANDO ASSIGNAT AL JUGADOR"
|
||||
action:
|
||||
left: "ESQUERRA"
|
||||
right: "DRETA"
|
||||
fire: "DISPARAR"
|
||||
accelerate: "ACCELERAR"
|
||||
start: "START"
|
||||
menu: "MENU"
|
||||
|
||||
@@ -13,6 +13,8 @@ notification:
|
||||
postfx_on: "POSTPROCESS ON"
|
||||
postfx_off: "POSTPROCESS OFF"
|
||||
locale_switched: "LANGUAGE: {lang}"
|
||||
gamepad_connected: "{name} CONNECTED"
|
||||
gamepad_disconnected: "{name} DISCONNECTED"
|
||||
|
||||
language:
|
||||
ca: "CATALAN"
|
||||
@@ -81,3 +83,29 @@ service_menu:
|
||||
# Common values
|
||||
value_on: "ON"
|
||||
value_off: "OFF"
|
||||
# Items of CONTROLS submenu
|
||||
controls_pad_p1: "PLAYER 1 GAMEPAD"
|
||||
controls_pad_p2: "PLAYER 2 GAMEPAD"
|
||||
controls_no_pad: "NO GAMEPAD"
|
||||
controls_define_keyboard_p1: "REDEFINE KEYS P1"
|
||||
controls_define_keyboard_p2: "REDEFINE KEYS P2"
|
||||
controls_define_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||
controls_define_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||
|
||||
# Modal overlay for input redefinition (DefineInputs)
|
||||
define:
|
||||
title_keyboard_p1: "REDEFINE KEYS P1"
|
||||
title_keyboard_p2: "REDEFINE KEYS P2"
|
||||
title_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||
title_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||
press_key: "PRESS A KEY"
|
||||
press_button: "PRESS A BUTTON"
|
||||
complete: "CONFIGURATION COMPLETE"
|
||||
no_gamepad: "NO GAMEPAD ASSIGNED TO PLAYER"
|
||||
action:
|
||||
left: "LEFT"
|
||||
right: "RIGHT"
|
||||
fire: "FIRE"
|
||||
accelerate: "ACCELERATE"
|
||||
start: "START"
|
||||
menu: "MENU"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_lparen.shp - Símbol ( (parèntesi esquerre)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_lparen
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Arc cap a l'esquerra aproximat amb 4 trams rectes
|
||||
polyline: 14,4 8,12 6,20 8,28 14,36
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_rparen.shp - Símbol ) (parèntesi dret)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_rparen
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Arc cap a la dreta aproximat amb 4 trams rectes
|
||||
polyline: 6,4 12,12 14,20 12,28 6,36
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_slash.shp - Símbol / (barra)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_slash
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Línia diagonal de baix-esquerra a dalt-dreta
|
||||
line: 4,36 16,4
|
||||
@@ -48,12 +48,15 @@ namespace Config {
|
||||
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
|
||||
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
|
||||
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
|
||||
int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button
|
||||
int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei
|
||||
};
|
||||
|
||||
struct PlayerBindings {
|
||||
KeyboardBindings keyboard{};
|
||||
GamepadBindings gamepad{};
|
||||
std::string gamepad_name; // Empty = auto-assign by index
|
||||
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
|
||||
};
|
||||
|
||||
struct AudioConfig {
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
// Cargar símbolos
|
||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
|
||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"};
|
||||
for (const auto& sym : SYMBOLS) {
|
||||
char c = sym[0];
|
||||
std::string filename = getShapeFilename(c);
|
||||
@@ -164,6 +164,12 @@ namespace Graphics {
|
||||
return "font/char_exclamation.shp";
|
||||
case '?':
|
||||
return "font/char_question.shp";
|
||||
case '/':
|
||||
return "font/char_slash.shp";
|
||||
case '(':
|
||||
return "font/char_lparen.shp";
|
||||
case ')':
|
||||
return "font/char_rparen.shp";
|
||||
case ' ':
|
||||
return ""; // Espai es maneja sin load shape
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Graphics {
|
||||
|
||||
// Renderizar string completo
|
||||
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
|
||||
// '!', '?', ' ')
|
||||
// '!', '?', '/', '(', ')', ' ')
|
||||
// - position: posición inicial (esquina superior izquierda)
|
||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/input/define_inputs.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/defaults/service_menu.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/config_yaml.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float CANVAS_W = 1280.0F;
|
||||
constexpr float CANVAS_H = 720.0F;
|
||||
|
||||
// Llindar de trigger per a edge-detect L2/R2 com a boto virtual.
|
||||
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||
|
||||
// Codis virtuals per als triggers (consistents amb input_types.cpp).
|
||||
constexpr int TRIGGER_L2_VIRTUAL = 100;
|
||||
constexpr int TRIGGER_R2_VIRTUAL = 101;
|
||||
|
||||
// Durada del missatge de confirmacio abans de tancar-se.
|
||||
constexpr float COMPLETE_DISPLAY_S = 1.5F;
|
||||
|
||||
// Llindar dpad als axis sticks: no es captura per evitar conflicte amb el
|
||||
// moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando).
|
||||
constexpr Sint16 STICK_THRESHOLD = 16384;
|
||||
|
||||
// Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]).
|
||||
void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
|
||||
renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
|
||||
}
|
||||
|
||||
auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string {
|
||||
const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD);
|
||||
const bool IS_P1 = (player == System::DefineInputs::Player::P1);
|
||||
if (IS_KB && IS_P1) {
|
||||
return "define.title_keyboard_p1";
|
||||
}
|
||||
if (IS_KB) {
|
||||
return "define.title_keyboard_p2";
|
||||
}
|
||||
if (IS_P1) {
|
||||
return "define.title_gamepad_p1";
|
||||
}
|
||||
return "define.title_gamepad_p2";
|
||||
}
|
||||
|
||||
// Scancodes que MAI capturem com a binding (reservats per a navegacio o
|
||||
// global hotkeys). Tornen true → handleEvent les deixa passar al pipeline
|
||||
// global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12
|
||||
// son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio).
|
||||
auto isReservedScancode(SDL_Scancode sc) -> bool {
|
||||
if (sc == SDL_SCANCODE_ESCAPE) {
|
||||
return true;
|
||||
}
|
||||
if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) {
|
||||
return true;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) {
|
||||
return true;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Conversio sense pèrdua de SDL_Scancode → int per a comparacions
|
||||
// homogenies dins de sequence_ (que guarda codis de tots dos modes).
|
||||
auto scancodeToInt(SDL_Scancode sc) -> int {
|
||||
return static_cast<int>(sc);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace System {
|
||||
|
||||
std::unique_ptr<DefineInputs> DefineInputs::instance;
|
||||
|
||||
void DefineInputs::init(Rendering::Renderer* renderer) {
|
||||
if (!instance) {
|
||||
instance = std::unique_ptr<DefineInputs>(new DefineInputs(renderer));
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::destroy() { instance.reset(); }
|
||||
|
||||
auto DefineInputs::get() -> DefineInputs* { return instance.get(); }
|
||||
|
||||
DefineInputs::DefineInputs(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer),
|
||||
text_(renderer) {}
|
||||
|
||||
auto DefineInputs::isActive() const -> bool {
|
||||
return phase_ != Phase::INACTIVE;
|
||||
}
|
||||
|
||||
auto DefineInputs::begin(Mode mode, Player player) -> bool {
|
||||
if (mode == Mode::GAMEPAD) {
|
||||
// Requereix un pad assignat al jugador.
|
||||
const auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const int IDX = (player == Player::P1) ? 0 : 1;
|
||||
if (input->getPlayerGamepad(IDX) == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
mode_ = mode;
|
||||
player_ = player;
|
||||
index_ = 0;
|
||||
complete_timer_s_ = 0.0F;
|
||||
l2_was_pressed_ = false;
|
||||
r2_was_pressed_ = false;
|
||||
|
||||
buildSequence();
|
||||
phase_ = Phase::CAPTURING;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DefineInputs::cancel() {
|
||||
phase_ = Phase::INACTIVE;
|
||||
sequence_.clear();
|
||||
index_ = 0;
|
||||
complete_timer_s_ = 0.0F;
|
||||
}
|
||||
|
||||
void DefineInputs::buildSequence() {
|
||||
sequence_.clear();
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
// Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST)
|
||||
sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
|
||||
} else {
|
||||
// Mando: FIRE, ACCELERATE, START, MENU
|
||||
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1});
|
||||
}
|
||||
}
|
||||
|
||||
auto DefineInputs::isInUse(int code) const -> bool {
|
||||
return std::ranges::any_of(sequence_, [code](const Step& s) {
|
||||
return s.captured == code;
|
||||
});
|
||||
}
|
||||
|
||||
void DefineInputs::captureAndAdvance(int code) {
|
||||
if (index_ >= sequence_.size()) {
|
||||
return;
|
||||
}
|
||||
if (isInUse(code)) {
|
||||
return; // Duplicat dins de la sessio: rebutgem silenciosament
|
||||
}
|
||||
sequence_[index_].captured = code;
|
||||
++index_;
|
||||
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||
audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
|
||||
}
|
||||
if (index_ >= sequence_.size()) {
|
||||
persistAndComplete();
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::persistAndComplete() {
|
||||
auto& cfg = (player_ == Player::P1)
|
||||
? ConfigYaml::engine_config.player1
|
||||
: ConfigYaml::engine_config.player2;
|
||||
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
for (const Step& s : sequence_) {
|
||||
switch (s.action) {
|
||||
case InputAction::LEFT:
|
||||
cfg.keyboard.key_left = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::RIGHT:
|
||||
cfg.keyboard.key_right = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::SHOOT:
|
||||
cfg.keyboard.key_shoot = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::THRUST:
|
||||
cfg.keyboard.key_thrust = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
default:
|
||||
break; // START / MENU no es redefineixen al teclat
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const Step& s : sequence_) {
|
||||
switch (s.action) {
|
||||
case InputAction::SHOOT:
|
||||
cfg.gamepad.button_shoot = s.captured;
|
||||
break;
|
||||
case InputAction::THRUST:
|
||||
cfg.gamepad.button_thrust = s.captured;
|
||||
break;
|
||||
case InputAction::START:
|
||||
cfg.gamepad.button_start = s.captured;
|
||||
break;
|
||||
case InputAction::MENU:
|
||||
cfg.gamepad.button_menu = s.captured;
|
||||
break;
|
||||
default:
|
||||
break; // LEFT / RIGHT no es redefineixen al mando
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar canvis al runtime de l'Input i persistir a disc.
|
||||
if (auto* input = Input::get(); input != nullptr) {
|
||||
if (player_ == Player::P1) {
|
||||
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
|
||||
} else {
|
||||
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
|
||||
}
|
||||
}
|
||||
ConfigYaml::saveToFile();
|
||||
|
||||
phase_ = Phase::COMPLETE;
|
||||
complete_timer_s_ = COMPLETE_DISPLAY_S;
|
||||
}
|
||||
|
||||
void DefineInputs::update(float delta_time) {
|
||||
if (phase_ != Phase::COMPLETE) {
|
||||
return;
|
||||
}
|
||||
complete_timer_s_ -= delta_time;
|
||||
if (complete_timer_s_ <= 0.0F) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) {
|
||||
if (now && !was_pressed) {
|
||||
captureAndAdvance(virtual_button);
|
||||
}
|
||||
was_pressed = now;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool {
|
||||
if (event.type != SDL_EVENT_KEY_DOWN) {
|
||||
return true; // Empassem la resta sense fer res
|
||||
}
|
||||
const SDL_Scancode SC = event.key.scancode;
|
||||
if (isReservedScancode(SC)) {
|
||||
// ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline
|
||||
// global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.).
|
||||
return false;
|
||||
}
|
||||
captureAndAdvance(scancodeToInt(SC));
|
||||
return true;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool {
|
||||
// KEY_DOWN no es per al rebind de mando: deixem que el global el
|
||||
// gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.).
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrar events al pad del jugador actiu.
|
||||
const auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return true;
|
||||
}
|
||||
const int IDX = (player_ == Player::P1) ? 0 : 1;
|
||||
auto pad = input->getPlayerGamepad(IDX);
|
||||
if (!pad) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
if (event.gbutton.which != pad->instance_id) {
|
||||
return true;
|
||||
}
|
||||
captureAndAdvance(static_cast<int>(event.gbutton.button));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
if (event.gaxis.which != pad->instance_id) {
|
||||
return true;
|
||||
}
|
||||
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
|
||||
const Sint16 VAL = event.gaxis.value;
|
||||
if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
|
||||
processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
|
||||
} else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
|
||||
}
|
||||
// Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles).
|
||||
(void)STICK_THRESHOLD;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleEvent(const SDL_Event& event) -> bool {
|
||||
if (phase_ == Phase::INACTIVE) {
|
||||
return false;
|
||||
}
|
||||
// SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la
|
||||
// finestra encara que el modal estiga obert; els passem al pipeline.
|
||||
if (event.type == SDL_EVENT_QUIT ||
|
||||
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
|
||||
return false;
|
||||
}
|
||||
if (phase_ == Phase::COMPLETE) {
|
||||
// Mentre mostrem el missatge OK, empassem la resta d'events sense
|
||||
// capturar perque l'usuari no puga avançar accions sense voler.
|
||||
return true;
|
||||
}
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
return handleKeyboardEvent(event);
|
||||
}
|
||||
return handleGamepadEvent(event);
|
||||
}
|
||||
|
||||
void DefineInputs::draw() const {
|
||||
if (phase_ == Phase::INACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
using namespace Defaults::ServiceMenu;
|
||||
|
||||
// Caixa centrada, dimensions fixes (no depen del contingut a redefinir).
|
||||
constexpr float BOX_W = 560.0F;
|
||||
constexpr float BOX_H = 280.0F;
|
||||
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
|
||||
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
|
||||
|
||||
// Fons + brackets als 4 cantons (estil HUD del menu de servei).
|
||||
fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);
|
||||
|
||||
const auto T = static_cast<float>(CORNER_THICKNESS);
|
||||
const auto AH = static_cast<float>(CORNER_ARM_H);
|
||||
const auto AV = static_cast<float>(CORNER_ARM_V);
|
||||
fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
|
||||
|
||||
const std::string TITLE = Locale::get().text(titleKey(mode_, player_));
|
||||
const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING);
|
||||
const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F);
|
||||
const float TITLE_Y = BOX_Y + 26.0F;
|
||||
text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR);
|
||||
|
||||
if (phase_ == Phase::COMPLETE) {
|
||||
const std::string OK = Locale::get().text("define.complete");
|
||||
constexpr float OK_SCALE = 0.7F;
|
||||
const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING);
|
||||
const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F);
|
||||
const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F;
|
||||
constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255};
|
||||
text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Instruccio (premeu tecla / boto) + accio actual + progres.
|
||||
const std::string PROMPT = Locale::get().text(
|
||||
mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button");
|
||||
const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING);
|
||||
const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F);
|
||||
const float PROMPT_Y = BOX_Y + 86.0F;
|
||||
text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR);
|
||||
|
||||
if (index_ < sequence_.size()) {
|
||||
const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key);
|
||||
constexpr float ACTION_SCALE = 0.9F;
|
||||
const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING);
|
||||
const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F);
|
||||
const float ACTION_Y = BOX_Y + 130.0F;
|
||||
text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR);
|
||||
}
|
||||
|
||||
const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size());
|
||||
constexpr float PROG_SCALE = 0.4F;
|
||||
const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING);
|
||||
const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F);
|
||||
const float PROG_Y = BOX_Y + 200.0F;
|
||||
text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR);
|
||||
}
|
||||
|
||||
} // namespace System
|
||||
@@ -0,0 +1,107 @@
|
||||
// define_inputs.hpp - Overlay modal de redefinici de controls (singleton)
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Sub-mòdul inspirat en aee_arcade/source/core/input/define_buttons. Quan el
|
||||
// menú de servei dispara una acció "Redefinir tecles/botons P1/P2", aquest
|
||||
// singleton pren el control: pinta una caixa central, captura events SDL i
|
||||
// avança per una seqüència fixa d'accions, persistint les noves assignacions
|
||||
// a config.yaml en acabar.
|
||||
//
|
||||
// Cicle de vida:
|
||||
// 1. begin(mode, player) → construeix la seqüència (4 passos) i activa
|
||||
// l'overlay. Per a GAMEPAD, retorna false si el jugador no té pad.
|
||||
// 2. handleEvent() captura el següent event vàlid; ESC cancel·la sense
|
||||
// desar; duplicats dins de la sessió es rebutgen silenciosament.
|
||||
// 3. Quan la seqüència es completa, persistim a engine_config + saveToFile,
|
||||
// reapliquem els bindings i mostrem un missatge "OK" durant 1.5 s
|
||||
// abans d'auto-tancar-se.
|
||||
//
|
||||
// El routing d'events es fa des de GlobalEvents::handle: mentre isActive()
|
||||
// retorna true, tots els events SDL es desvien aquí i no arriben al joc ni
|
||||
// al menú de servei.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/input/input_types.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
namespace System {
|
||||
|
||||
class DefineInputs {
|
||||
public:
|
||||
enum class Mode : std::uint8_t { KEYBOARD,
|
||||
GAMEPAD };
|
||||
enum class Player : std::uint8_t { P1,
|
||||
P2 };
|
||||
|
||||
static void init(Rendering::Renderer* renderer);
|
||||
static void destroy();
|
||||
[[nodiscard]] static auto get() -> DefineInputs*;
|
||||
|
||||
// Comença la sessió. Retorna false per a GAMEPAD si el jugador no té
|
||||
// cap pad assignat (el caller hauria de notificar a l'usuari abans).
|
||||
auto begin(Mode mode, Player player) -> bool;
|
||||
void cancel();
|
||||
|
||||
[[nodiscard]] auto isActive() const -> bool;
|
||||
|
||||
void update(float delta_time);
|
||||
void draw() const;
|
||||
|
||||
// Retorna true si l'event s'ha consumit (és a dir, mentre l'overlay
|
||||
// és actiu sempre consumeix tot per evitar passages al joc o menú).
|
||||
auto handleEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
private:
|
||||
explicit DefineInputs(Rendering::Renderer* renderer);
|
||||
|
||||
enum class Phase : std::uint8_t {
|
||||
INACTIVE,
|
||||
CAPTURING,
|
||||
COMPLETE, // mostra missatge OK breu abans d'auto-cancel
|
||||
};
|
||||
|
||||
struct Step {
|
||||
std::string action_label_key; // p.ex. "define.action.left"
|
||||
InputAction action; // mapeig a la struct PlayerBindings
|
||||
int captured{-1}; // scancode o button code; -1 = sense capturar
|
||||
};
|
||||
|
||||
void buildSequence();
|
||||
[[nodiscard]] auto isInUse(int code) const -> bool;
|
||||
void captureAndAdvance(int code);
|
||||
void persistAndComplete();
|
||||
|
||||
// Handlers especialitzats segons mode_.
|
||||
auto handleKeyboardEvent(const SDL_Event& event) -> bool;
|
||||
auto handleGamepadEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
// Edge-detect per als triggers L2/R2 com a botons virtuals.
|
||||
void processTrigger(int virtual_button, bool& was_pressed, bool now);
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
Graphics::VectorText text_;
|
||||
|
||||
Phase phase_{Phase::INACTIVE};
|
||||
Mode mode_{Mode::KEYBOARD};
|
||||
Player player_{Player::P1};
|
||||
std::vector<Step> sequence_;
|
||||
std::size_t index_{0};
|
||||
float complete_timer_s_{0.0F};
|
||||
|
||||
// Estat d'edge-detect dels triggers durant la sessió GAMEPAD.
|
||||
bool l2_was_pressed_{false};
|
||||
bool r2_was_pressed_{false};
|
||||
|
||||
static std::unique_ptr<DefineInputs> instance;
|
||||
};
|
||||
|
||||
} // namespace System
|
||||
+84
-18
@@ -8,6 +8,10 @@
|
||||
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||
#include <utility> // Para move
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/system/notifier.hpp"
|
||||
#include "core/utils/string_utils.hpp"
|
||||
|
||||
// Singleton
|
||||
Input* Input::instance = nullptr;
|
||||
|
||||
@@ -373,9 +377,25 @@ void Input::update() {
|
||||
|
||||
// --- MANDOS ---
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
// LEFT i RIGHT NO son redefinibles al mando (assumits dpad o stick).
|
||||
// Llegim el left stick X i el fusionem amb l'estat del dpad: qualsevol
|
||||
// de les dos fonts activa l'accio. Llindar AXIS_THRESHOLD (30000).
|
||||
const Sint16 STICK_X = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
const bool STICK_LEFT = STICK_X < -AXIS_THRESHOLD;
|
||||
const bool STICK_RIGHT = STICK_X > AXIS_THRESHOLD;
|
||||
|
||||
for (auto& binding : gamepad->bindings) {
|
||||
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
|
||||
|
||||
// Per a LEFT/RIGHT, fer un OR amb el stick X. La resta d'accions
|
||||
// (THRUST/SHOOT/START/MENU) ignoren el stick aqui — si es vol
|
||||
// dispar amb trigger L2/R2 cal binding amb codi 100/101.
|
||||
if (binding.first == Action::LEFT) {
|
||||
button_is_down_now = button_is_down_now || STICK_LEFT;
|
||||
} else if (binding.first == Action::RIGHT) {
|
||||
button_is_down_now = button_is_down_now || STICK_RIGHT;
|
||||
}
|
||||
|
||||
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
||||
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
|
||||
binding.second.is_held = button_is_down_now;
|
||||
@@ -407,6 +427,16 @@ auto Input::addGamepad(int device_index) -> std::string {
|
||||
auto name = gamepad->name;
|
||||
std::cout << "Gamepad connected (" << name << ")" << '\n';
|
||||
gamepads_.push_back(std::move(gamepad));
|
||||
|
||||
// Toast a pantalla. Pot ser nullptr durant discoverGamepads() inicial
|
||||
// (l'Input::init() es crida abans que el Director instanciï el Notifier).
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.gamepad_connected"),
|
||||
"{name}",
|
||||
Utils::toUpperAscii(name)));
|
||||
}
|
||||
|
||||
return name + " CONNECTED";
|
||||
}
|
||||
|
||||
@@ -419,6 +449,14 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
|
||||
std::string name = (*it)->name;
|
||||
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
|
||||
gamepads_.erase(it);
|
||||
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.gamepad_disconnected"),
|
||||
"{name}",
|
||||
Utils::toUpperAscii(name)));
|
||||
}
|
||||
|
||||
return name + " DISCONNECTED";
|
||||
}
|
||||
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
|
||||
@@ -465,6 +503,33 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
|
||||
|
||||
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
|
||||
|
||||
// Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els
|
||||
// dos camps venen buits o no n'hi ha cap match retornem nullptr (sense
|
||||
// mando explicit). L'autoassignacio inicial es resol al boot.
|
||||
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad> {
|
||||
if (gamepads_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!bindings.gamepad_path.empty()) {
|
||||
for (const auto& pad : gamepads_) {
|
||||
if (pad && pad->path == bindings.gamepad_path) {
|
||||
return pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bindings.gamepad_name.empty()) {
|
||||
for (const auto& pad : gamepads_) {
|
||||
if (pad && pad->name == bindings.gamepad_name) {
|
||||
return pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Aplica configuración de controles del player 1
|
||||
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
|
||||
@@ -474,15 +539,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||
|
||||
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
|
||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||
if (bindings.gamepad_name.empty()) {
|
||||
// Fallback: usar primer gamepad disponible
|
||||
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
|
||||
} else {
|
||||
// Buscar por nombre
|
||||
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
||||
}
|
||||
// 2. Resoldre gamepad per path/name
|
||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||
|
||||
if (!gamepad) {
|
||||
player1_gamepad_ = nullptr;
|
||||
@@ -494,6 +552,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
|
||||
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
|
||||
|
||||
// 4. Cachear referencia
|
||||
player1_gamepad_ = gamepad;
|
||||
@@ -508,15 +568,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
||||
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||
|
||||
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
|
||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||
if (bindings.gamepad_name.empty()) {
|
||||
// Fallback: usar segundo gamepad disponible
|
||||
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
|
||||
} else {
|
||||
// Buscar por nombre
|
||||
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
||||
}
|
||||
// 2. Resoldre gamepad per path/name
|
||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||
|
||||
if (!gamepad) {
|
||||
player2_gamepad_ = nullptr;
|
||||
@@ -528,6 +581,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
||||
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
|
||||
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
|
||||
|
||||
// 4. Cachear referencia
|
||||
player2_gamepad_ = gamepad;
|
||||
@@ -555,6 +610,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
|
||||
return keyboard_active || gamepad_active;
|
||||
}
|
||||
|
||||
// Retorna el pad assignat (0=P1, 1=P2). Pot ser nullptr.
|
||||
auto Input::getPlayerGamepad(int player_index) const -> std::shared_ptr<Input::Gamepad> {
|
||||
if (player_index == 0) {
|
||||
return player1_gamepad_;
|
||||
}
|
||||
if (player_index == 1) {
|
||||
return player2_gamepad_;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Consulta de input para player 2
|
||||
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
|
||||
// Comprobar teclado con el mapa específico de P2
|
||||
|
||||
@@ -62,7 +62,9 @@ class Input {
|
||||
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
|
||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {}
|
||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}},
|
||||
{Action::START, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
|
||||
{Action::MENU, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}}} {}
|
||||
|
||||
~Gamepad() {
|
||||
if (pad != nullptr) {
|
||||
@@ -107,6 +109,10 @@ class Input {
|
||||
auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
|
||||
auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
|
||||
|
||||
// Accés al gamepad assignat per jugador (0=P1, 1=P2). nullptr si no n'hi
|
||||
// ha cap d'assignat o connectat. Usat per la UI de redefinició de botons.
|
||||
[[nodiscard]] auto getPlayerGamepad(int player_index) const -> std::shared_ptr<Gamepad>;
|
||||
|
||||
// Check if any player pressed any action from a list
|
||||
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||
|
||||
@@ -142,6 +148,7 @@ class Input {
|
||||
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||
void addGamepadMappingsFromFile();
|
||||
void discoverGamepads();
|
||||
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Input* instance; // Instancia única del singleton
|
||||
|
||||
@@ -6,6 +6,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||
{InputAction::RIGHT, "RIGHT"},
|
||||
{InputAction::THRUST, "THRUST"},
|
||||
{InputAction::SHOOT, "SHOOT"},
|
||||
{InputAction::START, "START"},
|
||||
{InputAction::MENU, "MENU"},
|
||||
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
||||
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
||||
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
||||
@@ -18,6 +20,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||
{"RIGHT", InputAction::RIGHT},
|
||||
{"THRUST", InputAction::THRUST},
|
||||
{"SHOOT", InputAction::SHOOT},
|
||||
{"START", InputAction::START},
|
||||
{"MENU", InputAction::MENU},
|
||||
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
||||
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
||||
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
||||
|
||||
@@ -15,6 +15,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j
|
||||
THRUST, // Acelerar
|
||||
SHOOT, // Disparar
|
||||
START, // Empezar match
|
||||
MENU, // Abrir/cerrar menu de servicio (equivalent a F12)
|
||||
|
||||
// Inputs de sistema (globales)
|
||||
WINDOW_INC_ZOOM, // F2
|
||||
|
||||
@@ -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"
|
||||
@@ -108,6 +109,34 @@ Director::Director(int argc, char* argv[])
|
||||
// Inicialitzar sistema de input
|
||||
Input::init("data/gamecontrollerdb.txt");
|
||||
|
||||
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
|
||||
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
|
||||
// P2 = pad 1 si existeix) i ho persistim. Aixo nomes dispara amb tots
|
||||
// dos buits perque un "SENSE MANDO" explicit ha de sobreviure entre
|
||||
// arrancades.
|
||||
{
|
||||
auto& p1 = cfg_->player1;
|
||||
auto& p2 = cfg_->player2;
|
||||
const bool BOTH_EMPTY = p1.gamepad_name.empty() && p1.gamepad_path.empty() && p2.gamepad_name.empty() && p2.gamepad_path.empty();
|
||||
if (BOTH_EMPTY) {
|
||||
const auto& pads = Input::get()->getGamepads();
|
||||
bool changed = false;
|
||||
if (!pads.empty() && pads[0]) {
|
||||
p1.gamepad_name = pads[0]->name;
|
||||
p1.gamepad_path = pads[0]->path;
|
||||
changed = true;
|
||||
}
|
||||
if (pads.size() > 1 && pads[1]) {
|
||||
p2.gamepad_name = pads[1]->name;
|
||||
p2.gamepad_path = pads[1]->path;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
ConfigYaml::saveToFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar configuración de controls dels jugadors
|
||||
Input::get()->applyPlayer1Bindings(cfg_->player1);
|
||||
Input::get()->applyPlayer2Bindings(cfg_->player2);
|
||||
@@ -178,6 +207,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 +222,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 +407,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.),
|
||||
@@ -389,9 +423,17 @@ auto Director::iterate() -> SDL_AppResult {
|
||||
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->draw(); // toast: per damunt de tot
|
||||
}
|
||||
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||
// Mentre l'overlay de redefinicio esta actiu, amaguem el menu de servei
|
||||
// (encara queda "open" per a absorbir events un cop el modal s'auto-tanqui,
|
||||
// pero no es pinta per no confondre's visualment amb el modal).
|
||||
const auto* di = System::DefineInputs::get();
|
||||
const bool DEFINE_ACTIVE = (di != nullptr) && di->isActive();
|
||||
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && !DEFINE_ACTIVE) {
|
||||
menu->draw(); // service menu: per damunt fins i tot dels toasts
|
||||
}
|
||||
if (di != nullptr) {
|
||||
di->draw(); // overlay de rebind: per damunt de tot
|
||||
}
|
||||
sdl_->present();
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -22,24 +23,74 @@ namespace GlobalEvents {
|
||||
|
||||
namespace {
|
||||
|
||||
// Reenvia el KEY_DOWN al menu de servei si esta obert i la tecla no
|
||||
// es F1-F12 ni ESC (que sempre passen com a globals). Retorna true si
|
||||
// el menu l'ha consumit.
|
||||
// Reenvia events al menu de servei si esta obert. Accepta:
|
||||
// - KEY_DOWN (excepte F1-F12 i ESC, que sempre passen com a globals)
|
||||
// - GAMEPAD_BUTTON_DOWN (per navegacio amb dpad + FIRE/ACCELERATE)
|
||||
// - GAMEPAD_AXIS_MOTION (per navegacio amb stick)
|
||||
// Retorna true si l'event s'ha entregat al menu.
|
||||
auto forwardToServiceMenu(const SDL_Event& event) -> bool {
|
||||
if (event.type != SDL_EVENT_KEY_DOWN) {
|
||||
return false;
|
||||
}
|
||||
auto* menu = System::ServiceMenu::get();
|
||||
if (menu == nullptr || !menu->isOpen()) {
|
||||
return false;
|
||||
}
|
||||
const SDL_Scancode SC = event.key.scancode;
|
||||
const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) ||
|
||||
(SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12);
|
||||
if (PASSTHROUGH) {
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
const SDL_Scancode SC = event.key.scancode;
|
||||
const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) ||
|
||||
(SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12);
|
||||
if (PASSTHROUGH) {
|
||||
return false;
|
||||
}
|
||||
menu->handleEvent(event);
|
||||
return true;
|
||||
}
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
menu->handleEvent(event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Engoleix els events que DefineInputs vol consumir mentre l'overlay
|
||||
// es actiu. Els events que el modul torna a passar (QUIT, ESC) cauen
|
||||
// cap al pipeline normal i poden tancar la finestra o obrir el prompt
|
||||
// d'eixida sense haver de completar la sequencia.
|
||||
auto consumeIfDefineActive(const SDL_Event& event) -> bool {
|
||||
auto* di = System::DefineInputs::get();
|
||||
if (di == nullptr || !di->isActive()) {
|
||||
return false;
|
||||
}
|
||||
menu->handleEvent(event);
|
||||
return di->handleEvent(event);
|
||||
}
|
||||
|
||||
// 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.
|
||||
auto handleGamepadMenuButton(const SDL_Event& event) -> bool {
|
||||
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
return false;
|
||||
}
|
||||
auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto match_player = [&](int player_index) {
|
||||
auto pad = input->getPlayerGamepad(player_index);
|
||||
if (!pad || pad->instance_id != event.gbutton.which) {
|
||||
return false;
|
||||
}
|
||||
auto it = pad->bindings.find(InputAction::MENU);
|
||||
if (it == pad->bindings.end()) {
|
||||
return false;
|
||||
}
|
||||
return it->second.button == static_cast<int>(event.gbutton.button);
|
||||
};
|
||||
if (!match_player(0) && !match_player(1)) {
|
||||
return false;
|
||||
}
|
||||
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||
menu->toggle();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -52,6 +103,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);
|
||||
@@ -62,6 +119,11 @@ namespace GlobalEvents {
|
||||
// 3. Gestió del ratolí (auto-ocultar)
|
||||
Mouse::handleEvent(event);
|
||||
|
||||
// 3b. Botó MENU al mando (equivalent a F12)
|
||||
if (handleGamepadMenuButton(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de
|
||||
// funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen,
|
||||
// vsync, AA, postfx, locale, exit prompt). Aixi el menu captura
|
||||
|
||||
@@ -15,11 +15,15 @@
|
||||
#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 "core/utils/string_utils.hpp"
|
||||
#include "game/config_yaml.hpp"
|
||||
#include "project.h"
|
||||
|
||||
@@ -54,17 +58,6 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// VectorText nomes admet ASCII en majuscules. El git hash sortit de git
|
||||
// rev-parse es lowercase (a-f), aixi que el passem a uppercase per al
|
||||
// display sense modificar Project::GIT_HASH.
|
||||
auto toUpperAscii(const std::string& s) -> std::string {
|
||||
std::string result = s;
|
||||
for (char& c : result) {
|
||||
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Resol el text del label d'un item: prioritza label_text (literal) sobre
|
||||
// label_key (locale). Retorna cadena buida si tots dos son buits.
|
||||
auto resolveLabel(const System::ServiceMenu::Item& item) -> std::string {
|
||||
@@ -77,6 +70,107 @@ 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 Utils::toUpperAscii(pad->name);
|
||||
}
|
||||
|
||||
// Index del pad assignat dins de la llista. Retorna pads.size() per
|
||||
// representar el slot virtual SENSE MANDO (al final del cycle).
|
||||
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 pads.size(); // Slot virtual "sense mando"
|
||||
}
|
||||
|
||||
// Aplica les noves assignacions a Input. Si ha hagut swap, refresca els
|
||||
// dos jugadors; en cas contrari nomes el que ha canviat.
|
||||
void reapplyBindings(int player_index, bool swap_other) {
|
||||
auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (player_index == 0) {
|
||||
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
|
||||
if (swap_other) {
|
||||
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
|
||||
}
|
||||
} else {
|
||||
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
|
||||
if (swap_other) {
|
||||
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assigna un pad concret (per nom+path) a un jugador i ho persisteix.
|
||||
// Si l'altre jugador ja tenia eixe pad, fa swap: l'altre rep
|
||||
// l'assignacio prèvia d'aquest. Per desasignar, passar new_name i
|
||||
// new_path buits.
|
||||
void assignPadToPlayer(int player_index, const std::string& new_name, const std::string& new_path) {
|
||||
auto& pcfg = (player_index == 0)
|
||||
? ConfigYaml::engine_config.player1
|
||||
: ConfigYaml::engine_config.player2;
|
||||
auto& other = (player_index == 0)
|
||||
? ConfigYaml::engine_config.player2
|
||||
: ConfigYaml::engine_config.player1;
|
||||
|
||||
// Detecta conflicte amb l'altre jugador per fer swap. Prioritzem
|
||||
// path (mateix criteri que resolvePlayerGamepad); si nomes tenim
|
||||
// nom (mando reconegut sense path), comparem per nom.
|
||||
const bool CONFLICT = !new_path.empty() && other.gamepad_path == new_path;
|
||||
const bool CONFLICT_BY_NAME = !new_name.empty() && new_path.empty() &&
|
||||
other.gamepad_name == new_name;
|
||||
const bool DO_SWAP = CONFLICT || CONFLICT_BY_NAME;
|
||||
|
||||
const std::string PREV_NAME = pcfg.gamepad_name;
|
||||
const std::string PREV_PATH = pcfg.gamepad_path;
|
||||
|
||||
pcfg.gamepad_name = new_name;
|
||||
pcfg.gamepad_path = new_path;
|
||||
if (DO_SWAP) {
|
||||
other.gamepad_name = PREV_NAME;
|
||||
other.gamepad_path = PREV_PATH;
|
||||
}
|
||||
|
||||
reapplyBindings(player_index, DO_SWAP);
|
||||
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 +247,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,13 +532,138 @@ namespace System {
|
||||
return page;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl) -> ServiceMenu::Item {
|
||||
return ServiceMenu::Item{
|
||||
.kind = ServiceMenu::Kind::ACTION,
|
||||
.label_key = label_key,
|
||||
.label_text = {},
|
||||
.selectable = true,
|
||||
// El menu de servei NO es tanca: queda obert per sota de
|
||||
// l'overlay i absorbira qualsevol event que arribi un cop
|
||||
// l'overlay s'haja auto-cancel·lat.
|
||||
.on_activate = [mode, pl] { startDefine(mode, pl); },
|
||||
.get_value_text = {},
|
||||
.on_change = {},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto ServiceMenu::buildControlsPage() -> Page {
|
||||
Page page;
|
||||
page.title_key = "service_menu.controls";
|
||||
page.items = {
|
||||
Item{
|
||||
.kind = Kind::SUBMENU,
|
||||
.label_key = "service_menu.controls_pad_p1",
|
||||
.label_text = {},
|
||||
.selectable = true,
|
||||
.on_activate = [this] { pushPage(buildPadPickerPage(0)); },
|
||||
.get_value_text = [] { return padDisplayName(0); },
|
||||
.on_change = {},
|
||||
},
|
||||
Item{
|
||||
.kind = Kind::SUBMENU,
|
||||
.label_key = "service_menu.controls_pad_p2",
|
||||
.label_text = {},
|
||||
.selectable = true,
|
||||
.on_activate = [this] { pushPage(buildPadPickerPage(1)); },
|
||||
.get_value_text = [] { return padDisplayName(1); },
|
||||
.on_change = {},
|
||||
},
|
||||
makeDefineItem("service_menu.controls_define_keyboard_p1",
|
||||
DefineInputs::Mode::KEYBOARD,
|
||||
DefineInputs::Player::P1),
|
||||
makeDefineItem("service_menu.controls_define_keyboard_p2",
|
||||
DefineInputs::Mode::KEYBOARD,
|
||||
DefineInputs::Player::P2),
|
||||
makeDefineItem("service_menu.controls_define_gamepad_p1",
|
||||
DefineInputs::Mode::GAMEPAD,
|
||||
DefineInputs::Player::P1),
|
||||
makeDefineItem("service_menu.controls_define_gamepad_p2",
|
||||
DefineInputs::Mode::GAMEPAD,
|
||||
DefineInputs::Player::P2),
|
||||
};
|
||||
return page;
|
||||
}
|
||||
|
||||
auto ServiceMenu::buildPadPickerPage(int player_index) -> Page {
|
||||
Page page;
|
||||
page.title_key = (player_index == 0)
|
||||
? "service_menu.controls_pad_p1"
|
||||
: "service_menu.controls_pad_p2";
|
||||
|
||||
const auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return page;
|
||||
}
|
||||
const auto& pads = input->getGamepads();
|
||||
const auto& pcfg = (player_index == 0)
|
||||
? ConfigYaml::engine_config.player1
|
||||
: ConfigYaml::engine_config.player2;
|
||||
const auto& other = (player_index == 0)
|
||||
? ConfigYaml::engine_config.player2
|
||||
: ConfigYaml::engine_config.player1;
|
||||
|
||||
// Cursor inicial sobre el pad assignat, o sobre "SENSE MANDO"
|
||||
// (ultim item) si el jugador no en te cap.
|
||||
page.cursor = findAssignedIndex(pads, pcfg);
|
||||
|
||||
for (const auto& pad : pads) {
|
||||
if (!pad) {
|
||||
continue;
|
||||
}
|
||||
std::string label = Utils::toUpperAscii(pad->name);
|
||||
// Sufix (PX) nomes si el mando el te l'altre jugador, perque
|
||||
// l'usuari sapiga que assignar-lo li'l "robarà".
|
||||
const bool OTHER_HAS_BY_PATH = !other.gamepad_path.empty() && other.gamepad_path == pad->path;
|
||||
const bool OTHER_HAS_BY_NAME = other.gamepad_path.empty() && !other.gamepad_name.empty() && other.gamepad_name == pad->name;
|
||||
if (OTHER_HAS_BY_PATH || OTHER_HAS_BY_NAME) {
|
||||
label += (player_index == 0) ? " (P2)" : " (P1)";
|
||||
}
|
||||
const std::string PAD_NAME = pad->name;
|
||||
const std::string PAD_PATH = pad->path;
|
||||
const int PI = player_index;
|
||||
page.items.push_back(Item{
|
||||
.kind = Kind::ACTION,
|
||||
.label_key = {},
|
||||
.label_text = std::move(label),
|
||||
.selectable = true,
|
||||
.on_activate = [this, PI, PAD_NAME, PAD_PATH] {
|
||||
assignPadToPlayer(PI, PAD_NAME, PAD_PATH);
|
||||
popPage();
|
||||
},
|
||||
.get_value_text = {},
|
||||
.on_change = {},
|
||||
});
|
||||
}
|
||||
|
||||
// Item final: desasignar.
|
||||
const int PI = player_index;
|
||||
page.items.push_back(Item{
|
||||
.kind = Kind::ACTION,
|
||||
.label_key = "service_menu.controls_no_pad",
|
||||
.label_text = {},
|
||||
.selectable = true,
|
||||
.on_activate = [this, PI] {
|
||||
assignPadToPlayer(PI, {}, {});
|
||||
popPage();
|
||||
},
|
||||
.get_value_text = {},
|
||||
.on_change = {},
|
||||
});
|
||||
return page;
|
||||
}
|
||||
|
||||
auto ServiceMenu::buildSystemPage() -> Page {
|
||||
Page page;
|
||||
page.title_key = "service_menu.system";
|
||||
// Versio + hash com a subtitol sota el titol (apagat, mes petit).
|
||||
// Uppercase del hash perque VectorText nomes admet majuscules.
|
||||
page.subtitle_provider = [] {
|
||||
return std::format("V{} - {}", Project::VERSION, toUpperAscii(Project::GIT_HASH));
|
||||
return std::format("V{} - {}", Project::VERSION, Utils::toUpperAscii(Project::GIT_HASH));
|
||||
};
|
||||
page.items = {
|
||||
// REINICIAR (amb confirmacio).
|
||||
@@ -593,10 +813,55 @@ namespace System {
|
||||
}
|
||||
}
|
||||
|
||||
auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool {
|
||||
if (!open_ || stack_.empty() || event.type != SDL_EVENT_KEY_DOWN) {
|
||||
namespace {
|
||||
|
||||
// Llindar de stick per a navegacio de menu (mig camp del rang ±32767).
|
||||
// Mes baix que el del joc (30000) per a una resposta mes agil al menu.
|
||||
constexpr Sint16 MENU_STICK_THRESHOLD = 16384;
|
||||
// Llindar de trigger (mateix valor que MENU_TRIGGER_THRESHOLD, que
|
||||
// és private). Edge a partir del 50% del rang.
|
||||
constexpr Sint16 MENU_TRIGGER_THRESHOLD = 16384;
|
||||
|
||||
// Retorna true si el codi de boto SDL coincideix amb l'accio
|
||||
// configurada per algun dels dos jugadors (es a dir, el boto te el
|
||||
// mateix codi al binding de FIRE o ACCELERATE del pad emissor).
|
||||
auto buttonMatchesAction(SDL_JoystickID which, int button, InputAction action) -> bool {
|
||||
const auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
auto pad = input->getPlayerGamepad(i);
|
||||
if (!pad || pad->instance_id != which) {
|
||||
continue;
|
||||
}
|
||||
auto it = pad->bindings.find(action);
|
||||
if (it != pad->bindings.end() && it->second.button == button) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool {
|
||||
if (!open_ || stack_.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
return handleKeyDown(event);
|
||||
}
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
return handleGamepadButton(event);
|
||||
}
|
||||
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
return handleGamepadAxis(event);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ServiceMenu::handleKeyDown(const SDL_Event& event) -> bool {
|
||||
switch (event.key.scancode) {
|
||||
case SDL_SCANCODE_UP:
|
||||
moveCursor(-1);
|
||||
@@ -622,6 +887,101 @@ namespace System {
|
||||
}
|
||||
}
|
||||
|
||||
auto ServiceMenu::handleGamepadButton(const SDL_Event& event) -> bool {
|
||||
const int BTN = static_cast<int>(event.gbutton.button);
|
||||
if (BTN == SDL_GAMEPAD_BUTTON_DPAD_UP) {
|
||||
moveCursor(-1);
|
||||
return true;
|
||||
}
|
||||
if (BTN == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
|
||||
moveCursor(+1);
|
||||
return true;
|
||||
}
|
||||
if (BTN == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
|
||||
changeValue(-1);
|
||||
return true;
|
||||
}
|
||||
if (BTN == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
|
||||
changeValue(+1);
|
||||
return true;
|
||||
}
|
||||
// Botons d'accio per al pad emissor: FIRE = ENTER, ACCELERATE = BACK.
|
||||
if (buttonMatchesAction(event.gbutton.which, BTN, InputAction::SHOOT)) {
|
||||
activateCurrent();
|
||||
return true;
|
||||
}
|
||||
if (buttonMatchesAction(event.gbutton.which, BTN, InputAction::THRUST)) {
|
||||
popPage();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ServiceMenu::processStickX(Sint16 val) {
|
||||
const bool LEFT_NOW = val < -MENU_STICK_THRESHOLD;
|
||||
const bool RIGHT_NOW = val > MENU_STICK_THRESHOLD;
|
||||
if (LEFT_NOW && !stick_left_held_) {
|
||||
changeValue(-1);
|
||||
}
|
||||
if (RIGHT_NOW && !stick_right_held_) {
|
||||
changeValue(+1);
|
||||
}
|
||||
stick_left_held_ = LEFT_NOW;
|
||||
stick_right_held_ = RIGHT_NOW;
|
||||
}
|
||||
|
||||
void ServiceMenu::processStickY(Sint16 val) {
|
||||
const bool UP_NOW = val < -MENU_STICK_THRESHOLD;
|
||||
const bool DOWN_NOW = val > MENU_STICK_THRESHOLD;
|
||||
if (UP_NOW && !stick_up_held_) {
|
||||
moveCursor(-1);
|
||||
}
|
||||
if (DOWN_NOW && !stick_down_held_) {
|
||||
moveCursor(+1);
|
||||
}
|
||||
stick_up_held_ = UP_NOW;
|
||||
stick_down_held_ = DOWN_NOW;
|
||||
}
|
||||
|
||||
// Edge-detect d'un trigger: si creua el llindar amunt, despatxa
|
||||
// ENTER/BACK segons el binding (FIRE/ACCEL) que apunta al codi
|
||||
// virtual del trigger (100 = L2, 101 = R2).
|
||||
void ServiceMenu::processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held) {
|
||||
const bool NOW = val > MENU_TRIGGER_THRESHOLD;
|
||||
if (NOW && !held) {
|
||||
if (buttonMatchesAction(which, virtual_button, InputAction::SHOOT)) {
|
||||
activateCurrent();
|
||||
} else if (buttonMatchesAction(which, virtual_button, InputAction::THRUST)) {
|
||||
popPage();
|
||||
}
|
||||
}
|
||||
held = NOW;
|
||||
}
|
||||
|
||||
auto ServiceMenu::handleGamepadAxis(const SDL_Event& event) -> bool {
|
||||
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
|
||||
const Sint16 VAL = event.gaxis.value;
|
||||
switch (AXIS) {
|
||||
case SDL_GAMEPAD_AXIS_LEFTX:
|
||||
processStickX(VAL);
|
||||
return true;
|
||||
case SDL_GAMEPAD_AXIS_LEFTY:
|
||||
processStickY(VAL);
|
||||
return true;
|
||||
// Triggers L2/R2: SDL3 nomes emet AXIS_MOTION, no button events.
|
||||
// Per poder rebindar FIRE/ACCEL als triggers, sintetitzem aqui
|
||||
// la pulsacio amb edge-detect i la passem pel mateix flux.
|
||||
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
|
||||
processTriggerEdge(event.gaxis.which, VAL, Input::TRIGGER_L2_AS_BUTTON, trigger_l2_held_);
|
||||
return true;
|
||||
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
|
||||
processTriggerEdge(event.gaxis.which, VAL, Input::TRIGGER_R2_AS_BUTTON, trigger_r2_held_);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto ServiceMenu::computeTargetHeight() const -> float {
|
||||
if (stack_.empty()) {
|
||||
return 0.0F;
|
||||
|
||||
@@ -85,19 +85,39 @@ namespace System {
|
||||
void update(float delta_time);
|
||||
void draw() const;
|
||||
|
||||
// Processa el KEY_DOWN. Retorna true si l'ha consumit (UP/DOWN/ENTER/
|
||||
// RIGHT/BACKSPACE/LEFT mentre esta obert). false en qualsevol altre cas.
|
||||
// Processa events de navegacio. Retorna true si l'event s'ha consumit.
|
||||
// Accepta:
|
||||
// - SDL_EVENT_KEY_DOWN: UP/DOWN/ENTER/RIGHT/LEFT/BACKSPACE.
|
||||
// - SDL_EVENT_GAMEPAD_BUTTON_DOWN: DPAD per nav, FIRE = ENTER,
|
||||
// ACCELERATE = BACK. La resta de botons s'ignoren.
|
||||
// - SDL_EVENT_GAMEPAD_AXIS_MOTION: stick X/Y amb edge-detect.
|
||||
auto handleEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
private:
|
||||
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
||||
|
||||
// Sub-handlers de handleEvent. Privats, no son part de l'API publica.
|
||||
auto handleKeyDown(const SDL_Event& event) -> bool;
|
||||
auto handleGamepadButton(const SDL_Event& event) -> bool;
|
||||
auto handleGamepadAxis(const SDL_Event& event) -> bool;
|
||||
// Helpers per a cada eix; permeten que handleGamepadAxis es quedi
|
||||
// com a dispatcher i no bote el llindar de complexitat.
|
||||
void processStickX(Sint16 val);
|
||||
void processStickY(Sint16 val);
|
||||
void processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held);
|
||||
|
||||
void buildRootPage();
|
||||
[[nodiscard]] auto buildVideoPage() -> Page;
|
||||
[[nodiscard]] auto buildResolutionPage() const -> Page;
|
||||
[[nodiscard]] static auto buildAudioPage() -> Page;
|
||||
[[nodiscard]] auto buildOptionsPage() const -> Page;
|
||||
[[nodiscard]] auto buildSystemPage() -> Page;
|
||||
[[nodiscard]] auto buildControlsPage() -> Page;
|
||||
// Llista de mandos detectats per a un jugador. Cada item assigna el
|
||||
// pad triat (amb swap automatic si l'altre jugador ja el tenia) i
|
||||
// tanca la picker amb popPage. L'ultim item es "SENSE MANDO" per a
|
||||
// desasignar.
|
||||
[[nodiscard]] auto buildPadPickerPage(int player_index) -> 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);
|
||||
@@ -139,6 +159,20 @@ namespace System {
|
||||
float highlight_h_ = 0.0F;
|
||||
bool highlight_snap_ = true;
|
||||
|
||||
// Edge-detect de stick analogic per a navegacio. Una sola activacio
|
||||
// per direccio: cal tornar a centre (sota el llindar) per disparar
|
||||
// una altra. Compartit entre tots els pads — qualsevol jugador pot
|
||||
// navegar el menu.
|
||||
bool stick_left_held_ = false;
|
||||
bool stick_right_held_ = false;
|
||||
bool stick_up_held_ = false;
|
||||
bool stick_down_held_ = false;
|
||||
// Edge-detect dels triggers L2/R2 com a botons virtuals. SDL3 no
|
||||
// emet button events per als triggers; els llegim com a axis i
|
||||
// sintetitzem una pulsacio quan creuen el llindar.
|
||||
bool trigger_l2_held_ = false;
|
||||
bool trigger_r2_held_ = false;
|
||||
|
||||
static std::unique_ptr<ServiceMenu> instance;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// string_utils.hpp - Utilitats genèriques de cadenes
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// VectorText només admet ASCII en majúscules; les notificacions, el menú
|
||||
// de servei i l'overlay de rebind passen els textos dinàmics per aquest
|
||||
// helper abans de pintar-los.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
namespace Utils {
|
||||
|
||||
inline auto toUpperAscii(const std::string& s) -> std::string {
|
||||
std::string result = s;
|
||||
for (char& c : result) {
|
||||
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Utils
|
||||
@@ -373,12 +373,21 @@ namespace ConfigYaml {
|
||||
if (gp.contains("button_shoot")) {
|
||||
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_start")) {
|
||||
player1.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_menu")) {
|
||||
player1.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar nom del gamepad
|
||||
// Carregar nom i path del gamepad assignat
|
||||
if (p1.contains("gamepad_name")) {
|
||||
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
|
||||
}
|
||||
if (p1.contains("gamepad_path")) {
|
||||
player1.gamepad_path = p1["gamepad_path"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar controls del player 2 desde YAML
|
||||
@@ -421,12 +430,21 @@ namespace ConfigYaml {
|
||||
if (gp.contains("button_shoot")) {
|
||||
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_start")) {
|
||||
player2.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_menu")) {
|
||||
player2.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar nom del gamepad
|
||||
// Carregar nom i path del gamepad assignat
|
||||
if (p2.contains("gamepad_name")) {
|
||||
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
|
||||
}
|
||||
if (p2.contains("gamepad_path")) {
|
||||
player2.gamepad_path = p2["gamepad_path"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar configuración des del file YAML
|
||||
@@ -531,7 +549,10 @@ namespace ConfigYaml {
|
||||
file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n";
|
||||
file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n";
|
||||
file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n";
|
||||
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n";
|
||||
file << " button_start: " << buttonToString(player1.gamepad.button_start) << "\n";
|
||||
file << " button_menu: " << buttonToString(player1.gamepad.button_menu) << "\n";
|
||||
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n";
|
||||
file << " gamepad_path: \"" << player1.gamepad_path << "\" # Prioritari sobre name\n\n";
|
||||
}
|
||||
|
||||
// Guardar controls del player 2 a YAML
|
||||
@@ -548,7 +569,10 @@ namespace ConfigYaml {
|
||||
file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n";
|
||||
file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n";
|
||||
file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n";
|
||||
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n";
|
||||
file << " button_start: " << buttonToString(player2.gamepad.button_start) << "\n";
|
||||
file << " button_menu: " << buttonToString(player2.gamepad.button_menu) << "\n";
|
||||
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n";
|
||||
file << " gamepad_path: \"" << player2.gamepad_path << "\" # Prioritari sobre name\n\n";
|
||||
}
|
||||
|
||||
// Guardar configuración al file YAML
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace ConfigYaml {
|
||||
.key_start = SDL_SCANCODE_2,
|
||||
},
|
||||
.gamepad_name = "",
|
||||
.gamepad_path = "",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/graphics/shape_loader.hpp"
|
||||
#include "core/input/define_inputs.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/math/easing.hpp"
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
#include "core/system/scene_context.hpp"
|
||||
#include "core/system/service_menu.hpp"
|
||||
#include "project.h"
|
||||
|
||||
using SceneManager::SceneContext;
|
||||
@@ -324,8 +326,20 @@ void TitleScene::update(float delta_time) {
|
||||
break;
|
||||
}
|
||||
|
||||
handleSkipInput();
|
||||
handleStartInput();
|
||||
// Les animacions segueixen pero els inputs es bloquegen mentre el menu
|
||||
// de servei o l'overlay de redefinicio estiguin actius: en cas contrari,
|
||||
// SDL_GetKeyboardState i SDL_GetGamepadButton segueixen veient les tecles
|
||||
// pulsades i podrien disparar handleSkipInput/handleStartInput sense
|
||||
// intencio. Mateixa logica: per a GameScene tota la pausa es global,
|
||||
// pero a TitleScene nomes guardem els polls d'input.
|
||||
const auto* menu = System::ServiceMenu::get();
|
||||
const auto* di = System::DefineInputs::get();
|
||||
const bool INPUT_BLOCKED = (menu != nullptr && menu->isOpen()) ||
|
||||
(di != nullptr && di->isActive());
|
||||
if (!INPUT_BLOCKED) {
|
||||
handleSkipInput();
|
||||
handleStartInput();
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::updateStarfieldFadeInState(float delta_time) {
|
||||
|
||||
Reference in New Issue
Block a user