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
+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
+146 -146
View File
@@ -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<int>(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<int>(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<size_t>(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<int>(stage_id)
std::cout << "[StageManager] Inicialitzat a stage " << static_cast<int>(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<int>(stage_id) << ": "
<< static_cast<int>(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<int>(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::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)
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<int>(stage_id)
<< '\n';
return;
}
// Configure spawn controller
spawn_controller_.configure(stage_config);
spawn_controller_.start();
std::cout << "[StageManager] Carregat stage " << static_cast<int>(stage_id) << ": "
<< static_cast<int>(stage_config->total_enemies) << " enemigos" << '\n';
}
} // namespace StageSystem