From 35d720bb77472dfe88141e17ef4e1d52319f88c8 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 10:28:56 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(locale):=20sistema=20i18n=20YAML=20amb?= =?UTF-8?q?=20catal=C3=A0=20i=20angl=C3=A8s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/locale/ca.yaml | 50 ++++ data/locale/en.yaml | 49 ++++ source/core/config/engine_config.hpp | 1 + source/core/locale/locale.cpp | 103 ++++++++ source/core/locale/locale.hpp | 49 ++++ source/core/rendering/sdl_manager.cpp | 19 +- source/core/system/director.cpp | 5 + source/core/system/global_events.cpp | 3 +- source/game/config_yaml.cpp | 17 ++ source/game/scenes/game_scene.cpp | 14 +- source/game/scenes/title_scene.cpp | 3 +- source/game/stage_system/stage_config.hpp | 21 -- source/game/stage_system/stage_manager.cpp | 292 ++++++++++----------- 13 files changed, 446 insertions(+), 180 deletions(-) create mode 100644 data/locale/ca.yaml create mode 100644 data/locale/en.yaml create mode 100644 source/core/locale/locale.cpp create mode 100644 source/core/locale/locale.hpp diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml new file mode 100644 index 0000000..4ad3cf0 --- /dev/null +++ b/data/locale/ca.yaml @@ -0,0 +1,50 @@ +# 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" + +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" + controls: "CONTROLS" + back: "ENRERE" + exit: "EIXIR DEL JOC" diff --git a/data/locale/en.yaml b/data/locale/en.yaml new file mode 100644 index 0000000..c777739 --- /dev/null +++ b/data/locale/en.yaml @@ -0,0 +1,49 @@ +# 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" + +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" + controls: "CONTROLS" + back: "BACK" + exit: "EXIT GAME" diff --git a/source/core/config/engine_config.hpp b/source/core/config/engine_config.hpp index 4ba9bec..e871703 100644 --- a/source/core/config/engine_config.hpp +++ b/source/core/config/engine_config.hpp @@ -64,6 +64,7 @@ namespace Config { 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 diff --git a/source/core/locale/locale.cpp b/source/core/locale/locale.cpp new file mode 100644 index 0000000..9c8c3cf --- /dev/null +++ b/source/core/locale/locale.cpp @@ -0,0 +1,103 @@ +// locale.cpp - Implementació del sistema de locale +// © 2026 JailDesigner + +#include "core/locale/locale.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#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& out) { + if (node.is_mapping()) { + for (auto it = node.begin(); it != node.end(); ++it) { + const std::string KEY = prefix.empty() + ? it.key().get_value() + : prefix + "." + it.key().get_value(); + 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(); + } + } + +} // namespace + +auto Locale::get() -> Locale& { + static Locale instance_; + return instance_; +} + +void Locale::load(const std::string& file_path) { + // 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 bytes = Resource::Helper::loadFile(normalized); + if (bytes.empty()) { + std::cerr << "[Locale] no s'ha pogut load " << normalized << '\n'; + return; + } + + 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'; + } catch (const std::exception& e) { + std::cerr << "[Locale] error parsejant " << normalized << ": " << e.what() << '\n'; + } +} + +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; +} diff --git a/source/core/locale/locale.hpp b/source/core/locale/locale.hpp new file mode 100644 index 0000000..0b8ee21 --- /dev/null +++ b/source/core/locale/locale.hpp @@ -0,0 +1,49 @@ +// 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 +#include +#include +#include + +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. + void load(const std::string& file_path); + + // 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 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; diff --git a/source/core/rendering/sdl_manager.cpp b/source/core/rendering/sdl_manager.cpp index a42e896..9fddfae 100644 --- a/source/core/rendering/sdl_manager.cpp +++ b/source/core/rendering/sdl_manager.cpp @@ -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,7 @@ 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")); } } @@ -384,6 +391,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")); } } diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 83c3dcb..b77e7d6 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -15,6 +15,7 @@ #include "core/defaults/window.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" @@ -99,6 +100,10 @@ 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"); diff --git a/source/core/system/global_events.cpp b/source/core/system/global_events.cpp index 7322a8d..af665e9 100644 --- a/source/core/system/global_events.cpp +++ b/source/core/system/global_events.cpp @@ -7,6 +7,7 @@ #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 "scene_context.hpp" @@ -71,7 +72,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 diff --git a/source/game/config_yaml.cpp b/source/game/config_yaml.cpp index d45d880..1527912 100644 --- a/source/game/config_yaml.cpp +++ b/source/game/config_yaml.cpp @@ -20,6 +20,7 @@ namespace ConfigYaml { 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 +209,9 @@ namespace ConfigYaml { rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT; rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT; + // Idioma + locale = "ca"; + // Version version = std::string(Project::VERSION); } @@ -446,6 +450,16 @@ namespace ConfigYaml { loadPlayer1ControlsFromYaml(yaml); loadPlayer2ControlsFromYaml(yaml); + // Idioma (opcional; valors admesos: "ca" | "en") + if (yaml.contains("locale")) { + try { + auto val = yaml["locale"].get_value(); + locale = (val == "ca" || val == "en") ? val : "ca"; + } catch (...) { + locale = "ca"; + } + } + if (console) { std::cout << "Config carregada correctament desde: " << config_file_path << '\n'; @@ -532,6 +546,9 @@ 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"; + // Guardar controls de jugadors savePlayer1ControlsToYaml(file); savePlayer2ControlsToYaml(file); diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 0ce0e0c..fda86d1 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -11,6 +11,7 @@ #include "core/audio/audio.hpp" #include "core/input/input.hpp" +#include "core/locale/locale.hpp" #include "core/system/scene_context.hpp" #include "game/stage_system/stage_loader.hpp" #include "game/systems/collision_system.hpp" @@ -608,7 +609,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 +711,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 +815,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 +947,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 +967,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; diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index b755e96..6e57169 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -14,6 +14,7 @@ #include "core/defaults.hpp" #include "core/graphics/shape_loader.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" @@ -602,7 +603,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; diff --git a/source/game/stage_system/stage_config.hpp b/source/game/stage_system/stage_config.hpp index 9616e43..1ed4ce8 100644 --- a/source/game/stage_system/stage_config.hpp +++ b/source/game/stage_system/stage_config.hpp @@ -3,7 +3,6 @@ #pragma once -#include #include #include #include @@ -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 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 diff --git a/source/game/stage_system/stage_manager.cpp b/source/game/stage_system/stage_manager.cpp index aec2b51..d3b1ae6 100644 --- a/source/game/stage_system/stage_manager.cpp +++ b/source/game/stage_system/stage_manager.cpp @@ -10,164 +10,164 @@ #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) - { - if (config_ == nullptr) { - std::cerr << "[StageManager] Error: config es null" << '\n'; - } -} - -void StageManager::init() { - stage_actual_ = 1; - loadStage(stage_actual_); - changeState(EstatStage::INIT_HUD); - - std::cout << "[StageManager] Inicialitzat a stage " << static_cast(stage_actual_) - << '\n'; -} - -void StageManager::update(float delta_time, bool pause_spawn) { - switch (estat_) { - case EstatStage::INIT_HUD: - processInitHud(delta_time); - break; - - case EstatStage::LEVEL_START: - processLevelStart(delta_time); - break; - - case EstatStage::PLAYING: - processPlaying(delta_time, pause_spawn); - break; - - case EstatStage::LEVEL_COMPLETED: - processLevelCompleted(delta_time); - break; - } -} - -void StageManager::markStageCompleted() { - std::cout << "[StageManager] Stage " << static_cast(stage_actual_) << " completat!" - << '\n'; - changeState(EstatStage::LEVEL_COMPLETED); -} - -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* { - return config_->findStage(stage_actual_); -} - -void StageManager::changeState(EstatStage nou_estat) { - estat_ = nou_estat; - - // Set timer based on state type - if (nou_estat == EstatStage::INIT_HUD) { - timer_transicio_ = Defaults::Game::INIT_HUD_DURATION; - } else if (nou_estat == EstatStage::LEVEL_START) { - timer_transicio_ = Defaults::Game::LEVEL_START_DURATION; - } else if (nou_estat == EstatStage::LEVEL_COMPLETED) { - timer_transicio_ = Defaults::Game::LEVEL_COMPLETED_DURATION; - } - - // Select random message when entering LEVEL_START - if (nou_estat == EstatStage::LEVEL_START) { - size_t index = static_cast(std::rand()) % Constants::MISSATGES_LEVEL_START.size(); - missatge_level_start_actual_ = Constants::MISSATGES_LEVEL_START[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) - if (Audio::getMusicState() != Audio::MusicState::PLAYING) { - Audio::get()->playMusic("game.ogg"); + StageManager::StageManager(const StageSystemConfig* config) + : config_(config) { + if (config_ == nullptr) { + std::cerr << "[StageManager] Error: config es null" << '\n'; } } - std::cout << "[StageManager] Canvi de state: "; - switch (nou_estat) { - case EstatStage::INIT_HUD: - std::cout << "INIT_HUD"; - break; - case EstatStage::LEVEL_START: - std::cout << "LEVEL_START"; - break; - case EstatStage::PLAYING: - std::cout << "PLAYING"; - break; - case EstatStage::LEVEL_COMPLETED: - std::cout << "LEVEL_COMPLETED"; - break; - } - std::cout << '\n'; -} - -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) { - timer_transicio_ -= delta_time; - - if (timer_transicio_ <= 0.0F) { - changeState(EstatStage::PLAYING); - } -} - -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) { - timer_transicio_ -= delta_time; - - if (timer_transicio_ <= 0.0F) { - // Advance to next stage - stage_actual_++; - - // Loop back to stage 1 after final stage - if (stage_actual_ > config_->metadata.total_stages) { - stage_actual_ = 1; - std::cout << "[StageManager] Todas las stages completades! Tornant a stage 1" - << '\n'; - } - - // Load next stage + void StageManager::init() { + stage_actual_ = 1; loadStage(stage_actual_); - changeState(EstatStage::LEVEL_START); - } -} + changeState(EstatStage::INIT_HUD); -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(stage_id) + std::cout << "[StageManager] Inicialitzat a stage " << static_cast(stage_actual_) << '\n'; - return; } - // Configure spawn controller - spawn_controller_.configure(stage_config); - spawn_controller_.start(); + void StageManager::update(float delta_time, bool pause_spawn) { + switch (estat_) { + case EstatStage::INIT_HUD: + processInitHud(delta_time); + break; - std::cout << "[StageManager] Carregat stage " << static_cast(stage_id) << ": " - << static_cast(stage_config->total_enemies) << " enemigos" << '\n'; -} + case EstatStage::LEVEL_START: + processLevelStart(delta_time); + break; + + case EstatStage::PLAYING: + processPlaying(delta_time, pause_spawn); + break; + + case EstatStage::LEVEL_COMPLETED: + processLevelCompleted(delta_time); + break; + } + } + + void StageManager::markStageCompleted() { + std::cout << "[StageManager] Stage " << static_cast(stage_actual_) << " completat!" + << '\n'; + changeState(EstatStage::LEVEL_COMPLETED); + } + + 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* { + return config_->findStage(stage_actual_); + } + + void StageManager::changeState(EstatStage nou_estat) { + estat_ = nou_estat; + + // Set timer based on state type + if (nou_estat == EstatStage::INIT_HUD) { + timer_transicio_ = Defaults::Game::INIT_HUD_DURATION; + } else if (nou_estat == EstatStage::LEVEL_START) { + timer_transicio_ = Defaults::Game::LEVEL_START_DURATION; + } else if (nou_estat == EstatStage::LEVEL_COMPLETED) { + timer_transicio_ = Defaults::Game::LEVEL_COMPLETED_DURATION; + } + + // Select random message when entering LEVEL_START + if (nou_estat == EstatStage::LEVEL_START) { + const std::size_t POOL = Locale::get().count("stage.start"); + const std::size_t INDEX = (POOL == 0) ? 0 : static_cast(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) + if (Audio::getMusicState() != Audio::MusicState::PLAYING) { + Audio::get()->playMusic("game.ogg"); + } + } + + std::cout << "[StageManager] Canvi de state: "; + switch (nou_estat) { + case EstatStage::INIT_HUD: + std::cout << "INIT_HUD"; + break; + case EstatStage::LEVEL_START: + std::cout << "LEVEL_START"; + break; + case EstatStage::PLAYING: + std::cout << "PLAYING"; + break; + case EstatStage::LEVEL_COMPLETED: + std::cout << "LEVEL_COMPLETED"; + break; + } + std::cout << '\n'; + } + + 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) { + timer_transicio_ -= delta_time; + + if (timer_transicio_ <= 0.0F) { + changeState(EstatStage::PLAYING); + } + } + + 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) { + timer_transicio_ -= delta_time; + + if (timer_transicio_ <= 0.0F) { + // Advance to next stage + stage_actual_++; + + // Loop back to stage 1 after final stage + if (stage_actual_ > config_->metadata.total_stages) { + stage_actual_ = 1; + std::cout << "[StageManager] Todas las stages completades! Tornant a stage 1" + << '\n'; + } + + // Load next stage + loadStage(stage_actual_); + changeState(EstatStage::LEVEL_START); + } + } + + 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(stage_id) + << '\n'; + return; + } + + // Configure spawn controller + spawn_controller_.configure(stage_config); + spawn_controller_.start(); + + std::cout << "[StageManager] Carregat stage " << static_cast(stage_id) << ": " + << static_cast(stage_config->total_enemies) << " enemigos" << '\n'; + } } // namespace StageSystem From c4c6881bd6749f7d56ecc7d04ff0cc29a8f29483 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 10:35:39 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat(locale):=20canvi=20d'idioma=20al=20vol?= =?UTF-8?q?=20amb=20F7=20i=20persist=C3=A8ncia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/locale/ca.yaml | 5 +++++ data/locale/en.yaml | 5 +++++ source/core/locale/locale.cpp | 10 ++++++++-- source/core/locale/locale.hpp | 11 +++++++++-- source/core/system/global_events.cpp | 21 +++++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 4ad3cf0..eefa10a 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -13,6 +13,11 @@ notification: antialias_off: "AA INACTIU" postfx_on: "POSTPROCESSAT ACTIU" postfx_off: "POSTPROCESSAT INACTIU" + locale_switched: "IDIOMA: {lang}" + +language: + ca: "CATALA" + en: "ANGLES" hud: level: "NIVELL " diff --git a/data/locale/en.yaml b/data/locale/en.yaml index c777739..1d7fdbe 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -12,6 +12,11 @@ notification: antialias_off: "AA OFF" postfx_on: "POSTPROCESS ON" postfx_off: "POSTPROCESS OFF" + locale_switched: "LANGUAGE: {lang}" + +language: + ca: "CATALAN" + en: "ENGLISH" hud: level: "LEVEL " diff --git a/source/core/locale/locale.cpp b/source/core/locale/locale.cpp index 9c8c3cf..ec1733a 100644 --- a/source/core/locale/locale.cpp +++ b/source/core/locale/locale.cpp @@ -51,7 +51,7 @@ auto Locale::get() -> Locale& { return instance_; } -void Locale::load(const std::string& file_path) { +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; @@ -62,7 +62,7 @@ void Locale::load(const std::string& file_path) { std::vector bytes = Resource::Helper::loadFile(normalized); if (bytes.empty()) { std::cerr << "[Locale] no s'ha pogut load " << normalized << '\n'; - return; + return false; } try { @@ -72,11 +72,17 @@ void Locale::load(const std::string& file_path) { 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()) { diff --git a/source/core/locale/locale.hpp b/source/core/locale/locale.hpp index 0b8ee21..20c6d6e 100644 --- a/source/core/locale/locale.hpp +++ b/source/core/locale/locale.hpp @@ -25,8 +25,15 @@ class Locale { // 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. - void load(const std::string& file_path); + // 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/.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 diff --git a/source/core/system/global_events.cpp b/source/core/system/global_events.cpp index af665e9..3839b6a 100644 --- a/source/core/system/global_events.cpp +++ b/source/core/system/global_events.cpp @@ -10,6 +10,7 @@ #include "core/locale/locale.hpp" #include "core/rendering/sdl_manager.hpp" #include "core/system/notifier.hpp" +#include "game/config_yaml.hpp" #include "scene_context.hpp" // Using declarations per simplificar el codi @@ -63,6 +64,26 @@ 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_ESCAPE: { // Doble pulsació per confirmar sortida: la primera ESC // dispara un toast d'avís; només si aquest toast concret