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
+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
+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