feat(locale): sistema i18n YAML amb català i anglès

This commit is contained in:
2026-05-24 10:28:56 +02:00
parent 274ce1ca63
commit 35d720bb77
13 changed files with 446 additions and 180 deletions
+50
View File
@@ -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"
+49
View File
@@ -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"
+1
View File
@@ -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
+103
View File
@@ -0,0 +1,103 @@
// 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_;
}
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<uint8_t> 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;
}
+49
View File
@@ -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 <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.
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<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;
+13 -6
View File
@@ -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"));
}
}
+5
View File
@@ -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");
+2 -1
View File
@@ -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
+17
View File
@@ -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<std::string>();
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);
+9 -5
View File
@@ -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;
+2 -1
View File
@@ -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;
-21
View File
@@ -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
+29 -29
View File
@@ -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