Compare commits

..

21 Commits

Author SHA1 Message Date
JailDesigner 99b18d208d chore: bump version a 0.8.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:41:25 +02:00
JailDesigner 1321566910 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.
2026-05-24 22:39:47 +02:00
JailDesigner cefafe99e4 feat(service_menu): triggers L2/R2 navegables + so al rebind
El menu de servei nomes processava AXIS_MOTION dels sticks i descartava
els triggers. Com SDL3 mai emet button events per a L2/R2 (nomes axis),
rebindar FIRE o ACCEL a un trigger feia que no funcionaren al menu, fins
i tot estant correctament al joc per via del poll de Input::checkTriggerInput.
Afegim edge-detect dels dos triggers al handleGamepadAxis i, quan creuen
el llindar, mirem si el codi virtual (100=L2, 101=R2) coincideix amb el
binding de FIRE → activateCurrent, o ACCEL → popPage. Estat held per
trigger per evitar repeticions mentre es mante premut.

DefineInputs ara reprodueix el so accept del menu en cada captura
valida, que estava silent i no donava feedback al rebind.

Tambe extraiem processStickX/Y i processTriggerEdge per mantenir
handleGamepadAxis com a dispatcher i sota el llindar de complexitat
cognitiva del clang-tidy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:38:10 +02:00
JailDesigner daa7eaf811 feat(service_menu): glyphs () + tanca picker al seleccionar mando
Afegim els glyphs ( i ) a VectorText (char_lparen.shp, char_rparen.shp,
arcs de 4 trams dins la caixa 20x40) perque el sufix (P1)/(P2) de la
picker de mando es renderitze net sense warnings.

A mes, al triar un mando o "SENSE MANDO" a la picker fem popPage
automatic, perque l'usuari no haja de tornar enrere a ma després
d'una assignacio.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:20:29 +02:00
JailDesigner 3dcf5c3a99 feat(service_menu): picker de mando per llista i fix SENSE MANDO
El cycle anterior fallava al desasignar perque Input::resolvePlayerGamepad
tenia un fallback per slot que reasignava gamepads_[player_index] quan
name+path eren buits. Això el contradeia el slot "SENSE MANDO" del cycle:
el YAML quedava buit pero el runtime seguia lligant el mando. Treure el
fallback i moure l'autoassignacio inicial al boot (nomes si tots dos
jugadors venen buits) restaura la semàntica: buit vol dir buit.

Sobre el fix, redissenyem la UX dels items MANDO P1/P2: ja no son CYCLE
sino SUBMENU que obrin una pàgina-llista (estil RESOLUCIÓ) amb tots els
mandos detectats. Cada item porta sufix (P1)/(P2) nomes si el mando el
te l'altre jugador, perque sapigues que assignar-lo li'l "robarà".
L'ultim item es "SENSE MANDO" per a desassignar explícitament. La
lògica de swap automatic en conflicte queda extreta a assignPadToPlayer
i es reutilitza des de la picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:12:53 +02:00
JailDesigner 99d0f62ab5 feat(service_menu): slot 'sense mando' al cycle i swap automatic en conflicte
El CYCLE de la pagina CONTROLS ara inclou un slot virtual al final que
desassigna el mando (gamepad_name + gamepad_path buits → padDisplayName
mostra "SENSE MANDO"). Aixi l'usuari pot recuperar el control teclat
sense haver d'editar el YAML.

A mes, si en assignar un mando l'altre jugador ja el tenia, fem swap
automatic: l'altre jugador rep l'assignacio previa d'aquest, evitant
que dos jugadors comparteixen el mateix dispositiu. La deteccio
prioritza path (mateixa branca que resolvePlayerGamepad).

Extracta tambe reapplyBindings per mantenir cyclePlayerPad llegible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:22:23 +02:00
JailDesigner 85050c8da4 fix(define-inputs): deixa passar QUIT i ESC al pipeline global
L'overlay de redefinicio engolia tots els events mentre estava actiu, fet
que impedia tancar la finestra amb l'aspa (SDL_EVENT_QUIT) i deixava
prendre ESC com a cancel-lacio del rebind. Ara:
- QUIT i WINDOW_CLOSE_REQUESTED passen sempre al global per tancar
  l'aplicacio des de l'aspa.
- ESC ja no cancel-la la sequencia; cau al global on obre el prompt
  d'eixida com a la resta del joc.
- isReservedScancode (ESC/F1-F12/RETURN/BACKSPACE/TAB) deixa passar.

