Compare commits
17 Commits
b3271b17a2
...
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 99b18d208d | |||
| 1321566910 | |||
| cefafe99e4 | |||
| daa7eaf811 | |||
| 3dcf5c3a99 | |||
| 99d0f62ab5 | |||
| 85050c8da4 | |||
| 120c5502fd | |||
| 64a6599e81 | |||
| a4b567588f | |||
| 2e74fea2d5 | |||
| c4933875dd | |||
| 10a54aef91 | |||
| 34be79192c | |||
| fcf13591be | |||
| 3e8f2f35bf | |||
| e5a91825b1 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(orni VERSION 0.7.2 LANGUAGES CXX)
|
||||
project(orni VERSION 0.8.0 LANGUAGES CXX)
|
||||
|
||||
# Info del projecte (font de veritat per a project.h)
|
||||
set(PROJECT_LONG_NAME "Orni Attack")
|
||||
|
||||
@@ -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