Compare commits
37 Commits
274ce1ca63
..
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 99b18d208d | |||
| 1321566910 | |||
| cefafe99e4 | |||
| daa7eaf811 | |||
| 3dcf5c3a99 | |||
| 99d0f62ab5 | |||
| 85050c8da4 | |||
| 120c5502fd | |||
| 64a6599e81 | |||
| a4b567588f | |||
| 2e74fea2d5 | |||
| c4933875dd | |||
| 10a54aef91 | |||
| 34be79192c | |||
| fcf13591be | |||
| 3e8f2f35bf | |||
| e5a91825b1 | |||
| b3271b17a2 | |||
| d4117e3505 | |||
| 73c7e4ea76 | |||
| 23cc5ce68d | |||
| e42059e486 | |||
| 00f40d194b | |||
| 31f348328e | |||
| 8c48a9a772 | |||
| bacfbe6eac | |||
| 63d08aef46 | |||
| 87f818ef96 | |||
| 7eafe21623 | |||
| 22827c28fa | |||
| 8c21345f14 | |||
| 56d7d4af52 | |||
| 71c43ec6fe | |||
| 443b461974 | |||
| cc16908b86 | |||
| c4c6881bd6 | |||
| 35d720bb77 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(orni VERSION 0.7.2 LANGUAGES CXX)
|
||||
project(orni VERSION 0.8.0 LANGUAGES CXX)
|
||||
|
||||
# Info del projecte (font de veritat per a project.h)
|
||||
set(PROJECT_LONG_NAME "Orni Attack")
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Orni Attack - locale: Catala (valencia)
|
||||
# Interficie traduida; pool in-game identic a en.yaml (es queda en angles).
|
||||
# Tots els textos en ASCII: VectorText no suporta caracters accentuats.
|
||||
|
||||
notification:
|
||||
press_again_exit: "PREMEU ESC UN ALTRE COP PER EIXIR"
|
||||
zoom: "ZOOM: {z}X"
|
||||
fullscreen_on: "PANTALLA COMPLETA"
|
||||
fullscreen_off: "MODE FINESTRA"
|
||||
vsync_on: "VSYNC ACTIU"
|
||||
vsync_off: "VSYNC INACTIU"
|
||||
antialias_on: "AA ACTIU"
|
||||
antialias_off: "AA INACTIU"
|
||||
postfx_on: "POSTPROCESSAT ACTIU"
|
||||
postfx_off: "POSTPROCESSAT INACTIU"
|
||||
locale_switched: "IDIOMA: {lang}"
|
||||
gamepad_connected: "{name} CONNECTAT"
|
||||
gamepad_disconnected: "{name} DESCONNECTAT"
|
||||
|
||||
language:
|
||||
ca: "CATALA"
|
||||
en: "ANGLES"
|
||||
|
||||
hud:
|
||||
level: "NIVELL "
|
||||
|
||||
title:
|
||||
press_start: "PREMEU START PER JUGAR"
|
||||
|
||||
game_screen:
|
||||
game_over: "FI DEL JOC"
|
||||
continue: "CONTINUAR"
|
||||
continues_left: "CONTINUACIONS: {n}"
|
||||
|
||||
stage:
|
||||
start:
|
||||
- "ORNI ALERT!"
|
||||
- "INCOMING ORNIS!"
|
||||
- "ROLLING THREAT!"
|
||||
- "ENEMY WAVE!"
|
||||
- "WAVE OF ORNIS DETECTED!"
|
||||
- "NEXT SWARM APPROACHING!"
|
||||
- "BRACE FOR THE NEXT WAVE!"
|
||||
- "ANOTHER ATTACK INCOMING!"
|
||||
- "SENSORS DETECT HOSTILE ORNIS..."
|
||||
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
|
||||
- "ENEMY FORCES MOBILIZING!"
|
||||
- "PREPARE FOR IMPACT!"
|
||||
completed: "GOOD JOB COMMANDER!"
|
||||
|
||||
service_menu:
|
||||
title: "MENU DE SERVEI"
|
||||
video: "VIDEO"
|
||||
audio: "AUDIO"
|
||||
options: "OPCIONS"
|
||||
system: "SISTEMA"
|
||||
controls: "CONTROLS"
|
||||
back: "ENRERE"
|
||||
exit: "EIXIR DEL JOC"
|
||||
# Items del submenu VIDEO
|
||||
video_zoom: "ZOOM"
|
||||
video_fullscreen: "PANTALLA COMPLETA"
|
||||
video_vsync: "VSYNC"
|
||||
video_aa: "ANTIALIAS"
|
||||
video_postfx: "POSTPROCESSAT"
|
||||
video_resolution: "RESOLUCIO"
|
||||
# Items del submenu OPCIONS
|
||||
options_language: "IDIOMA"
|
||||
options_show_info: "MOSTRAR INFO"
|
||||
# Items del submenu AUDIO
|
||||
audio_master: "AUDIO"
|
||||
audio_master_volume: "VOLUM GENERAL"
|
||||
audio_music: "MUSICA"
|
||||
audio_music_volume: "VOLUM MUSICA"
|
||||
audio_sound: "EFECTES"
|
||||
audio_sound_volume: "VOLUM EFECTES"
|
||||
# Items del submenu SISTEMA
|
||||
system_restart: "REINICIAR"
|
||||
# Pagines de confirmacio (estructura: titol + NO/SI)
|
||||
confirm_restart: "ESTAS SEGUR DE REINICIAR?"
|
||||
confirm_exit: "ESTAS SEGUR DE EIXIR?"
|
||||
confirm_no: "NO"
|
||||
confirm_yes: "SI"
|
||||
# Valors comuns
|
||||
value_on: "ACTIU"
|
||||
value_off: "INACTIU"
|
||||
# Items del submenu CONTROLS
|
||||
controls_pad_p1: "MANDO JUGADOR 1"
|
||||
controls_pad_p2: "MANDO JUGADOR 2"
|
||||
controls_no_pad: "SENSE MANDO"
|
||||
controls_define_keyboard_p1: "REDEFINIR TECLES P1"
|
||||
controls_define_keyboard_p2: "REDEFINIR TECLES P2"
|
||||
controls_define_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||
controls_define_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||
|
||||
# Overlay modal de redefinicio (DefineInputs)
|
||||
define:
|
||||
title_keyboard_p1: "REDEFINIR TECLES P1"
|
||||
title_keyboard_p2: "REDEFINIR TECLES P2"
|
||||
title_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||
title_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||
press_key: "PREMEU UNA TECLA"
|
||||
press_button: "PREMEU UN BOTO"
|
||||
complete: "CONFIGURACIO COMPLETA"
|
||||
no_gamepad: "CAP MANDO ASSIGNAT AL JUGADOR"
|
||||
action:
|
||||
left: "ESQUERRA"
|
||||
right: "DRETA"
|
||||
fire: "DISPARAR"
|
||||
accelerate: "ACCELERAR"
|
||||
start: "START"
|
||||
menu: "MENU"
|
||||
@@ -0,0 +1,111 @@
|
||||
# Orni Attack - locale: English
|
||||
# In-game pool kept English in both locales per design.
|
||||
|
||||
notification:
|
||||
press_again_exit: "PRESS ESC AGAIN TO EXIT"
|
||||
zoom: "ZOOM: {z}X"
|
||||
fullscreen_on: "FULLSCREEN"
|
||||
fullscreen_off: "WINDOWED"
|
||||
vsync_on: "VSYNC ON"
|
||||
vsync_off: "VSYNC OFF"
|
||||
antialias_on: "AA ON"
|
||||
antialias_off: "AA OFF"
|
||||
postfx_on: "POSTPROCESS ON"
|
||||
postfx_off: "POSTPROCESS OFF"
|
||||
locale_switched: "LANGUAGE: {lang}"
|
||||
gamepad_connected: "{name} CONNECTED"
|
||||
gamepad_disconnected: "{name} DISCONNECTED"
|
||||
|
||||
language:
|
||||
ca: "CATALAN"
|
||||
en: "ENGLISH"
|
||||
|
||||
hud:
|
||||
level: "LEVEL "
|
||||
|
||||
title:
|
||||
press_start: "PRESS START TO PLAY"
|
||||
|
||||
game_screen:
|
||||
game_over: "GAME OVER"
|
||||
continue: "CONTINUE"
|
||||
continues_left: "CONTINUES LEFT: {n}"
|
||||
|
||||
stage:
|
||||
start:
|
||||
- "ORNI ALERT!"
|
||||
- "INCOMING ORNIS!"
|
||||
- "ROLLING THREAT!"
|
||||
- "ENEMY WAVE!"
|
||||
- "WAVE OF ORNIS DETECTED!"
|
||||
- "NEXT SWARM APPROACHING!"
|
||||
- "BRACE FOR THE NEXT WAVE!"
|
||||
- "ANOTHER ATTACK INCOMING!"
|
||||
- "SENSORS DETECT HOSTILE ORNIS..."
|
||||
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
|
||||
- "ENEMY FORCES MOBILIZING!"
|
||||
- "PREPARE FOR IMPACT!"
|
||||
completed: "GOOD JOB COMMANDER!"
|
||||
|
||||
service_menu:
|
||||
title: "SERVICE MENU"
|
||||
video: "VIDEO"
|
||||
audio: "AUDIO"
|
||||
options: "OPTIONS"
|
||||
system: "SYSTEM"
|
||||
controls: "CONTROLS"
|
||||
back: "BACK"
|
||||
exit: "EXIT GAME"
|
||||
# Items of VIDEO submenu
|
||||
video_zoom: "ZOOM"
|
||||
video_fullscreen: "FULLSCREEN"
|
||||
video_vsync: "VSYNC"
|
||||
video_aa: "ANTIALIAS"
|
||||
video_postfx: "POSTPROCESS"
|
||||
video_resolution: "RESOLUTION"
|
||||
# Items of OPTIONS submenu
|
||||
options_language: "LANGUAGE"
|
||||
options_show_info: "SHOW INFO"
|
||||
# Items of AUDIO submenu
|
||||
audio_master: "AUDIO"
|
||||
audio_master_volume: "MASTER VOLUME"
|
||||
audio_music: "MUSIC"
|
||||
audio_music_volume: "MUSIC VOLUME"
|
||||
audio_sound: "SOUNDS"
|
||||
audio_sound_volume: "SOUND VOLUME"
|
||||
# Items of SYSTEM submenu
|
||||
system_restart: "RESTART"
|
||||
# Confirmation pages (structure: title + NO/YES)
|
||||
confirm_restart: "REALLY RESTART?"
|
||||
confirm_exit: "REALLY EXIT?"
|
||||
confirm_no: "NO"
|
||||
confirm_yes: "YES"
|
||||
# Common values
|
||||
value_on: "ON"
|
||||
value_off: "OFF"
|
||||
# Items of CONTROLS submenu
|
||||
controls_pad_p1: "PLAYER 1 GAMEPAD"
|
||||
controls_pad_p2: "PLAYER 2 GAMEPAD"
|
||||
controls_no_pad: "NO GAMEPAD"
|
||||
controls_define_keyboard_p1: "REDEFINE KEYS P1"
|
||||
controls_define_keyboard_p2: "REDEFINE KEYS P2"
|
||||
controls_define_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||
controls_define_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||
|
||||
# Modal overlay for input redefinition (DefineInputs)
|
||||
define:
|
||||
title_keyboard_p1: "REDEFINE KEYS P1"
|
||||
title_keyboard_p2: "REDEFINE KEYS P2"
|
||||
title_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||
title_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||
press_key: "PRESS A KEY"
|
||||
press_button: "PRESS A BUTTON"
|
||||
complete: "CONFIGURATION COMPLETE"
|
||||
no_gamepad: "NO GAMEPAD ASSIGNED TO PLAYER"
|
||||
action:
|
||||
left: "LEFT"
|
||||
right: "RIGHT"
|
||||
fire: "FIRE"
|
||||
accelerate: "ACCELERATE"
|
||||
start: "START"
|
||||
menu: "MENU"
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_lparen.shp - Símbol ( (parèntesi esquerre)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_lparen
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Arc cap a l'esquerra aproximat amb 4 trams rectes
|
||||
polyline: 14,4 8,12 6,20 8,28 14,36
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_rparen.shp - Símbol ) (parèntesi dret)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_rparen
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Arc cap a la dreta aproximat amb 4 trams rectes
|
||||
polyline: 6,4 12,12 14,20 12,28 6,36
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_slash.shp - Símbol / (barra)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_slash
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Línia diagonal de baix-esquerra a dalt-dreta
|
||||
line: 4,36 16,4
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+32
-29
@@ -51,8 +51,6 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
||||
return;
|
||||
}
|
||||
|
||||
if (!music_enabled_) { return; }
|
||||
|
||||
auto* resource = AudioResource::getMusic(name);
|
||||
if (resource == nullptr) { return; }
|
||||
|
||||
@@ -62,7 +60,7 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
||||
|
||||
// Reprodueix la música per punter (amb crossfade opcional)
|
||||
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||
if (!music_enabled_ || music == nullptr) { return; }
|
||||
if (music == nullptr) { return; }
|
||||
|
||||
playMusicInternal(music, loop, crossfade_ms);
|
||||
// Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el
|
||||
@@ -72,9 +70,12 @@ void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms)
|
||||
}
|
||||
|
||||
// Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i
|
||||
// actualitza el loop cachejat. Els callers s'encarreguen del gating
|
||||
// (music_enabled_, nullptr, same-track early return) y del nom. L'estat el
|
||||
// manté Ja (Ja::playMusic posa PLAYING al Ja::Music* corresponent).
|
||||
// actualitza el loop cachejat. Els callers s'encarreguen del same-track early
|
||||
// return i del nom. El gate de música deshabilitada NO atura la reproducció:
|
||||
// effectiveVolume porta el volum efectiu a 0 i la pista continua sonant
|
||||
// silenciada, per garantir que reactivar la música la torne a sentir sense
|
||||
// haver de reiniciar la pista. L'estat el manté Ja (Ja::playMusic posa
|
||||
// PLAYING al Ja::Music* corresponent).
|
||||
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
|
||||
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
|
||||
@@ -91,41 +92,35 @@ void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossf
|
||||
|
||||
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
|
||||
void Audio::pauseMusic() {
|
||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||
if (getMusicState() == MusicState::PLAYING) {
|
||||
engine_->pauseMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
||||
void Audio::resumeMusic() {
|
||||
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
||||
if (getMusicState() == MusicState::PAUSED) {
|
||||
engine_->resumeMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
||||
void Audio::stopMusic() {
|
||||
if (music_enabled_) {
|
||||
engine_->stopMusic();
|
||||
}
|
||||
}
|
||||
|
||||
void Audio::setMusicSpeed(float ratio) {
|
||||
if (music_enabled_) {
|
||||
engine_->setMusicSpeed(ratio);
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so per nom
|
||||
void Audio::playSound(const std::string& name, Group group) {
|
||||
if (sound_enabled_) {
|
||||
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so per punter directe
|
||||
void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
if (sound_enabled_ && sound != nullptr) {
|
||||
if (sound != nullptr) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
@@ -136,7 +131,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
||||
// la crida al ratio — sin efectes col·laterals.
|
||||
void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
@@ -149,7 +143,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
|
||||
// sec — l'usuari sent el so aún que la cua no s'apliqui.
|
||||
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
@@ -168,7 +161,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset
|
||||
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
||||
// fallback que playSoundWithEcho.
|
||||
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
@@ -186,14 +178,12 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
|
||||
|
||||
// Atura tots los sons
|
||||
void Audio::stopAllSounds() {
|
||||
if (sound_enabled_) {
|
||||
engine_->stopChannel(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Fa una fosa de sortida de la música
|
||||
void Audio::fadeOutMusic(int milliseconds) {
|
||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||
if (getMusicState() == MusicState::PLAYING) {
|
||||
engine_->fadeOutMusic(milliseconds);
|
||||
}
|
||||
}
|
||||
@@ -238,14 +228,27 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
|
||||
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
|
||||
}
|
||||
|
||||
// Estableix el volum dels sons (float 0.0..1.0)
|
||||
// Estableix el volum dels sons (float 0.0..1.0). Actualitza el valor cachejat
|
||||
// a config_ perquè els getters i les re-aplicacions internes (enableSound,
|
||||
// setMasterVolume) puguin tornar al volum que l'usuari va triar.
|
||||
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
||||
config_.sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
engine_->setSoundVolume(effectiveVolume(config_.sound_volume, sound_enabled_), static_cast<int>(group));
|
||||
}
|
||||
|
||||
// Estableix el volum de la música (float 0.0..1.0)
|
||||
// Estableix el volum de la música (float 0.0..1.0). Cf. setSoundVolume.
|
||||
void Audio::setMusicVolume(float music_volume) {
|
||||
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
||||
config_.music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
engine_->setMusicVolume(effectiveVolume(config_.music_volume, music_enabled_));
|
||||
}
|
||||
|
||||
// Estableix el volum master (multiplicador aplicat a sound + music). Re-aplica
|
||||
// els canals perquè el canvi tingui efecte immediat sense esperar al següent
|
||||
// setSoundVolume/setMusicVolume explícit.
|
||||
void Audio::setMasterVolume(float master_volume) {
|
||||
config_.volume = std::clamp(master_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
setSoundVolume(config_.sound_volume);
|
||||
setMusicVolume(config_.music_volume);
|
||||
}
|
||||
|
||||
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
|
||||
@@ -256,12 +259,12 @@ void Audio::applySettings(const Config& config) {
|
||||
enable(config_.enabled);
|
||||
}
|
||||
|
||||
// Estableix l'estat general
|
||||
// Estableix l'estat general. Re-aplica els volums actuals; effectiveVolume
|
||||
// retalla a 0 quan enabled_ és false, sense perdre els valors guardats.
|
||||
void Audio::enable(bool value) {
|
||||
enabled_ = value;
|
||||
|
||||
setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME);
|
||||
setMusicVolume(enabled_ ? config_.music_volume : MIN_VOLUME);
|
||||
setSoundVolume(config_.sound_volume);
|
||||
setMusicVolume(config_.music_volume);
|
||||
}
|
||||
|
||||
// Estableix l'estat dels sons i reaplica el volum porque los canals actius
|
||||
|
||||
@@ -101,6 +101,14 @@ class Audio {
|
||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
||||
void setMusicVolume(float volume); // Ajusta el volum de la música
|
||||
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
|
||||
|
||||
// Getters dels volums actuals (lectura de la config_ cachejada). Reflexen
|
||||
// el valor que l'usuari ha triat l'última vegada, independent del gating
|
||||
// d'enabled/channel.
|
||||
[[nodiscard]] auto getMasterVolume() const -> float { return config_.volume; }
|
||||
[[nodiscard]] auto getSoundVolume() const -> float { return config_.sound_volume; }
|
||||
[[nodiscard]] auto getMusicVolume() const -> float { return config_.music_volume; }
|
||||
|
||||
// --- Helpers de conversió para la capa de presentació ---
|
||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||
|
||||
@@ -48,22 +48,36 @@ namespace Config {
|
||||
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
|
||||
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
|
||||
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
|
||||
int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button
|
||||
int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei
|
||||
};
|
||||
|
||||
struct PlayerBindings {
|
||||
KeyboardBindings keyboard{};
|
||||
GamepadBindings gamepad{};
|
||||
std::string gamepad_name; // Empty = auto-assign by index
|
||||
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
|
||||
};
|
||||
|
||||
struct AudioConfig {
|
||||
bool enabled{true};
|
||||
float volume{1.0F}; // Master 0..1
|
||||
bool music_enabled{true};
|
||||
float music_volume{1.0F};
|
||||
bool sound_enabled{true};
|
||||
float sound_volume{0.25F};
|
||||
};
|
||||
|
||||
struct EngineConfig {
|
||||
WindowConfig window{};
|
||||
RenderingConfig rendering{};
|
||||
AudioConfig audio{};
|
||||
PlayerBindings player1{};
|
||||
PlayerBindings player2{};
|
||||
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
|
||||
GamepadBindings gamepad_controls{};
|
||||
bool console{false};
|
||||
std::string locale{"ca"}; // "ca" | "en" — fixat a l'arrencada, sense hot-swap
|
||||
};
|
||||
|
||||
} // namespace Config
|
||||
|
||||
@@ -33,7 +33,9 @@ namespace Defaults::Hud {
|
||||
namespace DebugOverlay {
|
||||
constexpr float X = 30.0F;
|
||||
constexpr float Y_FPS = 24.0F;
|
||||
constexpr float FPS_LINE_HEIGHT = 28.0F; // separació després del FPS (scale 0.7 → ~28 px)
|
||||
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
|
||||
constexpr float FPS_SCALE = 0.7F; // FPS més gran que la resta
|
||||
constexpr float TEXT_SCALE = 0.4F;
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
constexpr float BRIGHTNESS = 1.0F;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// service_menu.hpp - Constants del menu de servei (F12)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Defaults::ServiceMenu {
|
||||
|
||||
// ---- Mides en coordenades logiques del joc (1280×720) ----
|
||||
// BOX_WIDTH_MIN es el minim: si el titol o algun item no hi caben, el
|
||||
// marc s'expandeix dinamicament amb animacio (cf. WIDTH_RATE).
|
||||
constexpr int BOX_WIDTH_MIN = 460;
|
||||
constexpr int GAP_Y = 22;
|
||||
constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text
|
||||
constexpr int SUBTITLE_HEIGHT = 18; // scale 0.4 ≈ 16 px de text
|
||||
constexpr int SEPARATOR_HEIGHT = 1;
|
||||
constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight
|
||||
constexpr int ITEM_GAP_Y = 6;
|
||||
|
||||
// Brackets als 4 cantons (substitueixen la vora completa: estètica sci-fi).
|
||||
constexpr int CORNER_ARM_H = 48;
|
||||
constexpr int CORNER_ARM_V = 28;
|
||||
constexpr int CORNER_THICKNESS = 2;
|
||||
|
||||
// ---- Animacio open/close (mateixos parametres que aee_arcade) ----
|
||||
constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir
|
||||
constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar
|
||||
constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa
|
||||
constexpr float WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines
|
||||
|
||||
// ---- Animacio del highlight (rectangle del cursor) ----
|
||||
// Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial).
|
||||
constexpr float HIGHLIGHT_RATE = 18.0F;
|
||||
constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada
|
||||
constexpr int HIGHLIGHT_THICKNESS = 1;
|
||||
constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text
|
||||
constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical
|
||||
constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre)
|
||||
constexpr int MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic)
|
||||
|
||||
// ---- Colors RGBA ----
|
||||
constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215};
|
||||
constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon
|
||||
constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255};
|
||||
constexpr SDL_Color SUBTITLE_COLOR{.r = 110, .g = 170, .b = 210, .a = 220}; // cian apagat
|
||||
constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180};
|
||||
constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255};
|
||||
constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat
|
||||
constexpr SDL_Color HIGHLIGHT_OUTLINE{.r = 255, .g = 230, .b = 120, .a = 255}; // mateix groc, opac
|
||||
constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid
|
||||
|
||||
// ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ----
|
||||
constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard
|
||||
constexpr float SUBTITLE_SCALE = 0.40F; // sota el titol, info decorativa (versio/hash)
|
||||
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
|
||||
// ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ----
|
||||
constexpr const char* SELECT_SOUND = "ui/menu_select.wav";
|
||||
constexpr const char* ACCEPT_SOUND = "ui/menu_accept.wav";
|
||||
|
||||
} // namespace Defaults::ServiceMenu
|
||||
@@ -47,7 +47,7 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
// Cargar símbolos
|
||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
|
||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"};
|
||||
for (const auto& sym : SYMBOLS) {
|
||||
char c = sym[0];
|
||||
std::string filename = getShapeFilename(c);
|
||||
@@ -164,6 +164,12 @@ namespace Graphics {
|
||||
return "font/char_exclamation.shp";
|
||||
case '?':
|
||||
return "font/char_question.shp";
|
||||
case '/':
|
||||
return "font/char_slash.shp";
|
||||
case '(':
|
||||
return "font/char_lparen.shp";
|
||||
case ')':
|
||||
return "font/char_rparen.shp";
|
||||
case ' ':
|
||||
return ""; // Espai es maneja sin load shape
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Graphics {
|
||||
|
||||
// Renderizar string completo
|
||||
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
|
||||
// '!', '?', ' ')
|
||||
// '!', '?', '/', '(', ')', ' ')
|
||||
// - position: posición inicial (esquina superior izquierda)
|
||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/input/define_inputs.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/defaults/service_menu.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/config_yaml.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float CANVAS_W = 1280.0F;
|
||||
constexpr float CANVAS_H = 720.0F;
|
||||
|
||||
// Llindar de trigger per a edge-detect L2/R2 com a boto virtual.
|
||||
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||
|
||||
// Codis virtuals per als triggers (consistents amb input_types.cpp).
|
||||
constexpr int TRIGGER_L2_VIRTUAL = 100;
|
||||
constexpr int TRIGGER_R2_VIRTUAL = 101;
|
||||
|
||||
// Durada del missatge de confirmacio abans de tancar-se.
|
||||
constexpr float COMPLETE_DISPLAY_S = 1.5F;
|
||||
|
||||
// Llindar dpad als axis sticks: no es captura per evitar conflicte amb el
|
||||
// moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando).
|
||||
constexpr Sint16 STICK_THRESHOLD = 16384;
|
||||
|
||||
// Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]).
|
||||
void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
|
||||
renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
|
||||
}
|
||||
|
||||
auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string {
|
||||
const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD);
|
||||
const bool IS_P1 = (player == System::DefineInputs::Player::P1);
|
||||
if (IS_KB && IS_P1) {
|
||||
return "define.title_keyboard_p1";
|
||||
}
|
||||
if (IS_KB) {
|
||||
return "define.title_keyboard_p2";
|
||||
}
|
||||
if (IS_P1) {
|
||||
return "define.title_gamepad_p1";
|
||||
}
|
||||
return "define.title_gamepad_p2";
|
||||
}
|
||||
|
||||
// Scancodes que MAI capturem com a binding (reservats per a navegacio o
|
||||
// global hotkeys). Tornen true → handleEvent les deixa passar al pipeline
|
||||
// global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12
|
||||
// son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio).
|
||||
auto isReservedScancode(SDL_Scancode sc) -> bool {
|
||||
if (sc == SDL_SCANCODE_ESCAPE) {
|
||||
return true;
|
||||
}
|
||||
if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) {
|
||||
return true;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) {
|
||||
return true;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Conversio sense pèrdua de SDL_Scancode → int per a comparacions
|
||||
// homogenies dins de sequence_ (que guarda codis de tots dos modes).
|
||||
auto scancodeToInt(SDL_Scancode sc) -> int {
|
||||
return static_cast<int>(sc);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace System {
|
||||
|
||||
std::unique_ptr<DefineInputs> DefineInputs::instance;
|
||||
|
||||
void DefineInputs::init(Rendering::Renderer* renderer) {
|
||||
if (!instance) {
|
||||
instance = std::unique_ptr<DefineInputs>(new DefineInputs(renderer));
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::destroy() { instance.reset(); }
|
||||
|
||||
auto DefineInputs::get() -> DefineInputs* { return instance.get(); }
|
||||
|
||||
DefineInputs::DefineInputs(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer),
|
||||
text_(renderer) {}
|
||||
|
||||
auto DefineInputs::isActive() const -> bool {
|
||||
return phase_ != Phase::INACTIVE;
|
||||
}
|
||||
|
||||
auto DefineInputs::begin(Mode mode, Player player) -> bool {
|
||||
if (mode == Mode::GAMEPAD) {
|
||||
// Requereix un pad assignat al jugador.
|
||||
const auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const int IDX = (player == Player::P1) ? 0 : 1;
|
||||
if (input->getPlayerGamepad(IDX) == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
mode_ = mode;
|
||||
player_ = player;
|
||||
index_ = 0;
|
||||
complete_timer_s_ = 0.0F;
|
||||
l2_was_pressed_ = false;
|
||||
r2_was_pressed_ = false;
|
||||
|
||||
buildSequence();
|
||||
phase_ = Phase::CAPTURING;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DefineInputs::cancel() {
|
||||
phase_ = Phase::INACTIVE;
|
||||
sequence_.clear();
|
||||
index_ = 0;
|
||||
complete_timer_s_ = 0.0F;
|
||||
}
|
||||
|
||||
void DefineInputs::buildSequence() {
|
||||
sequence_.clear();
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
// Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST)
|
||||
sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
|
||||
} else {
|
||||
// Mando: FIRE, ACCELERATE, START, MENU
|
||||
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1});
|
||||
}
|
||||
}
|
||||
|
||||
auto DefineInputs::isInUse(int code) const -> bool {
|
||||
return std::ranges::any_of(sequence_, [code](const Step& s) {
|
||||
return s.captured == code;
|
||||
});
|
||||
}
|
||||
|
||||
void DefineInputs::captureAndAdvance(int code) {
|
||||
if (index_ >= sequence_.size()) {
|
||||
return;
|
||||
}
|
||||
if (isInUse(code)) {
|
||||
return; // Duplicat dins de la sessio: rebutgem silenciosament
|
||||
}
|
||||
sequence_[index_].captured = code;
|
||||
++index_;
|
||||
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||
audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
|
||||
}
|
||||
if (index_ >= sequence_.size()) {
|
||||
persistAndComplete();
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::persistAndComplete() {
|
||||
auto& cfg = (player_ == Player::P1)
|
||||
? ConfigYaml::engine_config.player1
|
||||
: ConfigYaml::engine_config.player2;
|
||||
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
for (const Step& s : sequence_) {
|
||||
switch (s.action) {
|
||||
case InputAction::LEFT:
|
||||
cfg.keyboard.key_left = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::RIGHT:
|
||||
cfg.keyboard.key_right = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::SHOOT:
|
||||
cfg.keyboard.key_shoot = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::THRUST:
|
||||
cfg.keyboard.key_thrust = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
default:
|
||||
break; // START / MENU no es redefineixen al teclat
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const Step& s : sequence_) {
|
||||
switch (s.action) {
|
||||
case InputAction::SHOOT:
|
||||
cfg.gamepad.button_shoot = s.captured;
|
||||
break;
|
||||
case InputAction::THRUST:
|
||||
cfg.gamepad.button_thrust = s.captured;
|
||||
break;
|
||||
case InputAction::START:
|
||||
cfg.gamepad.button_start = s.captured;
|
||||
break;
|
||||
case InputAction::MENU:
|
||||
cfg.gamepad.button_menu = s.captured;
|
||||
break;
|
||||
default:
|
||||
break; // LEFT / RIGHT no es redefineixen al mando
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar canvis al runtime de l'Input i persistir a disc.
|
||||
if (auto* input = Input::get(); input != nullptr) {
|
||||
if (player_ == Player::P1) {
|
||||
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
|
||||
} else {
|
||||
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
|
||||
}
|
||||
}
|
||||
ConfigYaml::saveToFile();
|
||||
|
||||
phase_ = Phase::COMPLETE;
|
||||
complete_timer_s_ = COMPLETE_DISPLAY_S;
|
||||
}
|
||||
|
||||
void DefineInputs::update(float delta_time) {
|
||||
if (phase_ != Phase::COMPLETE) {
|
||||
return;
|
||||
}
|
||||
complete_timer_s_ -= delta_time;
|
||||
if (complete_timer_s_ <= 0.0F) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) {
|
||||
if (now && !was_pressed) {
|
||||
captureAndAdvance(virtual_button);
|
||||
}
|
||||
was_pressed = now;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool {
|
||||
if (event.type != SDL_EVENT_KEY_DOWN) {
|
||||
return true; // Empassem la resta sense fer res
|
||||
}
|
||||
const SDL_Scancode SC = event.key.scancode;
|
||||
if (isReservedScancode(SC)) {
|
||||
// ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline
|
||||
// global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.).
|
||||
return false;
|
||||
}
|
||||
captureAndAdvance(scancodeToInt(SC));
|
||||
return true;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool {
|
||||
// KEY_DOWN no es per al rebind de mando: deixem que el global el
|
||||
// gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.).
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrar events al pad del jugador actiu.
|
||||
const auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return true;
|
||||
}
|
||||
const int IDX = (player_ == Player::P1) ? 0 : 1;
|
||||
auto pad = input->getPlayerGamepad(IDX);
|
||||
if (!pad) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
if (event.gbutton.which != pad->instance_id) {
|
||||
return true;
|
||||
}
|
||||
captureAndAdvance(static_cast<int>(event.gbutton.button));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
if (event.gaxis.which != pad->instance_id) {
|
||||
return true;
|
||||
}
|
||||
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
|
||||
const Sint16 VAL = event.gaxis.value;
|
||||
if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
|
||||
processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
|
||||
} else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
|
||||
}
|
||||
// Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles).
|
||||
(void)STICK_THRESHOLD;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleEvent(const SDL_Event& event) -> bool {
|
||||
if (phase_ == Phase::INACTIVE) {
|
||||
return false;
|
||||
}
|
||||
// SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la
|
||||
// finestra encara que el modal estiga obert; els passem al pipeline.
|
||||
if (event.type == SDL_EVENT_QUIT ||
|
||||
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
|
||||
return false;
|
||||
}
|
||||
if (phase_ == Phase::COMPLETE) {
|
||||
// Mentre mostrem el missatge OK, empassem la resta d'events sense
|
||||
// capturar perque l'usuari no puga avançar accions sense voler.
|
||||
return true;
|
||||
}
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
return handleKeyboardEvent(event);
|
||||
}
|
||||
return handleGamepadEvent(event);
|
||||
}
|
||||
|
||||
void DefineInputs::draw() const {
|
||||
if (phase_ == Phase::INACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
using namespace Defaults::ServiceMenu;
|
||||
|
||||
// Caixa centrada, dimensions fixes (no depen del contingut a redefinir).
|
||||
constexpr float BOX_W = 560.0F;
|
||||
constexpr float BOX_H = 280.0F;
|
||||
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
|
||||
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
|
||||
|
||||
// Fons + brackets als 4 cantons (estil HUD del menu de servei).
|
||||
fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);
|
||||
|
||||
const auto T = static_cast<float>(CORNER_THICKNESS);
|
||||
const auto AH = static_cast<float>(CORNER_ARM_H);
|
||||
const auto AV = static_cast<float>(CORNER_ARM_V);
|
||||
fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
|
||||
|
||||
const std::string TITLE = Locale::get().text(titleKey(mode_, player_));
|
||||
const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING);
|
||||
const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F);
|
||||
const float TITLE_Y = BOX_Y + 26.0F;
|
||||
text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR);
|
||||
|
||||
if (phase_ == Phase::COMPLETE) {
|
||||
const std::string OK = Locale::get().text("define.complete");
|
||||
constexpr float OK_SCALE = 0.7F;
|
||||
const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING);
|
||||
const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F);
|
||||
const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F;
|
||||
constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255};
|
||||
text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Instruccio (premeu tecla / boto) + accio actual + progres.
|
||||
const std::string PROMPT = Locale::get().text(
|
||||
mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button");
|
||||
const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING);
|
||||
const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F);
|
||||
const float PROMPT_Y = BOX_Y + 86.0F;
|
||||
text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR);
|
||||
|
||||
if (index_ < sequence_.size()) {
|
||||
const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key);
|
||||
constexpr float ACTION_SCALE = 0.9F;
|
||||
const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING);
|
||||
const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F);
|
||||
const float ACTION_Y = BOX_Y + 130.0F;
|
||||
text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR);
|
||||
}
|
||||
|
||||
const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size());
|
||||
constexpr float PROG_SCALE = 0.4F;
|
||||
const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING);
|
||||
const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F);
|
||||
const float PROG_Y = BOX_Y + 200.0F;
|
||||
text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR);
|
||||
}
|
||||
|
||||
} // namespace System
|
||||
@@ -0,0 +1,107 @@
|
||||
// define_inputs.hpp - Overlay modal de redefinici de controls (singleton)
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Sub-mòdul inspirat en aee_arcade/source/core/input/define_buttons. Quan el
|
||||
// menú de servei dispara una acció "Redefinir tecles/botons P1/P2", aquest
|
||||
// singleton pren el control: pinta una caixa central, captura events SDL i
|
||||
// avança per una seqüència fixa d'accions, persistint les noves assignacions
|
||||
// a config.yaml en acabar.
|
||||
//
|
||||
// Cicle de vida:
|
||||
// 1. begin(mode, player) → construeix la seqüència (4 passos) i activa
|
||||
// l'overlay. Per a GAMEPAD, retorna false si el jugador no té pad.
|
||||
// 2. handleEvent() captura el següent event vàlid; ESC cancel·la sense
|
||||
// desar; duplicats dins de la sessió es rebutgen silenciosament.
|
||||
// 3. Quan la seqüència es completa, persistim a engine_config + saveToFile,
|
||||
// reapliquem els bindings i mostrem un missatge "OK" durant 1.5 s
|
||||
// abans d'auto-tancar-se.
|
||||
//
|
||||
// El routing d'events es fa des de GlobalEvents::handle: mentre isActive()
|
||||
// retorna true, tots els events SDL es desvien aquí i no arriben al joc ni
|
||||
// al menú de servei.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/input/input_types.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
namespace System {
|
||||
|
||||
class DefineInputs {
|
||||
public:
|
||||
enum class Mode : std::uint8_t { KEYBOARD,
|
||||
GAMEPAD };
|
||||
enum class Player : std::uint8_t { P1,
|
||||
P2 };
|
||||
|
||||
static void init(Rendering::Renderer* renderer);
|
||||
static void destroy();
|
||||
[[nodiscard]] static auto get() -> DefineInputs*;
|
||||
|
||||
// Comença la sessió. Retorna false per a GAMEPAD si el jugador no té
|
||||
// cap pad assignat (el caller hauria de notificar a l'usuari abans).
|
||||
auto begin(Mode mode, Player player) -> bool;
|
||||
void cancel();
|
||||
|
||||
[[nodiscard]] auto isActive() const -> bool;
|
||||
|
||||
void update(float delta_time);
|
||||
void draw() const;
|
||||
|
||||
// Retorna true si l'event s'ha consumit (és a dir, mentre l'overlay
|
||||
// és actiu sempre consumeix tot per evitar passages al joc o menú).
|
||||
auto handleEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
private:
|
||||
explicit DefineInputs(Rendering::Renderer* renderer);
|
||||
|
||||
enum class Phase : std::uint8_t {
|
||||
INACTIVE,
|
||||
CAPTURING,
|
||||
COMPLETE, // mostra missatge OK breu abans d'auto-cancel
|
||||
};
|
||||
|
||||
struct Step {
|
||||
std::string action_label_key; // p.ex. "define.action.left"
|
||||
InputAction action; // mapeig a la struct PlayerBindings
|
||||
int captured{-1}; // scancode o button code; -1 = sense capturar
|
||||
};
|
||||
|
||||
void buildSequence();
|
||||
[[nodiscard]] auto isInUse(int code) const -> bool;
|
||||
void captureAndAdvance(int code);
|
||||
void persistAndComplete();
|
||||
|
||||
// Handlers especialitzats segons mode_.
|
||||
auto handleKeyboardEvent(const SDL_Event& event) -> bool;
|
||||
auto handleGamepadEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
// Edge-detect per als triggers L2/R2 com a botons virtuals.
|
||||
void processTrigger(int virtual_button, bool& was_pressed, bool now);
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
Graphics::VectorText text_;
|
||||
|
||||
Phase phase_{Phase::INACTIVE};
|
||||
Mode mode_{Mode::KEYBOARD};
|
||||
Player player_{Player::P1};
|
||||
std::vector<Step> sequence_;
|
||||
std::size_t index_{0};
|
||||
float complete_timer_s_{0.0F};
|
||||
|
||||
// Estat d'edge-detect dels triggers durant la sessió GAMEPAD.
|
||||
bool l2_was_pressed_{false};
|
||||
bool r2_was_pressed_{false};
|
||||
|
||||
static std::unique_ptr<DefineInputs> instance;
|
||||
};
|
||||
|
||||
} // namespace System
|
||||
+84
-18
@@ -8,6 +8,10 @@
|
||||
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||
#include <utility> // Para move
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/system/notifier.hpp"
|
||||
#include "core/utils/string_utils.hpp"
|
||||
|
||||
// Singleton
|
||||
Input* Input::instance = nullptr;
|
||||
|
||||
@@ -373,9 +377,25 @@ void Input::update() {
|
||||
|
||||
// --- MANDOS ---
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
// LEFT i RIGHT NO son redefinibles al mando (assumits dpad o stick).
|
||||
// Llegim el left stick X i el fusionem amb l'estat del dpad: qualsevol
|
||||
// de les dos fonts activa l'accio. Llindar AXIS_THRESHOLD (30000).
|
||||
const Sint16 STICK_X = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
const bool STICK_LEFT = STICK_X < -AXIS_THRESHOLD;
|
||||
const bool STICK_RIGHT = STICK_X > AXIS_THRESHOLD;
|
||||
|
||||
for (auto& binding : gamepad->bindings) {
|
||||
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
|
||||
|
||||
// Per a LEFT/RIGHT, fer un OR amb el stick X. La resta d'accions
|
||||
// (THRUST/SHOOT/START/MENU) ignoren el stick aqui — si es vol
|
||||
// dispar amb trigger L2/R2 cal binding amb codi 100/101.
|
||||
if (binding.first == Action::LEFT) {
|
||||
button_is_down_now = button_is_down_now || STICK_LEFT;
|
||||
} else if (binding.first == Action::RIGHT) {
|
||||
button_is_down_now = button_is_down_now || STICK_RIGHT;
|
||||
}
|
||||
|
||||
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
||||
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
|
||||
binding.second.is_held = button_is_down_now;
|
||||
@@ -407,6 +427,16 @@ auto Input::addGamepad(int device_index) -> std::string {
|
||||
auto name = gamepad->name;
|
||||
std::cout << "Gamepad connected (" << name << ")" << '\n';
|
||||
gamepads_.push_back(std::move(gamepad));
|
||||
|
||||
// Toast a pantalla. Pot ser nullptr durant discoverGamepads() inicial
|
||||
// (l'Input::init() es crida abans que el Director instanciï el Notifier).
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.gamepad_connected"),
|
||||
"{name}",
|
||||
Utils::toUpperAscii(name)));
|
||||
}
|
||||
|
||||
return name + " CONNECTED";
|
||||
}
|
||||
|
||||
@@ -419,6 +449,14 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
|
||||
std::string name = (*it)->name;
|
||||
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
|
||||
gamepads_.erase(it);
|
||||
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.gamepad_disconnected"),
|
||||
"{name}",
|
||||
Utils::toUpperAscii(name)));
|
||||
}
|
||||
|
||||
return name + " DISCONNECTED";
|
||||
}
|
||||
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
|
||||
@@ -465,6 +503,33 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
|
||||
|
||||
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
|
||||
|
||||
// Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els
|
||||
// dos camps venen buits o no n'hi ha cap match retornem nullptr (sense
|
||||
// mando explicit). L'autoassignacio inicial es resol al boot.
|
||||
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad> {
|
||||
if (gamepads_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!bindings.gamepad_path.empty()) {
|
||||
for (const auto& pad : gamepads_) {
|
||||
if (pad && pad->path == bindings.gamepad_path) {
|
||||
return pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bindings.gamepad_name.empty()) {
|
||||
for (const auto& pad : gamepads_) {
|
||||
if (pad && pad->name == bindings.gamepad_name) {
|
||||
return pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Aplica configuración de controles del player 1
|
||||
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
|
||||
@@ -474,15 +539,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||
|
||||
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
|
||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||
if (bindings.gamepad_name.empty()) {
|
||||
// Fallback: usar primer gamepad disponible
|
||||
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
|
||||
} else {
|
||||
// Buscar por nombre
|
||||
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
||||
}
|
||||
// 2. Resoldre gamepad per path/name
|
||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||
|
||||
if (!gamepad) {
|
||||
player1_gamepad_ = nullptr;
|
||||
@@ -494,6 +552,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
|
||||
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
|
||||
|
||||
// 4. Cachear referencia
|
||||
player1_gamepad_ = gamepad;
|
||||
@@ -508,15 +568,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
||||
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||
|
||||
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
|
||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||
if (bindings.gamepad_name.empty()) {
|
||||
// Fallback: usar segundo gamepad disponible
|
||||
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
|
||||
} else {
|
||||
// Buscar por nombre
|
||||
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
||||
}
|
||||
// 2. Resoldre gamepad per path/name
|
||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||
|
||||
if (!gamepad) {
|
||||
player2_gamepad_ = nullptr;
|
||||
@@ -528,6 +581,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
||||
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
|
||||
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
|
||||
|
||||
// 4. Cachear referencia
|
||||
player2_gamepad_ = gamepad;
|
||||
@@ -555,6 +610,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
|
||||
return keyboard_active || gamepad_active;
|
||||
}
|
||||
|
||||
// Retorna el pad assignat (0=P1, 1=P2). Pot ser nullptr.
|
||||
auto Input::getPlayerGamepad(int player_index) const -> std::shared_ptr<Input::Gamepad> {
|
||||
if (player_index == 0) {
|
||||
return player1_gamepad_;
|
||||
}
|
||||
if (player_index == 1) {
|
||||
return player2_gamepad_;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Consulta de input para player 2
|
||||
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
|
||||
// Comprobar teclado con el mapa específico de P2
|
||||
|
||||
@@ -62,7 +62,9 @@ class Input {
|
||||
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
|
||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {}
|
||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}},
|
||||
{Action::START, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
|
||||
{Action::MENU, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}}} {}
|
||||
|
||||
~Gamepad() {
|
||||
if (pad != nullptr) {
|
||||
@@ -107,6 +109,10 @@ class Input {
|
||||
auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
|
||||
auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
|
||||
|
||||
// Accés al gamepad assignat per jugador (0=P1, 1=P2). nullptr si no n'hi
|
||||
// ha cap d'assignat o connectat. Usat per la UI de redefinició de botons.
|
||||
[[nodiscard]] auto getPlayerGamepad(int player_index) const -> std::shared_ptr<Gamepad>;
|
||||
|
||||
// Check if any player pressed any action from a list
|
||||
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||
|
||||
@@ -142,6 +148,7 @@ class Input {
|
||||
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||
void addGamepadMappingsFromFile();
|
||||
void discoverGamepads();
|
||||
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Input* instance; // Instancia única del singleton
|
||||
|
||||
@@ -6,6 +6,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||
{InputAction::RIGHT, "RIGHT"},
|
||||
{InputAction::THRUST, "THRUST"},
|
||||
{InputAction::SHOOT, "SHOOT"},
|
||||
{InputAction::START, "START"},
|
||||
{InputAction::MENU, "MENU"},
|
||||
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
||||
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
||||
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
||||
@@ -18,6 +20,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||
{"RIGHT", InputAction::RIGHT},
|
||||
{"THRUST", InputAction::THRUST},
|
||||
{"SHOOT", InputAction::SHOOT},
|
||||
{"START", InputAction::START},
|
||||
{"MENU", InputAction::MENU},
|
||||
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
||||
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
||||
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
||||
|
||||
@@ -15,6 +15,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j
|
||||
THRUST, // Acelerar
|
||||
SHOOT, // Disparar
|
||||
START, // Empezar match
|
||||
MENU, // Abrir/cerrar menu de servicio (equivalent a F12)
|
||||
|
||||
// Inputs de sistema (globales)
|
||||
WINDOW_INC_ZOOM, // F2
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// locale.cpp - Implementació del sistema de locale
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Recorre el node YAML i aplana jerarquies en claus "a.b.c". Suporta
|
||||
// mappings (recursió) i seqüències de strings (desa "a.b.0", "a.b.1"...).
|
||||
// Altres tipus (nombres, booleans solts) s'ignoren silenciosament.
|
||||
void flatten(const fkyaml::node& node, const std::string& prefix, std::unordered_map<std::string, std::string>& out) {
|
||||
if (node.is_mapping()) {
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
const std::string KEY = prefix.empty()
|
||||
? it.key().get_value<std::string>()
|
||||
: prefix + "." + it.key().get_value<std::string>();
|
||||
flatten(it.value(), KEY, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.is_sequence()) {
|
||||
std::size_t index = 0;
|
||||
for (const auto& item : node) {
|
||||
const std::string KEY = prefix + "." + std::to_string(index);
|
||||
flatten(item, KEY, out);
|
||||
index++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.is_string()) {
|
||||
out[prefix] = node.get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto Locale::get() -> Locale& {
|
||||
static Locale instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
auto Locale::load(const std::string& file_path) -> bool {
|
||||
// Normalitza traient prefix "data/" com fa StageLoader: el pack de
|
||||
// recursos indexa rutes relatives a `data/`.
|
||||
std::string normalized = file_path;
|
||||
if (normalized.starts_with("data/")) {
|
||||
normalized = normalized.substr(5);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bytes = Resource::Helper::loadFile(normalized);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "[Locale] no s'ha pogut load " << normalized << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
std::string yaml_content(bytes.begin(), bytes.end());
|
||||
std::stringstream stream(yaml_content);
|
||||
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
||||
strings_.clear();
|
||||
flatten(yaml, "", strings_);
|
||||
std::cout << "[Locale] " << strings_.size() << " traduccions des de " << normalized << '\n';
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Locale] error parsejant " << normalized << ": " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto Locale::switchTo(const std::string& lang) -> bool {
|
||||
return load("locale/" + lang + ".yaml");
|
||||
}
|
||||
|
||||
auto Locale::text(const std::string& key) const -> std::string {
|
||||
auto it = strings_.find(key);
|
||||
if (it != strings_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
std::cerr << "[Locale] clau no trobada: " << key << '\n';
|
||||
return key;
|
||||
}
|
||||
|
||||
auto Locale::count(const std::string& prefix) const -> std::size_t {
|
||||
std::size_t n = 0;
|
||||
while (strings_.contains(prefix + "." + std::to_string(n))) {
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string {
|
||||
auto pos = tpl.find(placeholder);
|
||||
if (pos != std::string::npos) {
|
||||
tpl.replace(pos, placeholder.size(), value);
|
||||
}
|
||||
return tpl;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// locale.hpp - Sistema d'internacionalització (i18n) basat en YAML
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Locale amb claus en notació de punts ("notification.fullscreen_on"). El YAML
|
||||
// pot ser jerarquitzat i s'aplana en càrrega, així el consumidor només veu
|
||||
// claus planes. Suporta seqüències de strings (es desen com prefix.0,
|
||||
// prefix.1, ...). No hi ha hot-swap d'idioma: es fixa a l'arrencada des de
|
||||
// `config.yaml` (camp `locale`) i només es recarrega reiniciant el joc.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
class Locale {
|
||||
public:
|
||||
static auto get() -> Locale&;
|
||||
|
||||
Locale(const Locale&) = delete;
|
||||
Locale(Locale&&) = delete;
|
||||
auto operator=(const Locale&) -> Locale& = delete;
|
||||
auto operator=(Locale&&) -> Locale& = delete;
|
||||
|
||||
// Llig el fitxer YAML i emplena el mapping intern. Si hi ha un error de
|
||||
// parse o el fitxer no existeix, deixa el mapping com estava i ho
|
||||
// notifica per stderr. Retorna true només si la càrrega ha tingut èxit.
|
||||
auto load(const std::string& file_path) -> bool;
|
||||
|
||||
// Canvi d'idioma en runtime. Recarrega `locale/<lang>.yaml`. Retorna true
|
||||
// si la càrrega ha tingut èxit. Els lookups posteriors (tots els draw*
|
||||
// criden Locale::text() cada frame) ja veuen el nou idioma. Els missatges
|
||||
// ja capturats (toast actiu, banner de stage start ja triat) sobreviuen
|
||||
// fins al seu cicle natural.
|
||||
auto switchTo(const std::string& lang) -> bool;
|
||||
|
||||
// Retorna la traducció; si la clau no existeix, retorna la pròpia clau
|
||||
// com a fallback visible (així una clau mal escrita es detecta sense
|
||||
// trencar el render).
|
||||
[[nodiscard]] auto text(const std::string& key) const -> std::string;
|
||||
|
||||
// Compta quantes claus consecutives existeixen amb el prefix donat
|
||||
// (prefix.0, prefix.1, ...). Útil per pools indexats com stage.start.N.
|
||||
[[nodiscard]] auto count(const std::string& prefix) const -> std::size_t;
|
||||
|
||||
private:
|
||||
Locale() = default;
|
||||
~Locale() = default;
|
||||
|
||||
std::unordered_map<std::string, std::string> strings_;
|
||||
};
|
||||
|
||||
// Substitució simple d'un placeholder dins una plantilla (p.ex. "{n}" → "3").
|
||||
// S'usa per interpolar valors runtime en strings traduïdes.
|
||||
[[nodiscard]] auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string;
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -390,6 +391,10 @@ namespace Rendering::GPU {
|
||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
|
||||
|
||||
// L'scissor és per render pass: en reobrir cal restaurar-lo des del top
|
||||
// de la pila si pushClip/popClip s'han usat mid-frame.
|
||||
applyCurrentScissor();
|
||||
|
||||
SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());
|
||||
|
||||
// UBO de líneas usa el tamaño lógico (también del offscreen).
|
||||
@@ -415,6 +420,11 @@ namespace Rendering::GPU {
|
||||
SDL_ReleaseGPUBuffer(dev, vbo);
|
||||
SDL_ReleaseGPUBuffer(dev, ibo);
|
||||
SDL_ReleaseGPUTransferBuffer(dev, tbo);
|
||||
|
||||
// Buidem el batch perquè pushClip/popClip puguin emetre seccions
|
||||
// separades dins el mateix frame sense re-enviar geometria.
|
||||
vertices_.clear();
|
||||
indices_.clear();
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::bloomPass() {
|
||||
@@ -603,6 +613,51 @@ namespace Rendering::GPU {
|
||||
SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::pushClip(int logical_x, int logical_y, int logical_w, int logical_h) {
|
||||
// Convertim coordenades lògiques (espai del joc, 1280×720) a píxels
|
||||
// físics del offscreen (render_w_ × render_h_). Si l'usuari hi treballa
|
||||
// amb upscale (p.ex. 1920×1080), l'scissor escala proporcionalment.
|
||||
const float SX = render_w_ / logical_w_;
|
||||
const float SY = render_h_ / logical_h_;
|
||||
SDL_Rect rect{
|
||||
.x = static_cast<int>(static_cast<float>(logical_x) * SX),
|
||||
.y = static_cast<int>(static_cast<float>(logical_y) * SY),
|
||||
.w = std::max(0, static_cast<int>(static_cast<float>(logical_w) * SX)),
|
||||
.h = std::max(0, static_cast<int>(static_cast<float>(logical_h) * SY)),
|
||||
};
|
||||
// Emetem tot el batch acumulat *abans* d'activar l'scissor perquè quedi
|
||||
// dibuixat sense retallar.
|
||||
flushBatch();
|
||||
clip_stack_.push_back(rect);
|
||||
applyCurrentScissor();
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::popClip() {
|
||||
// Emetem el batch que s'ha acumulat *dins* del clip actiu.
|
||||
flushBatch();
|
||||
if (!clip_stack_.empty()) {
|
||||
clip_stack_.pop_back();
|
||||
}
|
||||
applyCurrentScissor();
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::applyCurrentScissor() {
|
||||
if (render_pass_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
SDL_Rect rect{};
|
||||
if (clip_stack_.empty()) {
|
||||
// Sense clips: scissor cobreix tot el offscreen.
|
||||
rect.x = 0;
|
||||
rect.y = 0;
|
||||
rect.w = static_cast<int>(render_w_);
|
||||
rect.h = static_cast<int>(render_h_);
|
||||
} else {
|
||||
rect = clip_stack_.back();
|
||||
}
|
||||
SDL_SetGPUScissor(render_pass_, &rect);
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::endFrame() {
|
||||
if (cmd_buffer_ == nullptr) {
|
||||
return;
|
||||
|
||||
@@ -94,6 +94,15 @@ namespace Rendering::GPU {
|
||||
// d'UI (notificacions, panels).
|
||||
void pushRect(float x, float y, float w, float h, float r, float g, float b, float a);
|
||||
|
||||
// Clipping rectangular per a UI (scissor a SDL_GPU). pushClip/popClip
|
||||
// forcen un flush intermedi del batch i activen/restauren l'scissor del
|
||||
// pase actiu. Coordenades en píxels lògics del joc (1280×720); es
|
||||
// converteixen a píxels físics del offscreen automàticament. Stack
|
||||
// d'scissors per a clips niats. Quan la pila queda buida, l'scissor
|
||||
// torna a cobrir el target sencer.
|
||||
void pushClip(int logical_x, int logical_y, int logical_w, int logical_h);
|
||||
void popClip();
|
||||
|
||||
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
||||
void endFrame();
|
||||
|
||||
@@ -168,6 +177,10 @@ namespace Rendering::GPU {
|
||||
std::vector<LineVertex> vertices_;
|
||||
std::vector<uint16_t> indices_;
|
||||
|
||||
// Pila d'scissors actius en píxels físics del offscreen. Buida = sense
|
||||
// clip (full target). Cada push/pop fa un flushBatch i reaplica scissor.
|
||||
std::vector<SDL_Rect> clip_stack_;
|
||||
|
||||
// Estado del frame en curso.
|
||||
SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
|
||||
SDL_GPUTexture* swapchain_texture_{nullptr};
|
||||
@@ -190,6 +203,7 @@ namespace Rendering::GPU {
|
||||
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
||||
void compositePass();
|
||||
void applyFinalViewport();
|
||||
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "core/defaults/rendering.hpp"
|
||||
#include "core/defaults/window.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/coordinate_transform.hpp"
|
||||
#include "core/system/notifier.hpp"
|
||||
#include "project.h"
|
||||
@@ -250,7 +251,10 @@ void SDLManager::increaseWindowSize() {
|
||||
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
|
||||
applyZoom(new_zoom);
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_));
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.zoom"),
|
||||
"{z}",
|
||||
std::format("{:.1f}", zoom_factor_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +265,10 @@ void SDLManager::decreaseWindowSize() {
|
||||
float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
|
||||
applyZoom(new_zoom);
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_));
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.zoom"),
|
||||
"{z}",
|
||||
std::format("{:.1f}", zoom_factor_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +317,7 @@ void SDLManager::toggleFullscreen() {
|
||||
Mouse::setForceHidden(is_fullscreen_);
|
||||
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(is_fullscreen_ ? "PANTALLA COMPLETA" : "MODE FINESTRA");
|
||||
notifier->notifyInfo(Locale::get().text(is_fullscreen_ ? "notification.fullscreen_on" : "notification.fullscreen_off"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +371,7 @@ void SDLManager::toggleVSync() {
|
||||
on_persist_();
|
||||
}
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(cfg_->rendering.vsync != 0 ? "VSYNC ACTIU" : "VSYNC INACTIU");
|
||||
notifier->notifyInfo(Locale::get().text(cfg_->rendering.vsync != 0 ? "notification.vsync_on" : "notification.vsync_off"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +381,27 @@ void SDLManager::toggleAntialias() {
|
||||
// No persistim: l'AA és toggleable runtime però el seu estat no es
|
||||
// guarda al YAML de moment (decisió volgudament conservadora).
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(cfg_->rendering.antialias != 0 ? "AA ACTIU" : "AA INACTIU");
|
||||
notifier->notifyInfo(Locale::get().text(cfg_->rendering.antialias != 0 ? "notification.antialias_on" : "notification.antialias_off"));
|
||||
}
|
||||
}
|
||||
|
||||
void SDLManager::setRenderResolution(int w, int h) {
|
||||
if (!Defaults::Rendering::isValidRenderResolution(w, h)) {
|
||||
std::cerr << "[SDLManager] Resolucio no valida (" << w << "x" << h
|
||||
<< "), ignorant.\n";
|
||||
return;
|
||||
}
|
||||
if (w == cfg_->rendering.render_width && h == cfg_->rendering.render_height) {
|
||||
return; // ja era l'actual
|
||||
}
|
||||
if (!gpu_renderer_.resizeRenderTarget(static_cast<float>(w), static_cast<float>(h))) {
|
||||
std::cerr << "[SDLManager] resizeRenderTarget ha fallat.\n";
|
||||
return;
|
||||
}
|
||||
cfg_->rendering.render_width = w;
|
||||
cfg_->rendering.render_height = h;
|
||||
if (on_persist_) {
|
||||
on_persist_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +411,6 @@ void SDLManager::togglePostFx() {
|
||||
// No persistim: el toggle és per A/B testing visual, l'estat per defecte
|
||||
// del joc continua sent "postfx ON" segons defaults/YAML.
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(NEW_STATE ? "POSTPROCESSAT ACTIU" : "POSTPROCESSAT INACTIU");
|
||||
notifier->notifyInfo(Locale::get().text(NEW_STATE ? "notification.postfx_on" : "notification.postfx_off"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ class SDLManager {
|
||||
void toggleVSync(); // F4
|
||||
void toggleAntialias(); // F5
|
||||
void togglePostFx(); // F6
|
||||
// Canvia la resolució del render target offscreen (recrea la textura).
|
||||
// Cal cridar-lo fora d'un frame (event phase, no draw phase). Si el
|
||||
// valor no es un preset valid o ja es l'actual, es no-op.
|
||||
void setRenderResolution(int w, int h);
|
||||
auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED
|
||||
|
||||
// Funciones principals (renderizado).
|
||||
@@ -47,6 +51,8 @@ class SDLManager {
|
||||
// Getters
|
||||
auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
|
||||
[[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; }
|
||||
[[nodiscard]] auto isFullscreen() const -> bool { return is_fullscreen_; }
|
||||
[[nodiscard]] auto isPostFxEnabled() const -> bool { return gpu_renderer_.isPostFxEnabled(); }
|
||||
|
||||
// [NUEVO] Actualitzar context de renderizado (factor de scale global)
|
||||
void updateRenderingContext() const;
|
||||
|
||||
@@ -9,29 +9,34 @@
|
||||
|
||||
namespace Resource::Helper {
|
||||
|
||||
// Inicialitzar el sistema de recursos
|
||||
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
|
||||
// Inicialitzar el sistema de recursos
|
||||
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
|
||||
return Loader::get().initialize(pack_file, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar un file
|
||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
// Carregar un file
|
||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
// Normalitzar la ruta
|
||||
std::string normalized = normalizePath(filepath);
|
||||
|
||||
// Carregar del sistema de recursos
|
||||
return Loader::get().loadResource(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
// Comprovar si existeix un file
|
||||
auto fileExists(const std::string& filepath) -> bool {
|
||||
// Llistar recursos amb un prefix donat
|
||||
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);
|
||||
return Loader::get().resourceExists(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtenir ruta normalitzada per al paquet
|
||||
// Elimina prefixos "data/", rutes absolutes, etc.
|
||||
auto getPackPath(const std::string& asset_path) -> std::string {
|
||||
// Obtenir ruta normalitzada per al paquet
|
||||
// Elimina prefixos "data/", rutes absolutes, etc.
|
||||
auto getPackPath(const std::string& asset_path) -> std::string {
|
||||
std::string path = asset_path;
|
||||
|
||||
// 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, '\\', '/');
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalitzar ruta (alias de getPackPath)
|
||||
auto normalizePath(const std::string& path) -> std::string {
|
||||
// Normalitzar ruta (alias de getPackPath)
|
||||
auto normalizePath(const std::string& path) -> std::string {
|
||||
return getPackPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Comprovar si hay paquet carregat
|
||||
auto isPackLoaded() -> bool {
|
||||
// Comprovar si hay paquet carregat
|
||||
auto isPackLoaded() -> bool {
|
||||
return Loader::get().isPackLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resource::Helper
|
||||
|
||||
@@ -10,18 +10,21 @@
|
||||
|
||||
namespace Resource::Helper {
|
||||
|
||||
// Inicialización del sistema
|
||||
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
|
||||
// Inicialización del sistema
|
||||
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
|
||||
|
||||
// Càrrega de archivos
|
||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
|
||||
auto fileExists(const std::string& filepath) -> bool;
|
||||
// Càrrega de archivos
|
||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
|
||||
auto fileExists(const std::string& filepath) -> bool;
|
||||
|
||||
// Normalització de rutes
|
||||
auto getPackPath(const std::string& asset_path) -> std::string;
|
||||
auto normalizePath(const std::string& path) -> std::string;
|
||||
// Llistat de recursos disponibles amb un prefix (ex. "shapes/", "sounds/").
|
||||
auto listResources(const std::string& prefix) -> std::vector<std::string>;
|
||||
|
||||
// Estat
|
||||
auto isPackLoaded() -> bool;
|
||||
// Normalització de rutes
|
||||
auto getPackPath(const std::string& asset_path) -> std::string;
|
||||
auto normalizePath(const std::string& path) -> std::string;
|
||||
|
||||
// Estat
|
||||
auto isPackLoaded() -> bool;
|
||||
|
||||
} // namespace Resource::Helper
|
||||
|
||||
@@ -3,20 +3,21 @@
|
||||
|
||||
#include "resource_loader.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Singleton
|
||||
auto Loader::get() -> Loader& {
|
||||
// Singleton
|
||||
auto Loader::get() -> Loader& {
|
||||
static Loader instance_;
|
||||
return instance_;
|
||||
}
|
||||
}
|
||||
|
||||
// Inicialitzar el sistema de recursos
|
||||
auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
|
||||
// Inicialitzar el sistema de recursos
|
||||
auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
|
||||
fallback_enabled_ = enable_fallback;
|
||||
|
||||
// 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";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar un recurs
|
||||
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||
// Carregar un recurs
|
||||
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||
// Intentar load del paquet primer
|
||||
if (pack_) {
|
||||
if (pack_->hasResource(filename)) {
|
||||
@@ -65,10 +66,46 @@ auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Comprovar si existeix un recurs
|
||||
auto Loader::resourceExists(const std::string& filename) -> bool {
|
||||
auto Loader::listResources(const std::string& prefix) -> std::vector<std::string> {
|
||||
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
|
||||
if (pack_ && pack_->hasResource(filename)) {
|
||||
return true;
|
||||
@@ -81,36 +118,36 @@ auto Loader::resourceExists(const std::string& filename) -> bool {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validar el paquet
|
||||
auto Loader::validatePack() -> bool {
|
||||
// Validar el paquet
|
||||
auto Loader::validatePack() -> bool {
|
||||
if (!pack_) {
|
||||
std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
return pack_->validatePack();
|
||||
}
|
||||
}
|
||||
|
||||
// Comprovar si hay paquet carregat
|
||||
auto Loader::isPackLoaded() const -> bool {
|
||||
// Comprovar si hay paquet carregat
|
||||
auto Loader::isPackLoaded() const -> bool {
|
||||
return pack_ != nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Establir la ruta base
|
||||
void Loader::setBasePath(const std::string& path) {
|
||||
// Establir la ruta base
|
||||
void Loader::setBasePath(const std::string& path) {
|
||||
base_path_ = path;
|
||||
std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Obtenir la ruta base
|
||||
auto Loader::getBasePath() const -> const std::string& {
|
||||
// Obtenir la ruta base
|
||||
auto Loader::getBasePath() const -> const std::string& {
|
||||
return base_path_;
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar des del sistema de archivos (fallback)
|
||||
auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> {
|
||||
// Carregar des del sistema de archivos (fallback)
|
||||
auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> {
|
||||
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
|
||||
// We need to prepend base_path + "data/"
|
||||
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";
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Singleton per gestionar la càrrega de recursos
|
||||
class Loader {
|
||||
// Singleton per gestionar la càrrega de recursos
|
||||
class Loader {
|
||||
public:
|
||||
// Singleton
|
||||
static auto get() -> Loader&;
|
||||
@@ -25,6 +25,11 @@ class Loader {
|
||||
auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
|
||||
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ó
|
||||
auto validatePack() -> bool;
|
||||
[[nodiscard]] auto isPackLoaded() const -> bool;
|
||||
@@ -48,6 +53,6 @@ class Loader {
|
||||
|
||||
// Funciones auxiliars
|
||||
auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>;
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
|
||||
@@ -2,20 +2,33 @@
|
||||
|
||||
#include "core/system/debug_overlay.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace System {
|
||||
|
||||
namespace {
|
||||
namespace Cfg = Defaults::Hud::DebugOverlay;
|
||||
|
||||
auto toUpperAscii(std::string s) -> std::string {
|
||||
for (char& c : s) {
|
||||
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DebugOverlay::DebugOverlay(Rendering::Renderer* renderer,
|
||||
const Config::RenderingConfig& rendering_cfg)
|
||||
: text_(renderer),
|
||||
renderer_(renderer),
|
||||
rendering_cfg_(&rendering_cfg) {}
|
||||
|
||||
void DebugOverlay::update(float delta_time) {
|
||||
@@ -23,7 +36,7 @@ namespace System {
|
||||
fps_frame_count_++;
|
||||
|
||||
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_accumulator_ = 0.0F;
|
||||
}
|
||||
@@ -35,23 +48,43 @@ namespace System {
|
||||
}
|
||||
|
||||
const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_);
|
||||
const std::string RES_TEXT = "RES: " + std::to_string(rendering_cfg_->render_width) + "X" + std::to_string(rendering_cfg_->render_height);
|
||||
const char* driver_raw = SDL_GetGPUDeviceDriver(renderer_->device().get());
|
||||
const std::string DRIVER_TEXT = "DRIVER: " + toUpperAscii(driver_raw != nullptr ? driver_raw : "?");
|
||||
const std::string VSYNC_TEXT = std::string("VSYNC: ") + (rendering_cfg_->vsync == 1 ? "ON" : "OFF");
|
||||
const std::string AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF");
|
||||
|
||||
float y = Cfg::Y_FPS;
|
||||
text_.render(FPS_TEXT,
|
||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS},
|
||||
Vec2{.x = Cfg::X, .y = y},
|
||||
Cfg::FPS_SCALE,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BRIGHTNESS,
|
||||
Cfg::COLOR);
|
||||
y += Cfg::FPS_LINE_HEIGHT;
|
||||
text_.render(RES_TEXT,
|
||||
Vec2{.x = Cfg::X, .y = y},
|
||||
Cfg::TEXT_SCALE,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BRIGHTNESS,
|
||||
Cfg::COLOR);
|
||||
y += Cfg::LINE_HEIGHT;
|
||||
text_.render(DRIVER_TEXT,
|
||||
Vec2{.x = Cfg::X, .y = y},
|
||||
Cfg::TEXT_SCALE,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BRIGHTNESS,
|
||||
Cfg::COLOR);
|
||||
y += Cfg::LINE_HEIGHT;
|
||||
text_.render(VSYNC_TEXT,
|
||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + Cfg::LINE_HEIGHT},
|
||||
Vec2{.x = Cfg::X, .y = y},
|
||||
Cfg::TEXT_SCALE,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BRIGHTNESS,
|
||||
Cfg::COLOR);
|
||||
y += Cfg::LINE_HEIGHT;
|
||||
text_.render(AA_TEXT,
|
||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + (2.0F * Cfg::LINE_HEIGHT)},
|
||||
Vec2{.x = Cfg::X, .y = y},
|
||||
Cfg::TEXT_SCALE,
|
||||
Cfg::TEXT_SPACING,
|
||||
Cfg::BRIGHTNESS,
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace System {
|
||||
|
||||
private:
|
||||
Graphics::VectorText text_;
|
||||
Rendering::Renderer* renderer_;
|
||||
const Config::RenderingConfig* rendering_cfg_;
|
||||
bool visible_{false};
|
||||
|
||||
|
||||
@@ -11,14 +11,17 @@
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/audio/audio_adapter.hpp"
|
||||
#include "core/defaults/audio.hpp"
|
||||
#include "core/defaults/window.hpp"
|
||||
#include "core/graphics/shape_loader.hpp"
|
||||
#include "core/input/define_inputs.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/sdl_manager.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "core/resources/resource_loader.hpp"
|
||||
#include "core/system/notifier.hpp"
|
||||
#include "core/system/service_menu.hpp"
|
||||
#include "core/utils/path_utils.hpp"
|
||||
#include "debug_overlay.hpp"
|
||||
#include "game/config_yaml.hpp"
|
||||
@@ -99,9 +102,41 @@ Director::Director(int argc, char* argv[])
|
||||
// Carregar o crear configuración
|
||||
ConfigYaml::loadFromFile();
|
||||
|
||||
// Carregar locale segons la config (per defecte "ca"). Si la càrrega
|
||||
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
|
||||
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
|
||||
|
||||
// Inicialitzar sistema de input
|
||||
Input::init("data/gamecontrollerdb.txt");
|
||||
|
||||
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
|
||||
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
|
||||
// P2 = pad 1 si existeix) i ho persistim. Aixo nomes dispara amb tots
|
||||
// dos buits perque un "SENSE MANDO" explicit ha de sobreviure entre
|
||||
// arrancades.
|
||||
{
|
||||
auto& p1 = cfg_->player1;
|
||||
auto& p2 = cfg_->player2;
|
||||
const bool BOTH_EMPTY = p1.gamepad_name.empty() && p1.gamepad_path.empty() && p2.gamepad_name.empty() && p2.gamepad_path.empty();
|
||||
if (BOTH_EMPTY) {
|
||||
const auto& pads = Input::get()->getGamepads();
|
||||
bool changed = false;
|
||||
if (!pads.empty() && pads[0]) {
|
||||
p1.gamepad_name = pads[0]->name;
|
||||
p1.gamepad_path = pads[0]->path;
|
||||
changed = true;
|
||||
}
|
||||
if (pads.size() > 1 && pads[1]) {
|
||||
p2.gamepad_name = pads[1]->name;
|
||||
p2.gamepad_path = pads[1]->path;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
ConfigYaml::saveToFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar configuración de controls dels jugadors
|
||||
Input::get()->applyPlayer1Bindings(cfg_->player1);
|
||||
Input::get()->applyPlayer2Bindings(cfg_->player2);
|
||||
@@ -132,20 +167,31 @@ Director::Director(int argc, char* argv[])
|
||||
}
|
||||
|
||||
const Audio::Config AUDIO_CONFIG{
|
||||
.enabled = Defaults::Audio::ENABLED,
|
||||
.volume = Defaults::Audio::VOLUME,
|
||||
.music_enabled = Defaults::Audio::MUSIC_ENABLED,
|
||||
.music_volume = Defaults::Audio::MUSIC_VOLUME,
|
||||
.sound_enabled = Defaults::Audio::SOUND_ENABLED,
|
||||
.sound_volume = Defaults::Audio::SOUND_VOLUME,
|
||||
.enabled = cfg_->audio.enabled,
|
||||
.volume = cfg_->audio.volume,
|
||||
.music_enabled = cfg_->audio.music_enabled,
|
||||
.music_volume = cfg_->audio.music_volume,
|
||||
.sound_enabled = cfg_->audio.sound_enabled,
|
||||
.sound_volume = cfg_->audio.sound_volume,
|
||||
};
|
||||
Audio::init(AUDIO_CONFIG);
|
||||
Audio::get()->applySettings(AUDIO_CONFIG);
|
||||
|
||||
AudioResource::getMusic("title.ogg");
|
||||
AudioResource::getMusic("game.ogg");
|
||||
// Precàrrega blocant de tots els recursos al boot per evitar hits d'I/O i
|
||||
// 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) {
|
||||
std::cout << "Música precacheada\n";
|
||||
std::cout << "Recursos precachejats (música, sons, shapes)\n";
|
||||
}
|
||||
|
||||
context_ = std::make_unique<SceneContext>();
|
||||
@@ -160,6 +206,8 @@ Director::Director(int argc, char* argv[])
|
||||
cfg_->rendering);
|
||||
|
||||
System::Notifier::init(sdl_->getRenderer());
|
||||
System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get());
|
||||
System::DefineInputs::init(sdl_->getRenderer());
|
||||
|
||||
last_ticks_ms_ = SDL_GetTicks();
|
||||
}
|
||||
@@ -174,6 +222,8 @@ Director::~Director() {
|
||||
// l'hem de cridar nosaltres.
|
||||
current_scene_.reset();
|
||||
debug_overlay_.reset();
|
||||
System::DefineInputs::destroy();
|
||||
System::ServiceMenu::destroy();
|
||||
System::Notifier::destroy();
|
||||
context_.reset();
|
||||
sdl_.reset();
|
||||
@@ -354,6 +404,12 @@ auto Director::iterate() -> SDL_AppResult {
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->update(delta_time);
|
||||
}
|
||||
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||
menu->update(delta_time);
|
||||
}
|
||||
if (auto* di = System::DefineInputs::get(); di != nullptr) {
|
||||
di->update(delta_time);
|
||||
}
|
||||
Audio::update();
|
||||
|
||||
// Si la swapchain no està disponible (finestra minimitzada, etc.),
|
||||
@@ -367,6 +423,17 @@ auto Director::iterate() -> SDL_AppResult {
|
||||
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->draw(); // toast: per damunt de tot
|
||||
}
|
||||
// Mentre l'overlay de redefinicio esta actiu, amaguem el menu de servei
|
||||
// (encara queda "open" per a absorbir events un cop el modal s'auto-tanqui,
|
||||
// pero no es pinta per no confondre's visualment amb el modal).
|
||||
const auto* di = System::DefineInputs::get();
|
||||
const bool DEFINE_ACTIVE = (di != nullptr) && di->isActive();
|
||||
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && !DEFINE_ACTIVE) {
|
||||
menu->draw(); // service menu: per damunt fins i tot dels toasts
|
||||
}
|
||||
if (di != nullptr) {
|
||||
di->draw(); // overlay de rebind: per damunt de tot
|
||||
}
|
||||
sdl_->present();
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/input/define_inputs.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/sdl_manager.hpp"
|
||||
#include "core/system/notifier.hpp"
|
||||
#include "core/system/service_menu.hpp"
|
||||
#include "game/config_yaml.hpp"
|
||||
#include "scene_context.hpp"
|
||||
|
||||
// Using declarations per simplificar el codi
|
||||
@@ -17,6 +21,81 @@ using SceneType = SceneContext::SceneType;
|
||||
|
||||
namespace GlobalEvents {
|
||||
|
||||
namespace {
|
||||
|
||||
// Reenvia events al menu de servei si esta obert. Accepta:
|
||||
// - KEY_DOWN (excepte F1-F12 i ESC, que sempre passen com a globals)
|
||||
// - GAMEPAD_BUTTON_DOWN (per navegacio amb dpad + FIRE/ACCELERATE)
|
||||
// - GAMEPAD_AXIS_MOTION (per navegacio amb stick)
|
||||
// Retorna true si l'event s'ha entregat al menu.
|
||||
auto forwardToServiceMenu(const SDL_Event& event) -> bool {
|
||||
auto* menu = System::ServiceMenu::get();
|
||||
if (menu == nullptr || !menu->isOpen()) {
|
||||
return false;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
const SDL_Scancode SC = event.key.scancode;
|
||||
const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) ||
|
||||
(SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12);
|
||||
if (PASSTHROUGH) {
|
||||
return false;
|
||||
}
|
||||
menu->handleEvent(event);
|
||||
return true;
|
||||
}
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
menu->handleEvent(event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Engoleix els events que DefineInputs vol consumir mentre l'overlay
|
||||
// es actiu. Els events que el modul torna a passar (QUIT, ESC) cauen
|
||||
// cap al pipeline normal i poden tancar la finestra o obrir el prompt
|
||||
// d'eixida sense haver de completar la sequencia.
|
||||
auto consumeIfDefineActive(const SDL_Event& event) -> bool {
|
||||
auto* di = System::DefineInputs::get();
|
||||
if (di == nullptr || !di->isActive()) {
|
||||
return false;
|
||||
}
|
||||
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
|
||||
|
||||
auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool {
|
||||
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
|
||||
auto event_msg = Input::get()->handleEvent(event);
|
||||
@@ -24,6 +103,12 @@ namespace GlobalEvents {
|
||||
std::cout << "[Input] " << event_msg << '\n';
|
||||
}
|
||||
|
||||
// 1b. Si l'overlay de redefinicio esta actiu, engoleix tots els events
|
||||
// (cap arriba al joc, al menu de servei ni als hotkeys F1-F12).
|
||||
if (consumeIfDefineActive(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
context.setNextScene(SceneType::EXIT);
|
||||
@@ -34,7 +119,20 @@ namespace GlobalEvents {
|
||||
// 3. Gestió del ratolí (auto-ocultar)
|
||||
Mouse::handleEvent(event);
|
||||
|
||||
// 4. Procesar acciones globales directamente desde eventos SDL
|
||||
// 3b. Botó MENU al mando (equivalent a F12)
|
||||
if (handleGamepadMenuButton(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de
|
||||
// funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen,
|
||||
// vsync, AA, postfx, locale, exit prompt). Aixi el menu captura
|
||||
// ENTER/BACKSPACE/UP/DOWN/LEFT/RIGHT i lletres mentre esta obert.
|
||||
if (forwardToServiceMenu(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. Procesar acciones globales directamente desde eventos SDL
|
||||
// (NO usar Input::checkAction() para evitar desfase de timing)
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
switch (event.key.scancode) {
|
||||
@@ -62,6 +160,35 @@ namespace GlobalEvents {
|
||||
sdl.togglePostFx();
|
||||
return true;
|
||||
|
||||
case SDL_SCANCODE_F7: {
|
||||
// Toggle d'idioma en runtime entre català i anglès. Els
|
||||
// strings ja capturats (toast actiu, banner stage start)
|
||||
// sobreviuen fins al seu cicle; la resta (HUD, pantalles,
|
||||
// pròxims toasts) es refresquen al següent frame perquè
|
||||
// criden Locale::text() cada draw.
|
||||
const std::string NEW_LANG = (ConfigYaml::engine_config.locale == "ca") ? "en" : "ca";
|
||||
if (Locale::get().switchTo(NEW_LANG)) {
|
||||
ConfigYaml::engine_config.locale = NEW_LANG;
|
||||
ConfigYaml::saveToFile();
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.locale_switched"),
|
||||
"{lang}",
|
||||
Locale::get().text("language." + NEW_LANG)));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case SDL_SCANCODE_F12: {
|
||||
// Toggle del menu de servei. Sempre passa com a global
|
||||
// (alterna obert/tancat des de qualsevol escena).
|
||||
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||
menu->toggle();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case SDL_SCANCODE_ESCAPE: {
|
||||
// Doble pulsació per confirmar sortida: la primera ESC
|
||||
// dispara un toast d'avís; només si aquest toast concret
|
||||
@@ -71,7 +198,7 @@ namespace GlobalEvents {
|
||||
// sortida en lloc de tancar.
|
||||
auto* notifier = System::Notifier::get();
|
||||
if (notifier != nullptr && !notifier->isExitPromptActive()) {
|
||||
notifier->notifyExit("PREMEU ESC UN ALTRE COP PER EIXIR");
|
||||
notifier->notifyExit(Locale::get().text("notification.press_again_exit"));
|
||||
return true;
|
||||
}
|
||||
// Notifier inexistent (degradació elegant) o segona ESC
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// relaunch.cpp - Implementacio del reinici en calent
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/system/relaunch.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h> // _execv
|
||||
#else
|
||||
#include <unistd.h> // execv
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// Estat global (process-scope). Aquesta TU es la unica que gestiona el
|
||||
// reinici, aixi que els static interns no s'escapen.
|
||||
char** g_argv = nullptr;
|
||||
bool g_requested = false;
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace System::Relaunch {
|
||||
|
||||
void setArgv(int /*argc*/, char** argv) {
|
||||
g_argv = argv;
|
||||
}
|
||||
|
||||
void request() {
|
||||
g_requested = true;
|
||||
}
|
||||
|
||||
auto isRequested() -> bool {
|
||||
return g_requested;
|
||||
}
|
||||
|
||||
void execIfRequested() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// Al navegador el reinici real seria location.reload(); aqui no fem res.
|
||||
return;
|
||||
#else
|
||||
if (!g_requested || g_argv == nullptr || g_argv[0] == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::cout << "[Relaunch] Reiniciant " << g_argv[0] << "...\n";
|
||||
#ifdef _WIN32
|
||||
_execv(g_argv[0], g_argv);
|
||||
#else
|
||||
execv(g_argv[0], g_argv);
|
||||
#endif
|
||||
// Si arribem aqui, execv ha fallat. Tots els subsistemes ja estan
|
||||
// destruits; sortim amb error i el shell rebra el codi.
|
||||
std::cerr << "[Relaunch] Ha fallat: " << std::strerror(errno) << '\n';
|
||||
std::exit(EXIT_FAILURE);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace System::Relaunch
|
||||
@@ -0,0 +1,33 @@
|
||||
// relaunch.hpp - Reinici en calent del proces (execv)
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Helper desacoblat per a permetre que el menu de servei demani un reinici
|
||||
// sense conèixer Director ni main.cpp. Patro:
|
||||
//
|
||||
// main() → Relaunch::setArgv(argc, argv) (a l'arrencada)
|
||||
// ServiceMenu → Relaunch::request() (en activar REINICIAR)
|
||||
// main() → Relaunch::execIfRequested() (a SDL_AppQuit)
|
||||
//
|
||||
// L'execv() reemplaca el proces actual: si torna, ha fallat. A EMSCRIPTEN
|
||||
// no es pot reiniciar; isRequested() seguira dient true pero execIfRequested
|
||||
// sera no-op.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace System::Relaunch {
|
||||
|
||||
// Emmagatzema l'argv original. Cal cridar-ho una vegada des de main.
|
||||
void setArgv(int argc, char** argv);
|
||||
|
||||
// Demana un reinici (no actua immediatament; nomes marca el flag).
|
||||
void request();
|
||||
|
||||
// Consulta del flag.
|
||||
[[nodiscard]] auto isRequested() -> bool;
|
||||
|
||||
// Si hi ha reinici demanat i tenim argv valid, fa execv. En cas d'exit
|
||||
// no torna. Si execv falla, registra l'error i torna; el caller hauria
|
||||
// de sortir normalment.
|
||||
void execIfRequested();
|
||||
|
||||
} // namespace System::Relaunch
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
||||
// service_menu.hpp - Menu de servei (singleton)
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Overlay de configuracio global accessible amb F12 des de qualsevol escena
|
||||
// (LOGO, TITLE, GAME). Captura tots els KEY_DOWN excepte F1-F12 i ESC, que
|
||||
// continuen arribant a GlobalEvents. Mentre esta obert, GameScene::update()
|
||||
// fa early return per pausar el joc; LOGO i TITLE continuen renderitzant-se
|
||||
// sota el menu.
|
||||
//
|
||||
// Arquitectura inspirada en aee_arcade service_menu.{hpp,cpp}: pila de
|
||||
// pagines amb cursor, animacio open/close amb easing easeOutQuad i clipping
|
||||
// del contingut mentre la caixa creix/decreix.
|
||||
//
|
||||
// API singleton equivalent a Notifier: init() al startup amb un renderer,
|
||||
// get() retorna el punter, destroy() al teardown.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
class SDLManager;
|
||||
|
||||
namespace System {
|
||||
|
||||
class DebugOverlay;
|
||||
|
||||
class ServiceMenu {
|
||||
public:
|
||||
// Tipus d'item de menu. En aquesta iteracio nomes s'usen SUBMENU i
|
||||
// LABEL; la resta queden reservats per a iteracions futures (toggles
|
||||
// de vsync/zoom, picker d'idioma, restart, exit...).
|
||||
enum class Kind : std::uint8_t {
|
||||
LABEL, // No interactiu, nomes es dibuixa
|
||||
TOGGLE, // bool flip — reservat
|
||||
CYCLE, // index amb modul — reservat
|
||||
INT_RANGE, // step ± — reservat
|
||||
SUBMENU, // pushPage en activar — usat
|
||||
ACTION // call al lambda en activar — reservat
|
||||
};
|
||||
|
||||
struct Item {
|
||||
Kind kind = Kind::LABEL;
|
||||
std::string label_key; // Clau de locale (s'ignora si label_text no esta buit)
|
||||
std::string label_text; // Text literal (no locale). Util per a labels que no necessiten traduccio (resolucions, etc.)
|
||||
bool selectable = true;
|
||||
// SUBMENU / ACTION: callback en ENTER / RIGHT.
|
||||
std::function<void()> on_activate;
|
||||
// TOGGLE / CYCLE / INT_RANGE: text del valor actual (renderitzat a la dreta).
|
||||
std::function<std::string()> get_value_text;
|
||||
// TOGGLE / CYCLE / INT_RANGE: callback amb +1 (RIGHT/ENTER) o -1 (LEFT).
|
||||
std::function<void(int)> on_change;
|
||||
};
|
||||
|
||||
struct Page {
|
||||
std::string title_key;
|
||||
// Subtitol opcional, renderitzat sota el titol amb tipografia mes
|
||||
// petita i color apagat. Es una funcio perque pot ser dinamic
|
||||
// (versio+hash, etc.). Si esta buit, no es renderitza.
|
||||
std::function<std::string()> subtitle_provider;
|
||||
std::vector<Item> items;
|
||||
std::size_t cursor = 0;
|
||||
};
|
||||
|
||||
// Inicialitza el singleton amb el renderer global, l'SDLManager (video
|
||||
// toggles: fullscreen, vsync, AA, postfx, zoom) i el DebugOverlay
|
||||
// (toggle del HUD de debug a OPCIONS). Tots propietat del Director.
|
||||
static void init(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
||||
static void destroy();
|
||||
[[nodiscard]] static auto get() -> ServiceMenu*;
|
||||
|
||||
// F12: alterna obrir/tancar amb animacio.
|
||||
void toggle();
|
||||
[[nodiscard]] auto isOpen() const -> bool;
|
||||
|
||||
void update(float delta_time);
|
||||
void draw() const;
|
||||
|
||||
// Processa events de navegacio. Retorna true si l'event s'ha consumit.
|
||||
// Accepta:
|
||||
// - SDL_EVENT_KEY_DOWN: UP/DOWN/ENTER/RIGHT/LEFT/BACKSPACE.
|
||||
// - SDL_EVENT_GAMEPAD_BUTTON_DOWN: DPAD per nav, FIRE = ENTER,
|
||||
// ACCELERATE = BACK. La resta de botons s'ignoren.
|
||||
// - SDL_EVENT_GAMEPAD_AXIS_MOTION: stick X/Y amb edge-detect.
|
||||
auto handleEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
private:
|
||||
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
||||
|
||||
// Sub-handlers de handleEvent. Privats, no son part de l'API publica.
|
||||
auto handleKeyDown(const SDL_Event& event) -> bool;
|
||||
auto handleGamepadButton(const SDL_Event& event) -> bool;
|
||||
auto handleGamepadAxis(const SDL_Event& event) -> bool;
|
||||
// Helpers per a cada eix; permeten que handleGamepadAxis es quedi
|
||||
// com a dispatcher i no bote el llindar de complexitat.
|
||||
void processStickX(Sint16 val);
|
||||
void processStickY(Sint16 val);
|
||||
void processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held);
|
||||
|
||||
void buildRootPage();
|
||||
[[nodiscard]] auto buildVideoPage() -> Page;
|
||||
[[nodiscard]] auto buildResolutionPage() const -> Page;
|
||||
[[nodiscard]] static auto buildAudioPage() -> Page;
|
||||
[[nodiscard]] auto buildOptionsPage() const -> Page;
|
||||
[[nodiscard]] auto buildSystemPage() -> Page;
|
||||
[[nodiscard]] auto buildControlsPage() -> Page;
|
||||
// Llista de mandos detectats per a un jugador. Cada item assigna el
|
||||
// pad triat (amb swap automatic si l'altre jugador ja el tenia) i
|
||||
// tanca la picker amb popPage. L'ultim item es "SENSE MANDO" per a
|
||||
// desasignar.
|
||||
[[nodiscard]] auto buildPadPickerPage(int player_index) -> Page;
|
||||
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
|
||||
// l'usuari selecciona SI; el cursor per defecte apunta a NO.
|
||||
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
|
||||
void pushPage(Page page);
|
||||
void popPage();
|
||||
void moveCursor(int direction);
|
||||
void activateCurrent();
|
||||
// RIGHT (direction=+1) / LEFT (direction=-1). Per a TOGGLE/CYCLE/INT_RANGE
|
||||
// crida on_change. Per a SUBMENU/ACTION nomes +1 (entra/activa).
|
||||
void changeValue(int direction);
|
||||
|
||||
// Alçada objectiu de la caixa per a la pagina superior (sense animacio).
|
||||
[[nodiscard]] auto computeTargetHeight() const -> float;
|
||||
|
||||
// Ample objectiu de la caixa per a la pagina superior (sense animacio).
|
||||
// Pren com a base BOX_WIDTH_MIN i s'eixampla si algun text no hi cap.
|
||||
[[nodiscard]] auto computeTargetWidth() const -> float;
|
||||
|
||||
// Y (top) de l'item index dins una caixa col·locada a box_y. Si la
|
||||
// pagina te subtitol, els items es desplacen cap avall.
|
||||
[[nodiscard]] static auto computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float;
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
SDLManager* sdl_;
|
||||
DebugOverlay* debug_overlay_;
|
||||
Graphics::VectorText text_;
|
||||
|
||||
std::vector<Page> stack_;
|
||||
bool open_ = false;
|
||||
bool closing_ = false;
|
||||
float open_anim_ = 0.0F; // 0..1 raw (sense easing)
|
||||
float animated_h_ = 0.0F; // Alçada animada amb smoothing exponencial
|
||||
float animated_w_ = 0.0F; // Ample animat (eixampla segons contingut)
|
||||
|
||||
// Estat del highlight (rectangle del cursor). Es lerpa cap a l'item
|
||||
// actiu amb ease-out exponencial; quan el cursor "salta" (open o
|
||||
// push/pop de pagina), s'enganxa directament al nou objectiu.
|
||||
float highlight_y_ = 0.0F;
|
||||
float highlight_h_ = 0.0F;
|
||||
bool highlight_snap_ = true;
|
||||
|
||||
// Edge-detect de stick analogic per a navegacio. Una sola activacio
|
||||
// per direccio: cal tornar a centre (sota el llindar) per disparar
|
||||
// una altra. Compartit entre tots els pads — qualsevol jugador pot
|
||||
// navegar el menu.
|
||||
bool stick_left_held_ = false;
|
||||
bool stick_right_held_ = false;
|
||||
bool stick_up_held_ = false;
|
||||
bool stick_down_held_ = false;
|
||||
// Edge-detect dels triggers L2/R2 com a botons virtuals. SDL3 no
|
||||
// emet button events per als triggers; els llegim com a axis i
|
||||
// sintetitzem una pulsacio quan creuen el llindar.
|
||||
bool trigger_l2_held_ = false;
|
||||
bool trigger_r2_held_ = false;
|
||||
|
||||
static std::unique_ptr<ServiceMenu> instance;
|
||||
};
|
||||
|
||||
} // namespace System
|
||||
@@ -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
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/defaults/audio.hpp"
|
||||
#include "core/defaults/rendering.hpp"
|
||||
#include "core/defaults/window.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
@@ -17,9 +18,11 @@ namespace ConfigYaml {
|
||||
// Permeten escriure window.width en lloc d'engine_config.window.width.
|
||||
Config::WindowConfig& window = engine_config.window;
|
||||
Config::RenderingConfig& rendering = engine_config.rendering;
|
||||
Config::AudioConfig& audio = engine_config.audio;
|
||||
Config::PlayerBindings& player1 = engine_config.player1;
|
||||
Config::PlayerBindings& player2 = engine_config.player2;
|
||||
bool& console = engine_config.console;
|
||||
std::string& locale = engine_config.locale;
|
||||
} // namespace
|
||||
|
||||
// ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ==========
|
||||
@@ -208,6 +211,17 @@ namespace ConfigYaml {
|
||||
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
|
||||
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
|
||||
|
||||
// Audio
|
||||
audio.enabled = Defaults::Audio::ENABLED;
|
||||
audio.volume = Defaults::Audio::VOLUME;
|
||||
audio.music_enabled = Defaults::Audio::MUSIC_ENABLED;
|
||||
audio.music_volume = Defaults::Audio::MUSIC_VOLUME;
|
||||
audio.sound_enabled = Defaults::Audio::SOUND_ENABLED;
|
||||
audio.sound_volume = Defaults::Audio::SOUND_VOLUME;
|
||||
|
||||
// Idioma
|
||||
locale = "ca";
|
||||
|
||||
// Version
|
||||
version = std::string(Project::VERSION);
|
||||
}
|
||||
@@ -303,6 +317,22 @@ namespace ConfigYaml {
|
||||
}
|
||||
}
|
||||
|
||||
static void loadAudioConfigFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("audio")) {
|
||||
return;
|
||||
}
|
||||
const auto& aud = yaml["audio"];
|
||||
|
||||
auto in_unit_range = [](float v) { return v >= 0.0F && v <= 1.0F; };
|
||||
|
||||
readField(aud, "enabled", audio.enabled, Defaults::Audio::ENABLED);
|
||||
readField(aud, "volume", audio.volume, Defaults::Audio::VOLUME, in_unit_range);
|
||||
readField(aud, "music_enabled", audio.music_enabled, Defaults::Audio::MUSIC_ENABLED);
|
||||
readField(aud, "music_volume", audio.music_volume, Defaults::Audio::MUSIC_VOLUME, in_unit_range);
|
||||
readField(aud, "sound_enabled", audio.sound_enabled, Defaults::Audio::SOUND_ENABLED);
|
||||
readField(aud, "sound_volume", audio.sound_volume, Defaults::Audio::SOUND_VOLUME, in_unit_range);
|
||||
}
|
||||
|
||||
// Carregar controls del player 1 desde YAML
|
||||
static void loadPlayer1ControlsFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("player1")) {
|
||||
@@ -343,12 +373,21 @@ namespace ConfigYaml {
|
||||
if (gp.contains("button_shoot")) {
|
||||
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_start")) {
|
||||
player1.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_menu")) {
|
||||
player1.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar nom del gamepad
|
||||
// Carregar nom i path del gamepad assignat
|
||||
if (p1.contains("gamepad_name")) {
|
||||
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
|
||||
}
|
||||
if (p1.contains("gamepad_path")) {
|
||||
player1.gamepad_path = p1["gamepad_path"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar controls del player 2 desde YAML
|
||||
@@ -391,12 +430,21 @@ namespace ConfigYaml {
|
||||
if (gp.contains("button_shoot")) {
|
||||
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_start")) {
|
||||
player2.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_menu")) {
|
||||
player2.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar nom del gamepad
|
||||
// Carregar nom i path del gamepad assignat
|
||||
if (p2.contains("gamepad_name")) {
|
||||
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
|
||||
}
|
||||
if (p2.contains("gamepad_path")) {
|
||||
player2.gamepad_path = p2["gamepad_path"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar configuración des del file YAML
|
||||
@@ -443,9 +491,20 @@ namespace ConfigYaml {
|
||||
// Carregar seccions
|
||||
loadWindowConfigFromYaml(yaml);
|
||||
loadRenderingConfigFromYaml(yaml);
|
||||
loadAudioConfigFromYaml(yaml);
|
||||
loadPlayer1ControlsFromYaml(yaml);
|
||||
loadPlayer2ControlsFromYaml(yaml);
|
||||
|
||||
// Idioma (opcional; valors admesos: "ca" | "en")
|
||||
if (yaml.contains("locale")) {
|
||||
try {
|
||||
auto val = yaml["locale"].get_value<std::string>();
|
||||
locale = (val == "ca" || val == "en") ? val : "ca";
|
||||
} catch (...) {
|
||||
locale = "ca";
|
||||
}
|
||||
}
|
||||
|
||||
if (console) {
|
||||
std::cout << "Config carregada correctament desde: " << config_file_path
|
||||
<< '\n';
|
||||
@@ -465,6 +524,17 @@ namespace ConfigYaml {
|
||||
}
|
||||
}
|
||||
|
||||
static void saveAudioConfigToYaml(std::ofstream& file) {
|
||||
file << "# AUDIO\n";
|
||||
file << "audio:\n";
|
||||
file << " enabled: " << (audio.enabled ? "true" : "false") << " # ON/OFF general\n";
|
||||
file << " volume: " << audio.volume << " # Master 0.0-1.0\n";
|
||||
file << " music_enabled: " << (audio.music_enabled ? "true" : "false") << "\n";
|
||||
file << " music_volume: " << audio.music_volume << " # 0.0-1.0\n";
|
||||
file << " sound_enabled: " << (audio.sound_enabled ? "true" : "false") << "\n";
|
||||
file << " sound_volume: " << audio.sound_volume << " # 0.0-1.0\n\n";
|
||||
}
|
||||
|
||||
// Guardar controls del player 1 a YAML
|
||||
static void savePlayer1ControlsToYaml(std::ofstream& file) {
|
||||
file << "# CONTROLS JUGADOR 1\n";
|
||||
@@ -479,7 +549,10 @@ namespace ConfigYaml {
|
||||
file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n";
|
||||
file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n";
|
||||
file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n";
|
||||
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n";
|
||||
file << " button_start: " << buttonToString(player1.gamepad.button_start) << "\n";
|
||||
file << " button_menu: " << buttonToString(player1.gamepad.button_menu) << "\n";
|
||||
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n";
|
||||
file << " gamepad_path: \"" << player1.gamepad_path << "\" # Prioritari sobre name\n\n";
|
||||
}
|
||||
|
||||
// Guardar controls del player 2 a YAML
|
||||
@@ -496,7 +569,10 @@ namespace ConfigYaml {
|
||||
file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n";
|
||||
file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n";
|
||||
file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n";
|
||||
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n";
|
||||
file << " button_start: " << buttonToString(player2.gamepad.button_start) << "\n";
|
||||
file << " button_menu: " << buttonToString(player2.gamepad.button_menu) << "\n";
|
||||
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n";
|
||||
file << " gamepad_path: \"" << player2.gamepad_path << "\" # Prioritari sobre name\n\n";
|
||||
}
|
||||
|
||||
// Guardar configuración al file YAML
|
||||
@@ -532,6 +608,11 @@ namespace ConfigYaml {
|
||||
file << " render_height: " << rendering.render_height
|
||||
<< " # Parell amb render_width (720, 900, 1080, 1440, 2160)\n\n";
|
||||
|
||||
file << "# IDIOMA\n";
|
||||
file << "locale: " << locale << " # ca | en\n\n";
|
||||
|
||||
saveAudioConfigToYaml(file);
|
||||
|
||||
// Guardar controls de jugadors
|
||||
savePlayer1ControlsToYaml(file);
|
||||
savePlayer2ControlsToYaml(file);
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace ConfigYaml {
|
||||
.key_start = SDL_SCANCODE_2,
|
||||
},
|
||||
.gamepad_name = "",
|
||||
.gamepad_path = "",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/system/scene_context.hpp"
|
||||
#include "core/system/service_menu.hpp"
|
||||
#include "game/stage_system/stage_loader.hpp"
|
||||
#include "game/systems/collision_system.hpp"
|
||||
#include "game/systems/continue_system.hpp"
|
||||
@@ -181,6 +183,13 @@ void GameScene::handleEvent(const SDL_Event& event) {
|
||||
}
|
||||
|
||||
void GameScene::update(float delta_time) {
|
||||
// Pausa global: mentre el menu de servei esta obert, congelem la lògica
|
||||
// de joc. El draw() segueix executant-se per a mantenir l'escena visible
|
||||
// sota el menu.
|
||||
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && menu->isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Orquestador delgado: cada paso vive en su propia función para
|
||||
// mantener update() legible y reducir complejidad cognitiva.
|
||||
stepPhysics(delta_time);
|
||||
@@ -608,7 +617,7 @@ void GameScene::drawGameOverState() {
|
||||
firework_manager_.draw();
|
||||
floating_score_manager_.draw();
|
||||
|
||||
const std::string GAME_OVER_TEXT = "GAME OVER";
|
||||
const std::string GAME_OVER_TEXT = Locale::get().text("game_screen.game_over");
|
||||
constexpr float SCALE = Defaults::Game::GameOverScreen::TEXT_SCALE;
|
||||
constexpr float SPACING = Defaults::Game::GameOverScreen::TEXT_SPACING;
|
||||
|
||||
@@ -710,7 +719,7 @@ void GameScene::drawLevelCompletedState() {
|
||||
debris_manager_.draw();
|
||||
firework_manager_.draw();
|
||||
floating_score_manager_.draw();
|
||||
drawStageMessage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED);
|
||||
drawStageMessage(Locale::get().text("stage.completed"));
|
||||
drawScoreboard();
|
||||
}
|
||||
|
||||
@@ -814,7 +823,7 @@ auto GameScene::buildScoreboard() const -> std::string {
|
||||
|
||||
// Format: "123456 03 LEVEL 01 654321 02"
|
||||
// Nota: dos espais entre seccions, mantenir ambdós slots siempre visibles
|
||||
return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2;
|
||||
return score_p1 + " " + vides_p1 + " " + Locale::get().text("hud.level") + stage_str + " " + score_p2 + " " + vides_p2;
|
||||
}
|
||||
|
||||
// [NEW] Stage system helper methods
|
||||
@@ -946,7 +955,7 @@ void GameScene::drawContinue() {
|
||||
constexpr float SPACING = 4.0F;
|
||||
|
||||
// "CONTINUE" text (using constants)
|
||||
const std::string CONTINUE_TEXT = "CONTINUE";
|
||||
const std::string CONTINUE_TEXT = Locale::get().text("game_screen.continue");
|
||||
float escala_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_SCALE;
|
||||
float y_ratio_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_Y_RATIO;
|
||||
|
||||
@@ -966,7 +975,10 @@ void GameScene::drawContinue() {
|
||||
|
||||
// "CONTINUES LEFT" (conditional + using constants)
|
||||
if (!Defaults::Game::INFINITE_CONTINUES) {
|
||||
const std::string CONTINUES_TEXT = "CONTINUES LEFT: " + std::to_string(Defaults::Game::MAX_CONTINUES - continues_used_);
|
||||
const std::string CONTINUES_TEXT = localeSubstitute(
|
||||
Locale::get().text("game_screen.continues_left"),
|
||||
"{n}",
|
||||
std::to_string(Defaults::Game::MAX_CONTINUES - continues_used_));
|
||||
float escala_info = Defaults::Game::ContinueScreen::INFO_TEXT_SCALE;
|
||||
float y_ratio_info = Defaults::Game::ContinueScreen::INFO_TEXT_Y_RATIO;
|
||||
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/graphics/shape_loader.hpp"
|
||||
#include "core/input/define_inputs.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/math/easing.hpp"
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
#include "core/system/scene_context.hpp"
|
||||
#include "core/system/service_menu.hpp"
|
||||
#include "project.h"
|
||||
|
||||
using SceneManager::SceneContext;
|
||||
@@ -323,8 +326,20 @@ void TitleScene::update(float delta_time) {
|
||||
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();
|
||||
handleStartInput();
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::updateStarfieldFadeInState(float delta_time) {
|
||||
@@ -602,7 +617,7 @@ void TitleScene::draw() {
|
||||
mostrar_text = (std::sin(FASE) > 0.0F);
|
||||
}
|
||||
if (mostrar_text) {
|
||||
const std::string MAIN_TEXT = "PRESS START TO PLAY";
|
||||
const std::string MAIN_TEXT = Locale::get().text("title.press_start");
|
||||
const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE;
|
||||
const float CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
||||
const float CENTRE_Y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -78,24 +77,4 @@ namespace StageSystem {
|
||||
}
|
||||
};
|
||||
|
||||
// Constants per messages de transición
|
||||
namespace Constants {
|
||||
// Pool de messages per start de level (selecció aleatòria)
|
||||
inline constexpr std::array<const char*, 12> MISSATGES_LEVEL_START = {
|
||||
"ORNI ALERT!",
|
||||
"INCOMING ORNIS!",
|
||||
"ROLLING THREAT!",
|
||||
"ENEMY WAVE!",
|
||||
"WAVE OF ORNIS DETECTED!",
|
||||
"NEXT SWARM APPROACHING!",
|
||||
"BRACE FOR THE NEXT WAVE!",
|
||||
"ANOTHER ATTACK INCOMING!",
|
||||
"SENSORS DETECT HOSTILE ORNIS...",
|
||||
"UNIDENTIFIED ROLLING OBJECTS INBOUND!",
|
||||
"ENEMY FORCES MOBILIZING!",
|
||||
"PREPARE FOR IMPACT!"};
|
||||
|
||||
constexpr const char* MISSATGE_LEVEL_COMPLETED = "GOOD JOB COMMANDER!";
|
||||
} // namespace Constants
|
||||
|
||||
} // namespace StageSystem
|
||||
|
||||
@@ -10,28 +10,28 @@
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "stage_config.hpp"
|
||||
|
||||
namespace StageSystem {
|
||||
|
||||
StageManager::StageManager(const StageSystemConfig* config)
|
||||
: config_(config)
|
||||
{
|
||||
StageManager::StageManager(const StageSystemConfig* config)
|
||||
: config_(config) {
|
||||
if (config_ == nullptr) {
|
||||
std::cerr << "[StageManager] Error: config es null" << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::init() {
|
||||
void StageManager::init() {
|
||||
stage_actual_ = 1;
|
||||
loadStage(stage_actual_);
|
||||
changeState(EstatStage::INIT_HUD);
|
||||
|
||||
std::cout << "[StageManager] Inicialitzat a stage " << static_cast<int>(stage_actual_)
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::update(float delta_time, bool pause_spawn) {
|
||||
void StageManager::update(float delta_time, bool pause_spawn) {
|
||||
switch (estat_) {
|
||||
case EstatStage::INIT_HUD:
|
||||
processInitHud(delta_time);
|
||||
@@ -49,25 +49,25 @@ void StageManager::update(float delta_time, bool pause_spawn) {
|
||||
processLevelCompleted(delta_time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::markStageCompleted() {
|
||||
void StageManager::markStageCompleted() {
|
||||
std::cout << "[StageManager] Stage " << static_cast<int>(stage_actual_) << " completat!"
|
||||
<< '\n';
|
||||
changeState(EstatStage::LEVEL_COMPLETED);
|
||||
}
|
||||
}
|
||||
|
||||
auto StageManager::isGameComplete() const -> bool {
|
||||
auto StageManager::isGameComplete() const -> bool {
|
||||
return stage_actual_ >= config_->metadata.total_stages &&
|
||||
estat_ == EstatStage::LEVEL_COMPLETED &&
|
||||
timer_transicio_ <= 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
auto StageManager::getCurrentConfig() const -> const StageConfig* {
|
||||
auto StageManager::getCurrentConfig() const -> const StageConfig* {
|
||||
return config_->findStage(stage_actual_);
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::changeState(EstatStage nou_estat) {
|
||||
void StageManager::changeState(EstatStage nou_estat) {
|
||||
estat_ = nou_estat;
|
||||
|
||||
// Set timer based on state type
|
||||
@@ -81,8 +81,9 @@ void StageManager::changeState(EstatStage nou_estat) {
|
||||
|
||||
// Select random message when entering LEVEL_START
|
||||
if (nou_estat == EstatStage::LEVEL_START) {
|
||||
size_t index = static_cast<size_t>(std::rand()) % Constants::MISSATGES_LEVEL_START.size();
|
||||
missatge_level_start_actual_ = Constants::MISSATGES_LEVEL_START[index];
|
||||
const std::size_t POOL = Locale::get().count("stage.start");
|
||||
const std::size_t INDEX = (POOL == 0) ? 0 : static_cast<std::size_t>(std::rand()) % POOL;
|
||||
missatge_level_start_actual_ = Locale::get().text("stage.start." + std::to_string(INDEX));
|
||||
|
||||
// [NOU] Iniciar música al entrar en LEVEL_START (después de INIT_HUD)
|
||||
// Solo si no está sonant ya (per evitar reset en loops posteriors)
|
||||
@@ -107,34 +108,33 @@ void StageManager::changeState(EstatStage nou_estat) {
|
||||
break;
|
||||
}
|
||||
std::cout << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::processInitHud(float delta_time) {
|
||||
void StageManager::processInitHud(float delta_time) {
|
||||
timer_transicio_ -= delta_time;
|
||||
|
||||
if (timer_transicio_ <= 0.0F) {
|
||||
changeState(EstatStage::LEVEL_START);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::processLevelStart(float delta_time) {
|
||||
void StageManager::processLevelStart(float delta_time) {
|
||||
timer_transicio_ -= delta_time;
|
||||
|
||||
if (timer_transicio_ <= 0.0F) {
|
||||
changeState(EstatStage::PLAYING);
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::processPlaying(float delta_time, bool pause_spawn) {
|
||||
}
|
||||
|
||||
void StageManager::processPlaying(float delta_time, bool pause_spawn) {
|
||||
// Update spawn controller (pauses when pause_spawn = true)
|
||||
// Note: The actual enemy array update happens in GameScene::update()
|
||||
// This is just for internal timekeeping
|
||||
(void)delta_time; // Spawn controller is updated externally
|
||||
(void)pause_spawn; // Passed to spawn_controller_.update() by GameScene
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::processLevelCompleted(float delta_time) {
|
||||
void StageManager::processLevelCompleted(float delta_time) {
|
||||
timer_transicio_ -= delta_time;
|
||||
|
||||
if (timer_transicio_ <= 0.0F) {
|
||||
@@ -152,9 +152,9 @@ void StageManager::processLevelCompleted(float delta_time) {
|
||||
loadStage(stage_actual_);
|
||||
changeState(EstatStage::LEVEL_START);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StageManager::loadStage(uint8_t stage_id) {
|
||||
void StageManager::loadStage(uint8_t stage_id) {
|
||||
const StageConfig* stage_config = config_->findStage(stage_id);
|
||||
if (stage_config == nullptr) {
|
||||
std::cerr << "[StageManager] Error: no es pot trobar stage " << static_cast<int>(stage_id)
|
||||
@@ -168,6 +168,6 @@ void StageManager::loadStage(uint8_t stage_id) {
|
||||
|
||||
std::cout << "[StageManager] Carregat stage " << static_cast<int>(stage_id) << ": "
|
||||
<< static_cast<int>(stage_config->total_enemies) << " enemigos" << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace StageSystem
|
||||
|
||||
@@ -13,8 +13,12 @@
|
||||
#include <memory>
|
||||
|
||||
#include "core/system/director.hpp"
|
||||
#include "core/system/relaunch.hpp"
|
||||
|
||||
auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
|
||||
// Desem argv perquè el menu de servei pugui demanar un reinici en calent
|
||||
// (execv) sense haver de conèixer Director.
|
||||
System::Relaunch::setArgv(argc, argv);
|
||||
auto director = std::make_unique<Director>(argc, argv);
|
||||
*appstate = director.release();
|
||||
return SDL_APP_CONTINUE;
|
||||
@@ -33,4 +37,8 @@ auto SDL_AppIterate(void* appstate) -> SDL_AppResult {
|
||||
void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/) {
|
||||
// Reabsorbim la propietat: el destructor del Director allibera tot.
|
||||
std::unique_ptr<Director> director(static_cast<Director*>(appstate));
|
||||
director.reset();
|
||||
// Si el menu va demanar reinici, fem execv ara que tot esta net. En cas
|
||||
// d'exit no torna; si falla, l'aplicacio surt amb codi d'error.
|
||||
System::Relaunch::execIfRequested();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user