Tambe ajusta DISPAR -> DISPARAR a ca.yaml i treu el hint "ESC PER
CANCEL-LAR" del modal i les claus de locale corresponents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:20:25 +02:00
JailDesigner 120c5502fd feat(vector-text): afegeix el glyph / al charset
El progres "i/n" del modal de redefinicio (ex. 1/4) sortia com a "14"
perque VectorText no tenia shape per a la barra i emetia un warning.
Afegim font/char_slash.shp (diagonal de baix-esquerra a dalt-dreta dins
de la caixa 20x40) i el registrem al loader i al getShapeFilename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:58:52 +02:00
JailDesigner 64a6599e81 fix(title): manten animacions amb menu obert, bloqueja nomes els polls d'input
El fix anterior pausava tot el title quan el menu de servei estava obert,
trencant l'efecte d'animacio de fons. Ara title segueix animant-se i
nomes guardem handleSkipInput/handleStartInput mentre el menu o el modal
de rebind estan actius, per evitar START fantasma sense congelar el render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:54:04 +02:00
JailDesigner a4b567588f feat(service_menu): navegacio amb mando (dpad, stick, fire = enter, accelerate = back)
ServiceMenu::handleEvent ara accepta tambe SDL_EVENT_GAMEPAD_BUTTON_DOWN
i SDL_EVENT_GAMEPAD_AXIS_MOTION. Mapeig: dpad UP/DOWN/LEFT/RIGHT mouen
el cursor, el boto FIRE configurat per qualsevol jugador equival a ENTER
(activa l'item), ACCELERATE equival a BACK (popPage). El stick esquerre
fa nav amb edge-detect: cal tornar a centre per disparar una altra entrada.
GlobalEvents::forwardToServiceMenu envia tots aquests events al menu
quan esta obert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:42:33 +02:00
JailDesigner 2e74fea2d5 feat(input): stick com a font alternativa de LEFT/RIGHT al mando
LEFT i RIGHT no son redefinibles al mando i s'assumeix dpad O stick.
Input::update() ara llegeix SDL_GAMEPAD_AXIS_LEFTX i fa OR amb l'estat
del dpad: qualsevol de les dos fonts dispara l'accio. Llindar 30000
(coherent amb el constant AXIS_THRESHOLD ja existent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:38:26 +02:00
JailDesigner c4933875dd fix(input): impedeix que els events traspassin al joc en acabar el rebind
El menu de servei queda obert per sota de l'overlay DefineInputs durant
tot el rebind (en lloc de tancar-se al activar la accio), de manera que
absorbeix qualsevol KEY_DOWN que arribi un cop l'overlay s'auto-cancela.
La pantalla de titol tambe pausa la seua logica mentre el menu de servei
esta obert, igual que GameScene, per evitar que detecti un START fantasma
si l'usuari encara te una tecla pulsada al moment de tancar-se el modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:36:51 +02:00
JailDesigner 10a54aef91 fix(ui): nom del mando en majuscules a la UI sense modificar el config
VectorText nomes admet ASCII en majuscules; els noms dels mandos (i el
git hash) passaven pel toUpperAscii local del service_menu, pero les
notificacions de hot-plug i el text del CYCLE de la pagina CONTROLS
es mostraven amb el case original. Mou el helper a un utils compartit i
l'aplica a tots els punts de display sense tocar gamepad_name al config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:33:01 +02:00
JailDesigner 34be79192c feat(service_menu): pagina CONTROLS amb assignacio de pad i rebind per jugador
Afegeix submenu CONTROLS al menu de servei amb 2 items CYCLE per
seleccionar el mando assignat a cada jugador (persistit per name + path)
i 4 items ACTION per arrancar DefineInputs (teclat/mando per a P1/P2).

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:18:49 +02:00
JailDesigner fcf13591be feat(input): modul DefineInputs per redefinir teclat i mando
Singleton inspirat en aee_arcade DefineButtons: pinta una caixa central
modal, captura events SDL i avança per una sequencia fixa d'accions per
jugador. Teclat: LEFT/RIGHT/FIRE/ACCELERATE. Mando: FIRE/ACCELERATE/
START/MENU. ESC cancel-la, duplicats dins la sessio es rebutgen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:17:30 +02:00
JailDesigner 3e8f2f35bf feat(input): accio MENU i assignacio de mando per path + name
Afegeix l'accio MENU a InputAction (obre el menu de servei des del mando,
equivalent a F12 al teclat) i els camps gamepad.button_start i
gamepad.button_menu al config per jugador. Tambe afegeix gamepad_path
per distingir dos mandos del mateix model i prioritza path > name >
slot a applyPlayerNBindings via el nou resolvePlayerGamepad.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:56:59 +02:00
JailDesigner e5a91825b1 feat(input): notifica connexio/desconnexio de mandos via Notifier
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:55:42 +02:00
JailDesigner b3271b17a2 Merge branch 'feat/preload-resources': precàrrega completa al boot 2026-05-24 19:32:04 +02:00
JailDesigner d4117e3505 feat(boot): precàrrega de música, sons i shapes a l'arrencada 2026-05-24 19:31:35 +02:00
JailDesigner 73c7e4ea76 Merge branch 'fix/fps-rounding': FPS arrodonit 2026-05-24 19:20:14 +02:00
JailDesigner 23cc5ce68d fix(debug-hud): FPS arrodonit en lloc de truncat 2026-05-24 19:20:06 +02:00
28 changed files with 1544 additions and 239 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) 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) # Info del projecte (font de veritat per a project.h)
set(PROJECT_LONG_NAME "Orni Attack") set(PROJECT_LONG_NAME "Orni Attack")
+28
View File
@@ -14,6 +14,8 @@ notification:
postfx_on: "POSTPROCESSAT ACTIU" postfx_on: "POSTPROCESSAT ACTIU"
postfx_off: "POSTPROCESSAT INACTIU" postfx_off: "POSTPROCESSAT INACTIU"
locale_switched: "IDIOMA: {lang}" locale_switched: "IDIOMA: {lang}"
gamepad_connected: "{name} CONNECTAT"
gamepad_disconnected: "{name} DESCONNECTAT"
language: language:
ca: "CATALA" ca: "CATALA"
@@ -82,3 +84,29 @@ service_menu:
# Valors comuns # Valors comuns
value_on: "ACTIU" value_on: "ACTIU"
value_off: "INACTIU" 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"
+28
View File
@@ -13,6 +13,8 @@ notification:
postfx_on: "POSTPROCESS ON" postfx_on: "POSTPROCESS ON"
postfx_off: "POSTPROCESS OFF" postfx_off: "POSTPROCESS OFF"
locale_switched: "LANGUAGE: {lang}" locale_switched: "LANGUAGE: {lang}"
gamepad_connected: "{name} CONNECTED"
gamepad_disconnected: "{name} DISCONNECTED"
language: language:
ca: "CATALAN" ca: "CATALAN"
@@ -81,3 +83,29 @@ service_menu:
# Common values # Common values
value_on: "ON" value_on: "ON"
value_off: "OFF" 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"
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+3
View File
@@ -48,12 +48,15 @@ namespace Config {
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT}; int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A 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 { struct PlayerBindings {
KeyboardBindings keyboard{}; KeyboardBindings keyboard{};
GamepadBindings gamepad{}; GamepadBindings gamepad{};
std::string gamepad_name; // Empty = auto-assign by index std::string gamepad_name; // Empty = auto-assign by index
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
}; };
struct AudioConfig { struct AudioConfig {
+7 -1
View File
@@ -47,7 +47,7 @@ namespace Graphics {
} }
// Cargar símbolos // Cargar símbolos
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"}; const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"};
for (const auto& sym : SYMBOLS) { for (const auto& sym : SYMBOLS) {
char c = sym[0]; char c = sym[0];
std::string filename = getShapeFilename(c); std::string filename = getShapeFilename(c);
@@ -164,6 +164,12 @@ namespace Graphics {
return "font/char_exclamation.shp"; return "font/char_exclamation.shp";
case '?': case '?':
return "font/char_question.shp"; return "font/char_question.shp";
case '/':
return "font/char_slash.shp";
case '(':
return "font/char_lparen.shp";
case ')':
return "font/char_rparen.shp";
case ' ': case ' ':
return ""; // Espai es maneja sin load shape return ""; // Espai es maneja sin load shape
+1 -1
View File
@@ -21,7 +21,7 @@ namespace Graphics {
// Renderizar string completo // Renderizar string completo
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':', // - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
// '!', '?', ' ') // '!', '?', '/', '(', ')', ' ')
// - position: posición inicial (esquina superior izquierda) // - position: posición inicial (esquina superior izquierda)
// - scale: factor de scale (1.0 = 20×40 px por carácter) // - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a scale 1.0) // - spacing: espacio entre caracteres en píxeles (a scale 1.0)
+405
View File
@@ -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
+107
View File
@@ -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
View File
@@ -8,6 +8,10 @@
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator #include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
#include <utility> // Para move #include <utility> // Para move
#include "core/locale/locale.hpp"
#include "core/system/notifier.hpp"
#include "core/utils/string_utils.hpp"
// Singleton // Singleton
Input* Input::instance = nullptr; Input* Input::instance = nullptr;
@@ -373,9 +377,25 @@ void Input::update() {
// --- MANDOS --- // --- MANDOS ---
for (const auto& gamepad : gamepads_) { 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) { 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; 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 // 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.just_pressed = button_is_down_now && !binding.second.is_held;
binding.second.is_held = button_is_down_now; binding.second.is_held = button_is_down_now;
@@ -407,6 +427,16 @@ auto Input::addGamepad(int device_index) -> std::string {
auto name = gamepad->name; auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n'; std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad)); 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"; return name + " CONNECTED";
} }
@@ -419,6 +449,14 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
std::string name = (*it)->name; std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n'; std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it); 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"; return name + " DISCONNECTED";
} }
std::cerr << "No se encontró el gamepad con ID " << id << '\n'; 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) ========== // ========== 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 // Aplica configuración de controles del player 1
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) { void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico) // 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::SHOOT].scancode = bindings.keyboard.key_shoot;
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback) // 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = nullptr; std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
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);
}
if (!gamepad) { if (!gamepad) {
player1_gamepad_ = nullptr; 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::RIGHT].button = bindings.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; 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 // 4. Cachear referencia
player1_gamepad_ = gamepad; 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::SHOOT].scancode = bindings.keyboard.key_shoot;
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback) // 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = nullptr; std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
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);
}
if (!gamepad) { if (!gamepad) {
player2_gamepad_ = nullptr; 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::RIGHT].button = bindings.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; 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 // 4. Cachear referencia
player2_gamepad_ = gamepad; player2_gamepad_ = gamepad;
@@ -555,6 +610,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
return keyboard_active || gamepad_active; 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 // Consulta de input para player 2
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool { auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
// Comprobar teclado con el mapa específico de P2 // Comprobar teclado con el mapa específico de P2
+8 -1
View File
@@ -62,7 +62,9 @@ class Input {
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}}, {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::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}, {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() { ~Gamepad() {
if (pad != nullptr) { if (pad != nullptr) {
@@ -107,6 +109,10 @@ class Input {
auto checkActionPlayer1(Action action, bool repeat = true) -> bool; auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
auto checkActionPlayer2(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 // 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; 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; auto removeGamepad(SDL_JoystickID id) -> std::string;
void addGamepadMappingsFromFile(); void addGamepadMappingsFromFile();
void discoverGamepads(); void discoverGamepads();
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
// --- Variables miembro --- // --- Variables miembro ---
static Input* instance; // Instancia única del singleton static Input* instance; // Instancia única del singleton
+4
View File
@@ -6,6 +6,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
{InputAction::RIGHT, "RIGHT"}, {InputAction::RIGHT, "RIGHT"},
{InputAction::THRUST, "THRUST"}, {InputAction::THRUST, "THRUST"},
{InputAction::SHOOT, "SHOOT"}, {InputAction::SHOOT, "SHOOT"},
{InputAction::START, "START"},
{InputAction::MENU, "MENU"},
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"}, {InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"}, {InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"}, {InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
@@ -18,6 +20,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"RIGHT", InputAction::RIGHT}, {"RIGHT", InputAction::RIGHT},
{"THRUST", InputAction::THRUST}, {"THRUST", InputAction::THRUST},
{"SHOOT", InputAction::SHOOT}, {"SHOOT", InputAction::SHOOT},
{"START", InputAction::START},
{"MENU", InputAction::MENU},
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM}, {"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM}, {"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN}, {"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
+1
View File
@@ -15,6 +15,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j
THRUST, // Acelerar THRUST, // Acelerar
SHOOT, // Disparar SHOOT, // Disparar
START, // Empezar match START, // Empezar match
MENU, // Abrir/cerrar menu de servicio (equivalent a F12)
// Inputs de sistema (globales) // Inputs de sistema (globales)
WINDOW_INC_ZOOM, // F2 WINDOW_INC_ZOOM, // F2
+24 -19
View File
@@ -9,29 +9,34 @@
namespace Resource::Helper { namespace Resource::Helper {
// Inicialitzar el sistema de recursos // Inicialitzar el sistema de recursos
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool { auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
return Loader::get().initialize(pack_file, fallback); return Loader::get().initialize(pack_file, fallback);
} }
// Carregar un file // Carregar un file
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> { auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
// Normalitzar la ruta // Normalitzar la ruta
std::string normalized = normalizePath(filepath); std::string normalized = normalizePath(filepath);
// Carregar del sistema de recursos // Carregar del sistema de recursos
return Loader::get().loadResource(normalized); return Loader::get().loadResource(normalized);
} }
// Comprovar si existeix un file // Llistar recursos amb un prefix donat
auto fileExists(const std::string& filepath) -> bool { auto listResources(const std::string& prefix) -> std::vector<std::string> {
return Loader::get().listResources(prefix);
}
// Comprovar si existeix un file
auto fileExists(const std::string& filepath) -> bool {
std::string normalized = normalizePath(filepath); std::string normalized = normalizePath(filepath);
return Loader::get().resourceExists(normalized); return Loader::get().resourceExists(normalized);
} }
// Obtenir ruta normalitzada per al paquet // Obtenir ruta normalitzada per al paquet
// Elimina prefixos "data/", rutes absolutes, etc. // Elimina prefixos "data/", rutes absolutes, etc.
auto getPackPath(const std::string& asset_path) -> std::string { auto getPackPath(const std::string& asset_path) -> std::string {
std::string path = asset_path; std::string path = asset_path;
// Eliminar rutes absolutes (detectar / o C:\ al principi) // Eliminar rutes absolutes (detectar / o C:\ al principi)
@@ -65,16 +70,16 @@ auto getPackPath(const std::string& asset_path) -> std::string {
std::ranges::replace(path, '\\', '/'); std::ranges::replace(path, '\\', '/');
return path; return path;
} }
// Normalitzar ruta (alias de getPackPath) // Normalitzar ruta (alias de getPackPath)
auto normalizePath(const std::string& path) -> std::string { auto normalizePath(const std::string& path) -> std::string {
return getPackPath(path); return getPackPath(path);
} }
// Comprovar si hay paquet carregat // Comprovar si hay paquet carregat
auto isPackLoaded() -> bool { auto isPackLoaded() -> bool {
return Loader::get().isPackLoaded(); return Loader::get().isPackLoaded();
} }
} // namespace Resource::Helper } // namespace Resource::Helper
+13 -10
View File
@@ -10,18 +10,21 @@
namespace Resource::Helper { namespace Resource::Helper {
// Inicialización del sistema // Inicialización del sistema
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool; auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
// Càrrega de archivos // Càrrega de archivos
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>; auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
auto fileExists(const std::string& filepath) -> bool; auto fileExists(const std::string& filepath) -> bool;
// Normalització de rutes // Llistat de recursos disponibles amb un prefix (ex. "shapes/", "sounds/").
auto getPackPath(const std::string& asset_path) -> std::string; auto listResources(const std::string& prefix) -> std::vector<std::string>;
auto normalizePath(const std::string& path) -> std::string;
// Estat // Normalització de rutes
auto isPackLoaded() -> bool; auto getPackPath(const std::string& asset_path) -> std::string;
auto normalizePath(const std::string& path) -> std::string;
// Estat
auto isPackLoaded() -> bool;
} // namespace Resource::Helper } // namespace Resource::Helper
+64 -27
View File
@@ -3,20 +3,21 @@
#include "resource_loader.hpp" #include "resource_loader.hpp"
#include <algorithm>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
namespace Resource { namespace Resource {
// Singleton // Singleton
auto Loader::get() -> Loader& { auto Loader::get() -> Loader& {
static Loader instance_; static Loader instance_;
return instance_; return instance_;
} }
// Inicialitzar el sistema de recursos // Inicialitzar el sistema de recursos
auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool { auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
fallback_enabled_ = enable_fallback; fallback_enabled_ = enable_fallback;
// Intentar load el paquet // Intentar load el paquet
@@ -36,10 +37,10 @@ auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> b
std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n"; std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n";
return true; return true;
} }
// Carregar un recurs // Carregar un recurs
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> { auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
// Intentar load del paquet primer // Intentar load del paquet primer
if (pack_) { if (pack_) {
if (pack_->hasResource(filename)) { if (pack_->hasResource(filename)) {
@@ -65,10 +66,46 @@ auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
} }
return {}; return {};
} }
// Comprovar si existeix un recurs auto Loader::listResources(const std::string& prefix) -> std::vector<std::string> {
auto Loader::resourceExists(const std::string& filename) -> bool { std::vector<std::string> result;
if (pack_) {
for (const auto& path : pack_->getResourceList()) {
if (path.starts_with(prefix)) {
result.push_back(path);
}
}
return result;
}
if (!fallback_enabled_) {
return result;
}
std::string root = base_path_.empty() ? "data/" + prefix : base_path_ + "/data/" + prefix;
if (!std::filesystem::exists(root)) {
return result;
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) {
if (!entry.is_regular_file()) {
continue;
}
std::string full = entry.path().generic_string();
if (auto pos = full.find("/data/"); pos != std::string::npos) {
result.push_back(full.substr(pos + 6));
} else if (full.starts_with("data/")) {
result.push_back(full.substr(5));
}
}
std::ranges::sort(result);
return result;
}
// Comprovar si existeix un recurs
auto Loader::resourceExists(const std::string& filename) -> bool {
// Comprovar al paquet // Comprovar al paquet
if (pack_ && pack_->hasResource(filename)) { if (pack_ && pack_->hasResource(filename)) {
return true; return true;
@@ -81,36 +118,36 @@ auto Loader::resourceExists(const std::string& filename) -> bool {
} }
return false; return false;
} }
// Validar el paquet // Validar el paquet
auto Loader::validatePack() -> bool { auto Loader::validatePack() -> bool {
if (!pack_) { if (!pack_) {
std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n"; std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n";
return false; return false;
} }
return pack_->validatePack(); return pack_->validatePack();
} }
// Comprovar si hay paquet carregat // Comprovar si hay paquet carregat
auto Loader::isPackLoaded() const -> bool { auto Loader::isPackLoaded() const -> bool {
return pack_ != nullptr; return pack_ != nullptr;
} }
// Establir la ruta base // Establir la ruta base
void Loader::setBasePath(const std::string& path) { void Loader::setBasePath(const std::string& path) {
base_path_ = path; base_path_ = path;
std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n"; std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
} }
// Obtenir la ruta base // Obtenir la ruta base
auto Loader::getBasePath() const -> const std::string& { auto Loader::getBasePath() const -> const std::string& {
return base_path_; return base_path_;
} }
// Carregar des del sistema de archivos (fallback) // Carregar des del sistema de archivos (fallback)
auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> { auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> {
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp") // The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
// We need to prepend base_path + "data/" // We need to prepend base_path + "data/"
std::string fullpath; std::string fullpath;
@@ -138,6 +175,6 @@ auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint
std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n"; std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n";
return data; return data;
} }
} // namespace Resource } // namespace Resource
+8 -3
View File
@@ -12,8 +12,8 @@
namespace Resource { namespace Resource {
// Singleton per gestionar la càrrega de recursos // Singleton per gestionar la càrrega de recursos
class Loader { class Loader {
public: public:
// Singleton // Singleton
static auto get() -> Loader&; static auto get() -> Loader&;
@@ -25,6 +25,11 @@ class Loader {
auto loadResource(const std::string& filename) -> std::vector<uint8_t>; auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
auto resourceExists(const std::string& filename) -> bool; auto resourceExists(const std::string& filename) -> bool;
// Llistat de recursos amb prefix (ex. "shapes/", "sounds/"). Si hi ha
// pack, retorna els fitxers del pack filtrats; si no, escaneja el
// sistema de fitxers recursivament a `data/<prefix>`.
auto listResources(const std::string& prefix) -> std::vector<std::string>;
// Validació // Validació
auto validatePack() -> bool; auto validatePack() -> bool;
[[nodiscard]] auto isPackLoaded() const -> bool; [[nodiscard]] auto isPackLoaded() const -> bool;
@@ -48,6 +53,6 @@ class Loader {
// Funciones auxiliars // Funciones auxiliars
auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>; auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>;
}; };
} // namespace Resource } // namespace Resource
+2 -1
View File
@@ -5,6 +5,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cctype> #include <cctype>
#include <cmath>
#include <string> #include <string>
#include "core/defaults.hpp" #include "core/defaults.hpp"
@@ -35,7 +36,7 @@ namespace System {
fps_frame_count_++; fps_frame_count_++;
if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) { if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) {
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_); fps_display_ = static_cast<int>(std::lround(static_cast<float>(fps_frame_count_) / fps_accumulator_));
fps_frame_count_ = 0; fps_frame_count_ = 0;
fps_accumulator_ = 0.0F; fps_accumulator_ = 0.0F;
} }
+58 -4
View File
@@ -12,6 +12,8 @@
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/audio/audio_adapter.hpp" #include "core/audio/audio_adapter.hpp"
#include "core/defaults/window.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/input.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
@@ -107,6 +109,34 @@ Director::Director(int argc, char* argv[])
// Inicialitzar sistema de input // Inicialitzar sistema de input
Input::init("data/gamecontrollerdb.txt"); 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 // Aplicar configuración de controls dels jugadors
Input::get()->applyPlayer1Bindings(cfg_->player1); Input::get()->applyPlayer1Bindings(cfg_->player1);
Input::get()->applyPlayer2Bindings(cfg_->player2); Input::get()->applyPlayer2Bindings(cfg_->player2);
@@ -147,10 +177,21 @@ Director::Director(int argc, char* argv[])
Audio::init(AUDIO_CONFIG); Audio::init(AUDIO_CONFIG);
Audio::get()->applySettings(AUDIO_CONFIG); Audio::get()->applySettings(AUDIO_CONFIG);
AudioResource::getMusic("title.ogg"); // Precàrrega blocant de tots els recursos al boot per evitar hits d'I/O i
AudioResource::getMusic("game.ogg"); // de decodificació en transicions (TITLE → GAME, primera explosió, etc.).
// Mateix patró que aee_arcade: iterem `listResources` i forcem la càrrega
// al cache de cada subsistema.
for (const auto& path : Resource::Helper::listResources("music/")) {
AudioResource::getMusic(path.substr(std::string_view{"music/"}.size()));
}
for (const auto& path : Resource::Helper::listResources("sounds/")) {
AudioResource::getSound(path.substr(std::string_view{"sounds/"}.size()));
}
for (const auto& path : Resource::Helper::listResources("shapes/")) {
Graphics::ShapeLoader::load(path.substr(std::string_view{"shapes/"}.size()));
}
if (cfg_->console) { if (cfg_->console) {
std::cout << "Música precacheada\n"; std::cout << "Recursos precachejats (música, sons, shapes)\n";
} }
context_ = std::make_unique<SceneContext>(); context_ = std::make_unique<SceneContext>();
@@ -166,6 +207,7 @@ Director::Director(int argc, char* argv[])
System::Notifier::init(sdl_->getRenderer()); System::Notifier::init(sdl_->getRenderer());
System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get()); System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get());
System::DefineInputs::init(sdl_->getRenderer());
last_ticks_ms_ = SDL_GetTicks(); last_ticks_ms_ = SDL_GetTicks();
} }
@@ -180,6 +222,7 @@ Director::~Director() {
// l'hem de cridar nosaltres. // l'hem de cridar nosaltres.
current_scene_.reset(); current_scene_.reset();
debug_overlay_.reset(); debug_overlay_.reset();
System::DefineInputs::destroy();
System::ServiceMenu::destroy(); System::ServiceMenu::destroy();
System::Notifier::destroy(); System::Notifier::destroy();
context_.reset(); context_.reset();
@@ -364,6 +407,9 @@ auto Director::iterate() -> SDL_AppResult {
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) { if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
menu->update(delta_time); menu->update(delta_time);
} }
if (auto* di = System::DefineInputs::get(); di != nullptr) {
di->update(delta_time);
}
Audio::update(); Audio::update();
// Si la swapchain no està disponible (finestra minimitzada, etc.), // Si la swapchain no està disponible (finestra minimitzada, etc.),
@@ -377,9 +423,17 @@ auto Director::iterate() -> SDL_AppResult {
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) { if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->draw(); // toast: per damunt de tot 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 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(); sdl_->present();
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
} }
+68 -6
View File
@@ -5,6 +5,7 @@
#include <iostream> #include <iostream>
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
@@ -22,17 +23,17 @@ namespace GlobalEvents {
namespace { namespace {
// Reenvia el KEY_DOWN al menu de servei si esta obert i la tecla no // Reenvia events al menu de servei si esta obert. Accepta:
// es F1-F12 ni ESC (que sempre passen com a globals). Retorna true si // - KEY_DOWN (excepte F1-F12 i ESC, que sempre passen com a globals)
// el menu l'ha consumit. // - 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 { auto forwardToServiceMenu(const SDL_Event& event) -> bool {
if (event.type != SDL_EVENT_KEY_DOWN) {
return false;
}
auto* menu = System::ServiceMenu::get(); auto* menu = System::ServiceMenu::get();
if (menu == nullptr || !menu->isOpen()) { if (menu == nullptr || !menu->isOpen()) {
return false; return false;
} }
if (event.type == SDL_EVENT_KEY_DOWN) {
const SDL_Scancode SC = event.key.scancode; const SDL_Scancode SC = event.key.scancode;
const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) || const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) ||
(SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12); (SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12);
@@ -42,6 +43,56 @@ namespace GlobalEvents {
menu->handleEvent(event); menu->handleEvent(event);
return true; 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;
}
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;
}
} // namespace } // namespace
@@ -52,6 +103,12 @@ namespace GlobalEvents {
std::cout << "[Input] " << event_msg << '\n'; 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) // 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
if (event.type == SDL_EVENT_QUIT) { if (event.type == SDL_EVENT_QUIT) {
context.setNextScene(SceneType::EXIT); context.setNextScene(SceneType::EXIT);
@@ -62,6 +119,11 @@ namespace GlobalEvents {
// 3. Gestió del ratolí (auto-ocultar) // 3. Gestió del ratolí (auto-ocultar)
Mouse::handleEvent(event); 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 // 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de
// funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen, // funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen,
// vsync, AA, postfx, locale, exit prompt). Aixi el menu captura // vsync, AA, postfx, locale, exit prompt). Aixi el menu captura
+374 -14
View File
@@ -15,11 +15,15 @@
#include "core/defaults/audio.hpp" #include "core/defaults/audio.hpp"
#include "core/defaults/rendering.hpp" #include "core/defaults/rendering.hpp"
#include "core/defaults/service_menu.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/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp" #include "core/rendering/sdl_manager.hpp"
#include "core/system/debug_overlay.hpp" #include "core/system/debug_overlay.hpp"
#include "core/system/notifier.hpp"
#include "core/system/relaunch.hpp" #include "core/system/relaunch.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "core/utils/string_utils.hpp"
#include "game/config_yaml.hpp" #include "game/config_yaml.hpp"
#include "project.h" #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 // Resol el text del label d'un item: prioritza label_text (literal) sobre
// label_key (locale). Retorna cadena buida si tots dos son buits. // label_key (locale). Retorna cadena buida si tots dos son buits.
auto resolveLabel(const System::ServiceMenu::Item& item) -> std::string { auto resolveLabel(const System::ServiceMenu::Item& item) -> std::string {
@@ -77,6 +70,107 @@ namespace {
return Locale::get().text(item.label_key); 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
namespace System { namespace System {
@@ -153,6 +247,7 @@ namespace System {
makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }), makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }),
makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }), makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }),
makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }), makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }),
makeSubmenu("service_menu.controls", [this] { pushPage(buildControlsPage()); }),
makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }), makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }),
}; };
stack_.clear(); stack_.clear();
@@ -437,13 +532,138 @@ namespace System {
return page; 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 { auto ServiceMenu::buildSystemPage() -> Page {
Page page; Page page;
page.title_key = "service_menu.system"; page.title_key = "service_menu.system";
// Versio + hash com a subtitol sota el titol (apagat, mes petit). // Versio + hash com a subtitol sota el titol (apagat, mes petit).
// Uppercase del hash perque VectorText nomes admet majuscules. // Uppercase del hash perque VectorText nomes admet majuscules.
page.subtitle_provider = [] { 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 = { page.items = {
// REINICIAR (amb confirmacio). // REINICIAR (amb confirmacio).
@@ -593,10 +813,55 @@ namespace System {
} }
} }
auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool { namespace {
if (!open_ || stack_.empty() || event.type != SDL_EVENT_KEY_DOWN) {
// 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; 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) { switch (event.key.scancode) {
case SDL_SCANCODE_UP: case SDL_SCANCODE_UP:
moveCursor(-1); 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 { auto ServiceMenu::computeTargetHeight() const -> float {
if (stack_.empty()) { if (stack_.empty()) {
return 0.0F; return 0.0F;
+36 -2
View File
@@ -85,19 +85,39 @@ namespace System {
void update(float delta_time); void update(float delta_time);
void draw() const; void draw() const;
// Processa el KEY_DOWN. Retorna true si l'ha consumit (UP/DOWN/ENTER/ // Processa events de navegacio. Retorna true si l'event s'ha consumit.
// RIGHT/BACKSPACE/LEFT mentre esta obert). false en qualsevol altre cas. // 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; auto handleEvent(const SDL_Event& event) -> bool;
private: private:
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay); 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(); void buildRootPage();
[[nodiscard]] auto buildVideoPage() -> Page; [[nodiscard]] auto buildVideoPage() -> Page;
[[nodiscard]] auto buildResolutionPage() const -> Page; [[nodiscard]] auto buildResolutionPage() const -> Page;
[[nodiscard]] static auto buildAudioPage() -> Page; [[nodiscard]] static auto buildAudioPage() -> Page;
[[nodiscard]] auto buildOptionsPage() const -> Page; [[nodiscard]] auto buildOptionsPage() const -> Page;
[[nodiscard]] auto buildSystemPage() -> 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 // Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
// l'usuari selecciona SI; el cursor per defecte apunta a NO. // l'usuari selecciona SI; el cursor per defecte apunta a NO.
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes); void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
@@ -139,6 +159,20 @@ namespace System {
float highlight_h_ = 0.0F; float highlight_h_ = 0.0F;
bool highlight_snap_ = true; 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; static std::unique_ptr<ServiceMenu> instance;
}; };
+23
View File
@@ -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
+28 -4
View File
@@ -373,12 +373,21 @@ namespace ConfigYaml {
if (gp.contains("button_shoot")) { if (gp.contains("button_shoot")) {
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>()); 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")) { if (p1.contains("gamepad_name")) {
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>(); 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 // Carregar controls del player 2 desde YAML
@@ -421,12 +430,21 @@ namespace ConfigYaml {
if (gp.contains("button_shoot")) { if (gp.contains("button_shoot")) {
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>()); 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")) { if (p2.contains("gamepad_name")) {
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>(); 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 // Carregar configuración des del file YAML
@@ -531,7 +549,10 @@ namespace ConfigYaml {
file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n"; file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n";
file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n"; file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n";
file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\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 // Guardar controls del player 2 a YAML
@@ -548,7 +569,10 @@ namespace ConfigYaml {
file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n"; file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n";
file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n"; file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n";
file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\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 // Guardar configuración al file YAML
+1
View File
@@ -28,6 +28,7 @@ namespace ConfigYaml {
.key_start = SDL_SCANCODE_2, .key_start = SDL_SCANCODE_2,
}, },
.gamepad_name = "", .gamepad_name = "",
.gamepad_path = "",
}, },
}; };
+14
View File
@@ -13,11 +13,13 @@
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp" #include "core/graphics/shape_loader.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/math/easing.hpp" #include "core/math/easing.hpp"
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/system/scene_context.hpp" #include "core/system/scene_context.hpp"
#include "core/system/service_menu.hpp"
#include "project.h" #include "project.h"
using SceneManager::SceneContext; using SceneManager::SceneContext;
@@ -324,8 +326,20 @@ void TitleScene::update(float delta_time) {
break; break;
} }
// 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(); handleSkipInput();
handleStartInput(); handleStartInput();
}
} }
void TitleScene::updateStarfieldFadeInState(float delta_time) { void TitleScene::updateStarfieldFadeInState(float delta_time) {