From ff1156747113044fec9f4434e1e8d54c15827dd1 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 26 May 2026 13:32:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(stages):=20sistema=20d'onades=20declarativ?= =?UTF-8?q?es=20amb=20condicions=20de=20transici=C3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/stages/stages.yaml | 301 +++++++++--------- source/game/scenes/game_scene.cpp | 8 +- source/game/stage_system/spawn_controller.cpp | 174 ---------- source/game/stage_system/spawn_controller.hpp | 59 ---- source/game/stage_system/stage_config.hpp | 82 +++-- source/game/stage_system/stage_loader.cpp | 217 +++++++------ source/game/stage_system/stage_loader.hpp | 22 +- source/game/stage_system/stage_manager.cpp | 9 +- source/game/stage_system/stage_manager.hpp | 38 +-- source/game/stage_system/wave_runner.cpp | 161 ++++++++++ source/game/stage_system/wave_runner.hpp | 55 ++++ 11 files changed, 570 insertions(+), 556 deletions(-) delete mode 100644 source/game/stage_system/spawn_controller.cpp delete mode 100644 source/game/stage_system/spawn_controller.hpp create mode 100644 source/game/stage_system/wave_runner.cpp create mode 100644 source/game/stage_system/wave_runner.hpp diff --git a/data/stages/stages.yaml b/data/stages/stages.yaml index 9a9b885..e6ebcfd 100644 --- a/data/stages/stages.yaml +++ b/data/stages/stages.yaml @@ -1,169 +1,182 @@ -# stages.yaml - Configuració de les 10 etapes d'Orni Attack +# stages.yaml - Configuració de les fases d'Orni Attack # © 2026 JailDesigner +# +# Format basat en onades (waves). Cada wave: +# - spawn: list d'enemics a generar, en ordre. +# - spawn_interval: segons entre spawns interns (default 0 = simultanis). +# - next: condició per avançar a la wave següent. +# - "all_dead" / "end" → quan tots els enemics de l'arena han mort. +# - { timeout: T } → quan han passat T segons des de l'inici de la wave. +# - { all_dead: true, timeout: T } → el que arribe abans (amuntegament si vas lent). +# +# Tipus d'enemic: pentagon, square (alias: cuadrado), pinwheel (alias: molinillo), star, big_pentagon. metadata: - version: "1.0" + version: "2.0" total_stages: 10 - description: "Progressive difficulty curve from novice to expert" + description: "Wave-based progression" stages: - # STAGE 1: Tutorial - 4 tipus (sense star: les bales fan injugable el test). + # STAGE 1 — Tutorial: contacte amb pentagons i un cuadrado. - stage_id: 1 - total_enemies: 50 - spawn_config: - mode: "progressive" - initial_delay: 0.3 - spawn_interval: 0.4 - enemy_distribution: - pentagon: 30 - cuadrado: 25 - molinillo: 25 - big_pentagon: 20 - difficulty_multipliers: - speed_multiplier: 0.7 - rotation_multiplier: 0.8 - tracking_strength: 0.0 + multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 } + waves: + - spawn: [pentagon, pentagon] + spawn_interval: 0.6 + next: all_dead + - spawn: [pentagon, pentagon, square] + spawn_interval: 0.5 + next: all_dead + - spawn: [pentagon, pentagon, square, square] + spawn_interval: 0.4 + next: end - # STAGE 2: Introduction to tracking enemies + # STAGE 2 — Apareixen molinillos. - stage_id: 2 - total_enemies: 7 - spawn_config: - mode: "progressive" - initial_delay: 1.5 - spawn_interval: 2.5 - enemy_distribution: - pentagon: 70 - cuadrado: 30 - molinillo: 0 - difficulty_multipliers: - speed_multiplier: 0.85 - rotation_multiplier: 0.9 - tracking_strength: 0.3 + multipliers: { velocity: 0.95, rotation: 1.0, tracking: 0.4 } + waves: + - spawn: [pentagon, pentagon, pentagon] + spawn_interval: 0.5 + next: all_dead + - spawn: [pinwheel] + next: all_dead + - spawn: [pentagon, square, pinwheel] + spawn_interval: 0.6 + next: all_dead + - spawn: [pinwheel, pinwheel, pentagon] + spawn_interval: 0.5 + next: end - # STAGE 3: All enemy types, normal speed + # STAGE 3 — Primer big_pentagon (HP=10). - stage_id: 3 - total_enemies: 10 - spawn_config: - mode: "progressive" - initial_delay: 1.0 - spawn_interval: 2.0 - enemy_distribution: - pentagon: 50 - cuadrado: 30 - molinillo: 20 - difficulty_multipliers: - speed_multiplier: 1.0 - rotation_multiplier: 1.0 - tracking_strength: 0.5 + multipliers: { velocity: 1.0, rotation: 1.0, tracking: 0.5 } + waves: + - spawn: [pentagon, pentagon, square] + spawn_interval: 0.4 + next: all_dead + - spawn: [big_pentagon] + next: { all_dead: true, timeout: 12.0 } + - spawn: [pinwheel, pinwheel] + spawn_interval: 0.5 + next: all_dead + - spawn: [pentagon, square, pinwheel, pinwheel] + spawn_interval: 0.4 + next: end - # STAGE 4: Increased count, faster enemies + # STAGE 4 — Pressió creixent: timeouts curts que poden encavalcar onades. - stage_id: 4 - total_enemies: 12 - spawn_config: - mode: "progressive" - initial_delay: 0.8 - spawn_interval: 1.8 - enemy_distribution: - pentagon: 40 - cuadrado: 35 - molinillo: 25 - difficulty_multipliers: - speed_multiplier: 1.1 - rotation_multiplier: 1.15 - tracking_strength: 0.6 + multipliers: { velocity: 1.05, rotation: 1.1, tracking: 0.6 } + waves: + - spawn: [pentagon, pentagon, pentagon] + spawn_interval: 0.3 + next: { all_dead: true, timeout: 5.0 } + - spawn: [square, square] + spawn_interval: 0.4 + next: { all_dead: true, timeout: 6.0 } + - spawn: [pinwheel, pinwheel, pinwheel] + spawn_interval: 0.4 + next: all_dead + - spawn: [big_pentagon, pentagon, pentagon] + spawn_interval: 0.5 + next: end - # STAGE 5: Maximum count reached + # STAGE 5 — Apareix la star (zigzag clon del pentagon). - stage_id: 5 - total_enemies: 15 - spawn_config: - mode: "progressive" - initial_delay: 0.5 - spawn_interval: 1.5 - enemy_distribution: - pentagon: 35 - cuadrado: 35 - molinillo: 30 - difficulty_multipliers: - speed_multiplier: 1.2 - rotation_multiplier: 1.25 - tracking_strength: 0.7 + multipliers: { velocity: 1.1, rotation: 1.2, tracking: 0.7 } + waves: + - spawn: [star, star] + spawn_interval: 0.4 + next: all_dead + - spawn: [pentagon, square, star] + spawn_interval: 0.4 + next: { all_dead: true, timeout: 6.0 } + - spawn: [pinwheel, pinwheel, star, star] + spawn_interval: 0.4 + next: all_dead + - spawn: [big_pentagon, square, square] + spawn_interval: 0.5 + next: end - # STAGE 6: Molinillo becomes dominant + # STAGE 6 — Densitat alta, mix amb timeouts agressius. - stage_id: 6 - total_enemies: 15 - spawn_config: - mode: "progressive" - initial_delay: 0.3 - spawn_interval: 1.3 - enemy_distribution: - pentagon: 30 - cuadrado: 30 - molinillo: 40 - difficulty_multipliers: - speed_multiplier: 1.3 - rotation_multiplier: 1.4 - tracking_strength: 0.8 + multipliers: { velocity: 1.15, rotation: 1.25, tracking: 0.8 } + waves: + - spawn: [pentagon, pinwheel, pentagon, pinwheel] + spawn_interval: 0.3 + next: { all_dead: true, timeout: 5.0 } + - spawn: [square, square, star] + spawn_interval: 0.4 + next: { all_dead: true, timeout: 5.0 } + - spawn: [pinwheel, pinwheel, pinwheel] + spawn_interval: 0.3 + next: all_dead + - spawn: [big_pentagon, pinwheel, pinwheel] + spawn_interval: 0.4 + next: end - # STAGE 7: High intensity, fast spawns + # STAGE 7 — Tiradors i agressivitat. - stage_id: 7 - total_enemies: 15 - spawn_config: - mode: "progressive" - initial_delay: 0.2 - spawn_interval: 1.0 - enemy_distribution: - pentagon: 25 - cuadrado: 30 - molinillo: 45 - difficulty_multipliers: - speed_multiplier: 1.4 - rotation_multiplier: 1.5 - tracking_strength: 0.9 + multipliers: { velocity: 1.25, rotation: 1.35, tracking: 0.9 } + waves: + - spawn: [square, square, square] + spawn_interval: 0.5 + next: { all_dead: true, timeout: 6.0 } + - spawn: [pinwheel, pinwheel, pentagon, pentagon] + spawn_interval: 0.3 + next: { all_dead: true, timeout: 5.0 } + - spawn: [star, star, star] + spawn_interval: 0.4 + next: all_dead + - spawn: [big_pentagon, pinwheel, pinwheel, square] + spawn_interval: 0.5 + next: end - # STAGE 8: Expert level, 50% molinillos + # STAGE 8 — Pressió constant. - stage_id: 8 - total_enemies: 15 - spawn_config: - mode: "progressive" - initial_delay: 0.1 - spawn_interval: 0.8 - enemy_distribution: - pentagon: 20 - cuadrado: 30 - molinillo: 50 - difficulty_multipliers: - speed_multiplier: 1.5 - rotation_multiplier: 1.6 - tracking_strength: 1.0 + multipliers: { velocity: 1.35, rotation: 1.45, tracking: 1.0 } + waves: + - spawn: [pinwheel, pinwheel, pinwheel] + spawn_interval: 0.3 + next: { all_dead: true, timeout: 4.0 } + - spawn: [square, square, star, star] + spawn_interval: 0.3 + next: { all_dead: true, timeout: 5.0 } + - spawn: [big_pentagon] + next: { all_dead: true, timeout: 8.0 } + - spawn: [pinwheel, pinwheel, square, star, pentagon] + spawn_interval: 0.3 + next: end - # STAGE 9: Near-maximum difficulty + # STAGE 9 — Quasi-final. - stage_id: 9 - total_enemies: 15 - spawn_config: - mode: "progressive" - initial_delay: 0.0 - spawn_interval: 0.6 - enemy_distribution: - pentagon: 15 - cuadrado: 25 - molinillo: 60 - difficulty_multipliers: - speed_multiplier: 1.6 - rotation_multiplier: 1.7 - tracking_strength: 1.1 + multipliers: { velocity: 1.5, rotation: 1.6, tracking: 1.1 } + waves: + - spawn: [pinwheel, pinwheel, star, star] + spawn_interval: 0.3 + next: { all_dead: true, timeout: 4.0 } + - spawn: [big_pentagon, square, square] + spawn_interval: 0.4 + next: { all_dead: true, timeout: 8.0 } + - spawn: [pinwheel, pinwheel, pinwheel, pinwheel] + spawn_interval: 0.3 + next: { all_dead: true, timeout: 5.0 } + - spawn: [big_pentagon, pinwheel, pinwheel, square, star] + spawn_interval: 0.4 + next: end - # STAGE 10: Final challenge, 70% molinillos + # STAGE 10 — Repte final. - stage_id: 10 - total_enemies: 15 - spawn_config: - mode: "progressive" - initial_delay: 0.0 - spawn_interval: 0.5 - enemy_distribution: - pentagon: 10 - cuadrado: 20 - molinillo: 70 - difficulty_multipliers: - speed_multiplier: 1.8 - rotation_multiplier: 2.0 - tracking_strength: 1.2 + multipliers: { velocity: 1.7, rotation: 1.8, tracking: 1.2 } + waves: + - spawn: [pinwheel, pinwheel, pinwheel, pinwheel] + spawn_interval: 0.25 + next: { all_dead: true, timeout: 4.0 } + - spawn: [big_pentagon, square, star] + spawn_interval: 0.4 + next: { all_dead: true, timeout: 6.0 } + - spawn: [pinwheel, pinwheel, star, star, square] + spawn_interval: 0.3 + next: { all_dead: true, timeout: 5.0 } + - spawn: [big_pentagon, big_pentagon, pinwheel, pinwheel, star] + spawn_interval: 0.4 + next: end diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index d3bcfd5..208bdc4 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -142,7 +142,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) stage_manager_->init(); // Set ship position reference for safe spawn (P1 for now, TODO: dual tracking) - stage_manager_->getSpawnController().setShipPosition(&ships_[0].getCenter()); + stage_manager_->getWaveRunner().setShipPosition(&ships_[0].getCenter()); // Inicialitzar timers de muerte per player hit_timer_per_player_[0] = 0.0F; @@ -513,11 +513,11 @@ void GameScene::runStageLevelStart(float delta_time) { void GameScene::runStagePlaying(float delta_time) { const bool PAUSE_SPAWN = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F); - stage_manager_->getSpawnController().update(delta_time, enemies_, PAUSE_SPAWN); + stage_manager_->getWaveRunner().update(delta_time, enemies_, PAUSE_SPAWN); - // Stage completado: cuando al menos un jugador está vivo y todos los enemies muertos. + // Stage completado: cuando al menos un jugador está vivo y todas las onades emeses y arena buida. const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F); - if (ALGU_VIU && stage_manager_->getSpawnController().allEnemiesDestroyed(enemies_)) { + if (ALGU_VIU && stage_manager_->getWaveRunner().stageComplete(enemies_)) { stage_manager_->markStageCompleted(); Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME); return; diff --git a/source/game/stage_system/spawn_controller.cpp b/source/game/stage_system/spawn_controller.cpp deleted file mode 100644 index c0e9c67..0000000 --- a/source/game/stage_system/spawn_controller.cpp +++ /dev/null @@ -1,174 +0,0 @@ -// spawn_controller.cpp - Implementació del controlador de spawn -// © 2026 JailDesigner - -#include "spawn_controller.hpp" - -#include -#include -#include -#include -#include -#include - -#include "core/types.hpp" -#include "game/entities/enemy.hpp" -#include "stage_config.hpp" - -namespace StageSystem { - - SpawnController::SpawnController() = default; - - void SpawnController::configure(const StageConfig* config) { - config_ = config; - } - - void SpawnController::start() { - if (config_ == nullptr) { - std::cerr << "[SpawnController] Error: config_ es null" << '\n'; - return; - } - - reset(); - generateSpawnEvents(); - - std::cout << "[SpawnController] Stage " << static_cast(config_->stage_id) - << ": generats " << spawn_queue_.size() << " spawn events" << '\n'; - } - - void SpawnController::reset() { - spawn_queue_.clear(); - temps_transcorregut_ = 0.0F; - index_spawn_actual_ = 0; - } - - void SpawnController::update(float delta_time, std::array& orni_array, bool pausar) { - if ((config_ == nullptr) || spawn_queue_.empty()) { - return; - } - - // Increment timer only when not paused - if (!pausar) { - temps_transcorregut_ += delta_time; - } - - // Process spawn events - while (index_spawn_actual_ < spawn_queue_.size()) { - SpawnEvent& event = spawn_queue_[index_spawn_actual_]; - - if (event.spawnejat) { - index_spawn_actual_++; - continue; - } - - if (temps_transcorregut_ >= event.temps_spawn) { - // Find first inactive enemy - for (auto& enemy : orni_array) { - if (!enemy.isActive()) { - spawnEnemy(enemy, event.type, ship_position_); - event.spawnejat = true; - index_spawn_actual_++; - break; - } - } - - // If no slot available, try next frame - if (!event.spawnejat) { - break; - } - } else { - // Not yet time for this spawn - break; - } - } - } - - auto SpawnController::allEnemiesSpawned() const -> bool { - return index_spawn_actual_ >= spawn_queue_.size(); - } - - auto SpawnController::allEnemiesDestroyed(const std::array& orni_array) const -> bool { - if (!allEnemiesSpawned()) { - return false; - } - return std::ranges::all_of(orni_array, [](const Enemy& enemy) { return !enemy.isActive(); }); - } - - auto SpawnController::getAliveEnemyCount(const std::array& orni_array) -> uint8_t { - uint8_t count = 0; - for (const auto& enemy : orni_array) { - if (enemy.isActive()) { - count++; - } - } - return count; - } - - auto SpawnController::countSpawnedEnemies() const -> uint8_t { - return static_cast(index_spawn_actual_); - } - - void SpawnController::generateSpawnEvents() { - if (config_ == nullptr) { - return; - } - - for (uint8_t i = 0; i < config_->total_enemies; i++) { - float spawn_time = config_->config_spawn.delay_inicial + - (i * config_->config_spawn.interval_spawn); - - EnemyType type = selectRandomType(); - - spawn_queue_.push_back({spawn_time, type, false}); - } - } - - auto SpawnController::selectRandomType() const -> EnemyType { - if (config_ == nullptr) { - return EnemyType::PENTAGON; - } - - // Weighted random selection based on distribution - int rand_val = std::rand() % 100; - const auto& d = config_->distribucio; - - if (std::cmp_less(rand_val, d.pentagon)) { - return EnemyType::PENTAGON; - } - if (rand_val < d.pentagon + d.cuadrado) { - return EnemyType::SQUARE; - } - if (rand_val < d.pentagon + d.cuadrado + d.molinillo) { - return EnemyType::PINWHEEL; - } - if (rand_val < d.pentagon + d.cuadrado + d.molinillo + d.star) { - return EnemyType::STAR; - } - return EnemyType::BIG_PENTAGON; - } - - void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) { - // Initialize enemy (with safe spawn if ship_pos provided) - enemy.init(type, ship_pos); - - // Apply difficulty multipliers - applyMultipliers(enemy); - } - - void SpawnController::applyMultipliers(Enemy& enemy) const { - if (config_ == nullptr) { - return; - } - - // Apply velocity multiplier - float base_vel = enemy.getBaseVelocity(); - enemy.setVelocity(base_vel * config_->multiplicadors.velocity); - - // Apply rotation multiplier - float base_rot = enemy.getBaseRotation(); - enemy.setRotation(base_rot * config_->multiplicadors.rotation); - - // Apply tracking strength (only affects SQUARE) - enemy.setTrackingStrength(config_->multiplicadors.tracking_strength); - } - -} // namespace StageSystem diff --git a/source/game/stage_system/spawn_controller.hpp b/source/game/stage_system/spawn_controller.hpp deleted file mode 100644 index a0657eb..0000000 --- a/source/game/stage_system/spawn_controller.hpp +++ /dev/null @@ -1,59 +0,0 @@ -// spawn_controller.hpp - Controlador de spawn de enemigos -// © 2026 JailDesigner - -#pragma once - -#include -#include -#include - -#include "core/types.hpp" -#include "game/entities/enemy.hpp" -#include "stage_config.hpp" - -namespace StageSystem { - -// Informació de spawn planificat -struct SpawnEvent { - float temps_spawn; // Temps absolut (segons) per spawnejar - EnemyType type; // Tipo de enemy - bool spawnejat; // Ya s'ha processat? -}; - -class SpawnController { - public: - SpawnController(); - - // Configuration - void configure(const StageConfig* config); // Set stage config - void start(); // Generate spawn schedule - void reset(); // Clear all pending spawns - - // Update - void update(float delta_time, std::array& orni_array, bool pausar = false); - - // Status queries - [[nodiscard]] auto allEnemiesSpawned() const -> bool; - [[nodiscard]] auto allEnemiesDestroyed(const std::array& orni_array) const -> bool; - // Estático: solo recorre el array pasado; no consulta estado del controller. - [[nodiscard]] static auto getAliveEnemyCount(const std::array& orni_array) -> uint8_t; - [[nodiscard]] auto countSpawnedEnemies() const -> uint8_t; - - // [NEW] Set ship position reference for safe spawn - void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } - - private: - const StageConfig* config_{nullptr}; // Non-owning pointer to current stage config - std::vector spawn_queue_; - float temps_transcorregut_{0.0F}; // Elapsed time since stage start - uint8_t index_spawn_actual_{0}; // Next spawn to process - - // Spawn generation - void generateSpawnEvents(); - [[nodiscard]] auto selectRandomType() const -> EnemyType; - void spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos = nullptr); - void applyMultipliers(Enemy& enemy) const; - const Vec2* ship_position_{nullptr}; // [NEW] Non-owning pointer to ship position -}; - -} // namespace StageSystem diff --git a/source/game/stage_system/stage_config.hpp b/source/game/stage_system/stage_config.hpp index 0a05d2e..d3979b9 100644 --- a/source/game/stage_system/stage_config.hpp +++ b/source/game/stage_system/stage_config.hpp @@ -1,76 +1,72 @@ -// stage_config.hpp - Estructures de dades per configuración de stages +// stage_config.hpp - Estructures de dades per configuració de stages // © 2026 JailDesigner #pragma once +#include #include #include #include +#include "game/entities/enemy.hpp" + namespace StageSystem { - // Tipo de mode de spawn - enum class ModeSpawn : std::uint8_t { - PROGRESSIVE, // Spawn progressiu con intervals - IMMEDIATE, // Todos los enemigos de cop - WAVE // Onades de 3-5 enemigos (futura extensió) - }; - - // Configuración de spawn - struct ConfigSpawn { - ModeSpawn mode; - float delay_inicial; // Segons antes del primer spawn - float interval_spawn; // Segons entre spawns consecutius - }; - - // Distribució de type de enemigos (percentatges) - struct DistribucioEnemics { - uint8_t pentagon; // 0-100 - uint8_t cuadrado; // 0-100 - uint8_t molinillo; // 0-100 - uint8_t star{0}; // 0-100 (opcional al YAML; default 0 per compat amb stages antics) - uint8_t big_pentagon{0}; // 0-100 (opcional; enemic gegant HP=10) - // Suma ha de ser 100, validat en StageLoader - }; - - // Multiplicadors de dificultat + // Multiplicadors de dificultat aplicats a tots els enemics del stage. struct MultiplicadorsDificultat { - float velocity; // 0.5-2.0 típic - float rotation; // 0.5-2.0 típic - float tracking_strength; // 0.0-1.5 (aplicat a Square) + float velocity{1.0F}; + float rotation{1.0F}; + float tracking_strength{0.0F}; + }; + + // Condició de transició a la següent onada. + // Pot ser una OR de "tots morts" i "timeout"; en falta tots dos cas, la + // wave no avança mai (invàlid: validat al loader). + struct WaveNext { + bool on_all_dead{false}; + bool has_timeout{false}; + float timeout{0.0F}; + + [[nodiscard]] auto isValid() const -> bool { + return on_all_dead || has_timeout; + } + }; + + // Una onada: llista d'enemics a spawnejar i regla per passar a la següent. + struct WaveConfig { + std::vector spawn; // Ordre i tipus dels spawns + float spawn_interval{0.0F}; // Segons entre spawns interns (0 = simultanis) + WaveNext next; }; // Metadades del file YAML struct MetadataStages { std::string version; - uint8_t total_stages; + uint8_t total_stages{0}; std::string descripcio; }; - // Configuración completa de un stage + // Configuració completa d'un stage struct StageConfig { - uint8_t stage_id; // 1-10 - uint8_t total_enemies; // 1-200 (el cap simultani en pantalla el marca MAX_ORNIS) - ConfigSpawn config_spawn; - DistribucioEnemics distribucio; + uint8_t stage_id{0}; MultiplicadorsDificultat multiplicadors; + std::vector waves; - // Validació [[nodiscard]] auto isValid() const -> bool { - // stage_id es uint8_t: el rango superior (<=255) está garantizado por - // el tipo; basta con confirmar que no es 0 (sentinela "sin asignar"). - return stage_id >= 1 && - total_enemies > 0 && total_enemies <= 200 && - distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star + distribucio.big_pentagon == 100; + if (stage_id == 0 || waves.empty()) { + return false; + } + return std::ranges::all_of(waves, [](const WaveConfig& w) { + return w.next.isValid() && !w.spawn.empty(); + }); } }; - // Configuración completa del sistema (carregada desde YAML) + // Configuració completa del sistema (carregada des de YAML) struct StageSystemConfig { MetadataStages metadata; std::vector stages; // Índex [0] = stage 1 - // Obtenir configuración de un stage específic [[nodiscard]] auto findStage(uint8_t stage_id) const -> const StageConfig* { if (stage_id < 1 || stage_id > stages.size()) { return nullptr; diff --git a/source/game/stage_system/stage_loader.cpp b/source/game/stage_system/stage_loader.cpp index 903cd21..896e567 100644 --- a/source/game/stage_system/stage_loader.cpp +++ b/source/game/stage_system/stage_loader.cpp @@ -1,11 +1,10 @@ -// stage_loader.cpp - Implementació del carregador de configuración YAML +// stage_loader.cpp - Implementació del carregador de configuració YAML // © 2026 JailDesigner #include "stage_loader.hpp" #include #include -#include #include #include #include @@ -15,6 +14,7 @@ #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" +#include "game/entities/enemy.hpp" #include "stage_config.hpp" namespace StageSystem { @@ -27,22 +27,18 @@ namespace StageSystem { normalized = normalized.substr(5); } - // Load from resource system std::vector data = Resource::Helper::loadFile(normalized); if (data.empty()) { std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n'; return nullptr; } - // Convert to string std::string yaml_content(data.begin(), data.end()); std::stringstream stream(yaml_content); - // Parse YAML fkyaml::node yaml = fkyaml::node::deserialize(stream); auto config = std::make_unique(); - // Parse metadata if (!yaml.contains("metadata")) { std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n'; return nullptr; @@ -51,12 +47,10 @@ namespace StageSystem { return nullptr; } - // Parse stages if (!yaml.contains("stages")) { std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n'; return nullptr; } - if (!yaml["stages"].is_sequence()) { std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n'; return nullptr; @@ -67,10 +61,9 @@ namespace StageSystem { if (!parseStage(stage_yaml, stage)) { return nullptr; } - config->stages.push_back(stage); + config->stages.push_back(std::move(stage)); } - // Validar configuración if (!validateConfig(*config)) { return nullptr; } @@ -107,29 +100,35 @@ namespace StageSystem { auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool { try { - if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") || - !yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") || - !yaml.contains("difficulty_multipliers")) { - std::cerr << "[StageLoader] Error: stage incompleta" << '\n'; + if (!yaml.contains("stage_id") || !yaml.contains("waves")) { + std::cerr << "[StageLoader] Error: stage incompleta (cal stage_id i waves)" << '\n'; return false; } stage.stage_id = yaml["stage_id"].get_value(); - stage.total_enemies = yaml["total_enemies"].get_value(); - if (!parseSpawnConfig(yaml["spawn_config"], stage.config_spawn)) { + // multipliers és opcional: si falta, queda als defaults (1.0/1.0/0.0). + if (yaml.contains("multipliers")) { + if (!parseMultipliers(yaml["multipliers"], stage.multiplicadors)) { + return false; + } + } + + if (!yaml["waves"].is_sequence()) { + std::cerr << "[StageLoader] Error: 'waves' ha de ser una list" << '\n'; return false; } - if (!parseDistribution(yaml["enemy_distribution"], stage.distribucio)) { - return false; - } - if (!parseMultipliers(yaml["difficulty_multipliers"], stage.multiplicadors)) { - return false; + for (const auto& wave_yaml : yaml["waves"]) { + WaveConfig wave; + if (!parseWave(wave_yaml, wave)) { + return false; + } + stage.waves.push_back(std::move(wave)); } if (!stage.isValid()) { std::cerr << "[StageLoader] Error: stage " << static_cast(stage.stage_id) - << " no es vàlid" << '\n'; + << " no és vàlid" << '\n'; return false; } @@ -140,76 +139,26 @@ namespace StageSystem { } } - auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool { - try { - if (!yaml.contains("mode") || !yaml.contains("initial_delay") || - !yaml.contains("spawn_interval")) { - std::cerr << "[StageLoader] Error: spawn_config incompleta" << '\n'; - return false; - } - - auto mode_str = yaml["mode"].get_value(); - config.mode = parseSpawnMode(mode_str); - config.delay_inicial = yaml["initial_delay"].get_value(); - config.interval_spawn = yaml["spawn_interval"].get_value(); - - return true; - } catch (const std::exception& e) { - std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n'; - return false; - } - } - - auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool { - try { - if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") || - !yaml.contains("molinillo")) { - std::cerr << "[StageLoader] Error: enemy_distribution incompleta" << '\n'; - return false; - } - - dist.pentagon = yaml["pentagon"].get_value(); - dist.cuadrado = yaml["cuadrado"].get_value(); - dist.molinillo = yaml["molinillo"].get_value(); - // 'star' i 'big_pentagon' són opcionals per compatibilitat amb stages antics (default 0). - dist.star = yaml.contains("star") ? yaml["star"].get_value() : 0; - dist.big_pentagon = yaml.contains("big_pentagon") ? yaml["big_pentagon"].get_value() : 0; - - // Validar que suma 100 - int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star + dist.big_pentagon; - if (sum != 100) { - std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n'; - return false; - } - - return true; - } catch (const std::exception& e) { - std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n'; - return false; - } - } - auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool { try { - if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") || - !yaml.contains("tracking_strength")) { - std::cerr << "[StageLoader] Error: difficulty_multipliers incompleta" << '\n'; - return false; + if (yaml.contains("velocity")) { + mult.velocity = yaml["velocity"].get_value(); + } + if (yaml.contains("rotation")) { + mult.rotation = yaml["rotation"].get_value(); + } + if (yaml.contains("tracking")) { + mult.tracking_strength = yaml["tracking"].get_value(); } - mult.velocity = yaml["speed_multiplier"].get_value(); - mult.rotation = yaml["rotation_multiplier"].get_value(); - mult.tracking_strength = yaml["tracking_strength"].get_value(); - - // Validar rangs raonables if (mult.velocity < 0.1F || mult.velocity > 5.0F) { - std::cerr << "[StageLoader] Warning: speed_multiplier fuera de rang (0.1-5.0)" << '\n'; + std::cerr << "[StageLoader] Warning: velocity fora de rang (0.1-5.0)" << '\n'; } if (mult.rotation < 0.1F || mult.rotation > 5.0F) { - std::cerr << "[StageLoader] Warning: rotation_multiplier fuera de rang (0.1-5.0)" << '\n'; + std::cerr << "[StageLoader] Warning: rotation fora de rang (0.1-5.0)" << '\n'; } if (mult.tracking_strength < 0.0F || mult.tracking_strength > 2.0F) { - std::cerr << "[StageLoader] Warning: tracking_strength fuera de rang (0.0-2.0)" << '\n'; + std::cerr << "[StageLoader] Warning: tracking fora de rang (0.0-2.0)" << '\n'; } return true; @@ -219,34 +168,110 @@ namespace StageSystem { } } - auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn { - if (mode_str == "progressive") { - return ModeSpawn::PROGRESSIVE; + auto StageLoader::parseWave(const fkyaml::node& yaml, WaveConfig& wave) -> bool { + try { + if (!yaml.contains("spawn") || !yaml.contains("next")) { + std::cerr << "[StageLoader] Error: wave sense 'spawn' o 'next'" << '\n'; + return false; + } + + if (!yaml["spawn"].is_sequence()) { + std::cerr << "[StageLoader] Error: 'spawn' ha de ser una list" << '\n'; + return false; + } + for (const auto& type_node : yaml["spawn"]) { + auto type_str = type_node.get_value(); + EnemyType type{}; + if (!parseEnemyType(type_str, type)) { + std::cerr << "[StageLoader] Error: tipus d'enemic desconegut '" + << type_str << "'" << '\n'; + return false; + } + wave.spawn.push_back(type); + } + + wave.spawn_interval = yaml.contains("spawn_interval") + ? yaml["spawn_interval"].get_value() + : 0.0F; + + if (!parseNext(yaml["next"], wave.next)) { + return false; + } + + if (!wave.next.isValid()) { + std::cerr << "[StageLoader] Error: wave 'next' sense condició (cal all_dead o timeout)" << '\n'; + return false; + } + + return true; + } catch (const std::exception& e) { + std::cerr << "[StageLoader] Error parsing wave: " << e.what() << '\n'; + return false; } - if (mode_str == "immediate") { - return ModeSpawn::IMMEDIATE; + } + + auto StageLoader::parseEnemyType(const std::string& type_str, EnemyType& out) -> bool { + if (type_str == "pentagon") { + out = EnemyType::PENTAGON; + } else if (type_str == "cuadrado" || type_str == "square") { + out = EnemyType::SQUARE; + } else if (type_str == "molinillo" || type_str == "pinwheel") { + out = EnemyType::PINWHEEL; + } else if (type_str == "star") { + out = EnemyType::STAR; + } else if (type_str == "big_pentagon") { + out = EnemyType::BIG_PENTAGON; + } else { + return false; } - if (mode_str == "wave") { - return ModeSpawn::WAVE; + return true; + } + + auto StageLoader::parseNext(const fkyaml::node& yaml, WaveNext& next) -> bool { + try { + // Forma curta: scalar string ("all_dead" o "end"). + if (yaml.is_string()) { + const auto S = yaml.get_value(); + if (S == "all_dead" || S == "end") { + next.on_all_dead = true; + return true; + } + std::cerr << "[StageLoader] Error: 'next' string desconegut '" << S << "'" << '\n'; + return false; + } + + // Forma llarga: mapping amb claus opcionals. + if (yaml.is_mapping()) { + if (yaml.contains("all_dead")) { + next.on_all_dead = yaml["all_dead"].get_value(); + } + if (yaml.contains("timeout")) { + next.has_timeout = true; + next.timeout = yaml["timeout"].get_value(); + } + return true; + } + + std::cerr << "[StageLoader] Error: 'next' ha de ser string o mapping" << '\n'; + return false; + } catch (const std::exception& e) { + std::cerr << "[StageLoader] Error parsing next: " << e.what() << '\n'; + return false; } - std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str - << "', usant PROGRESSIVE" << '\n'; - return ModeSpawn::PROGRESSIVE; } auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool { if (config.stages.empty()) { - std::cerr << "[StageLoader] Error: sin stage carregat" << '\n'; + std::cerr << "[StageLoader] Error: cap stage carregat" << '\n'; return false; } if (config.stages.size() != config.metadata.total_stages) { std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size() - << ") no coincideix con metadata.total_stages (" + << ") no coincideix amb metadata.total_stages (" << static_cast(config.metadata.total_stages) << ")" << '\n'; } - // Validar stage_id consecutius for (size_t i = 0; i < config.stages.size(); i++) { if (config.stages[i].stage_id != i + 1) { std::cerr << "[StageLoader] Error: stage_id no consecutius (esperat " diff --git a/source/game/stage_system/stage_loader.hpp b/source/game/stage_system/stage_loader.hpp index e25b907..b99a78a 100644 --- a/source/game/stage_system/stage_loader.hpp +++ b/source/game/stage_system/stage_loader.hpp @@ -1,4 +1,4 @@ -// stage_loader.hpp - Carregador de configuración YAML +// stage_loader.hpp - Carregador de configuració YAML // © 2026 JailDesigner #pragma once @@ -11,23 +11,21 @@ namespace StageSystem { -class StageLoader { - public: - // Carregar configuración desde file YAML - // Retorna nullptr si hay errors + class StageLoader { + public: + // Carregar configuració des de file YAML. + // Retorna nullptr si hi ha errors. static auto load(const std::string& path) -> std::unique_ptr; - private: - // Parsing helpers (implementats en .cpp) + private: static auto parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool; static auto parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool; - static auto parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool; - static auto parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool; static auto parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool; - static auto parseSpawnMode(const std::string& mode_str) -> ModeSpawn; + static auto parseWave(const fkyaml::node& yaml, WaveConfig& wave) -> bool; + static auto parseEnemyType(const std::string& type_str, EnemyType& out) -> bool; + static auto parseNext(const fkyaml::node& yaml, WaveNext& next) -> bool; - // Validació static auto validateConfig(const StageSystemConfig& config) -> bool; -}; + }; } // namespace StageSystem diff --git a/source/game/stage_system/stage_manager.cpp b/source/game/stage_system/stage_manager.cpp index d3b1ae6..8f04896 100644 --- a/source/game/stage_system/stage_manager.cpp +++ b/source/game/stage_system/stage_manager.cpp @@ -131,7 +131,7 @@ namespace StageSystem { // 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)pause_spawn; // Es propaga al WaveRunner des de GameScene } void StageManager::processLevelCompleted(float delta_time) { @@ -162,12 +162,11 @@ namespace StageSystem { return; } - // Configure spawn controller - spawn_controller_.configure(stage_config); - spawn_controller_.start(); + wave_runner_.configure(stage_config); + wave_runner_.start(); std::cout << "[StageManager] Carregat stage " << static_cast(stage_id) << ": " - << static_cast(stage_config->total_enemies) << " enemigos" << '\n'; + << stage_config->waves.size() << " onades" << '\n'; } } // namespace StageSystem diff --git a/source/game/stage_system/stage_manager.hpp b/source/game/stage_system/stage_manager.hpp index dd19d94..252cbfd 100644 --- a/source/game/stage_system/stage_manager.hpp +++ b/source/game/stage_system/stage_manager.hpp @@ -6,21 +6,21 @@ #include #include -#include "spawn_controller.hpp" #include "stage_config.hpp" +#include "wave_runner.hpp" namespace StageSystem { -// Estats del stage system -enum class EstatStage : std::uint8_t { - INIT_HUD, // Animación inicial del HUD (3s) - LEVEL_START, // Pantalla "ENEMY INCOMING" (3s) - PLAYING, // Gameplay normal - LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s) -}; + // Estats del stage system + enum class EstatStage : std::uint8_t { + INIT_HUD, // Animación inicial del HUD (3s) + LEVEL_START, // Pantalla "ENEMY INCOMING" (3s) + PLAYING, // Gameplay normal + LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s) + }; -class StageManager { - public: + class StageManager { + public: explicit StageManager(const StageSystemConfig* config); // Lifecycle @@ -28,7 +28,7 @@ class StageManager { void update(float delta_time, bool pause_spawn = false); // Stage progression - void markStageCompleted(); // Call when all enemies destroyed + void markStageCompleted(); // Call when all enemies destroyed [[nodiscard]] auto isGameComplete() const -> bool; // All 10 stages done? // Current state queries @@ -38,17 +38,17 @@ class StageManager { [[nodiscard]] auto getTransitionTimer() const -> float { return timer_transicio_; } [[nodiscard]] auto getLevelStartMessage() const -> const std::string& { return missatge_level_start_actual_; } - // Spawn control (delegate to SpawnController) - auto getSpawnController() -> SpawnController& { return spawn_controller_; } - [[nodiscard]] auto getSpawnController() const -> const SpawnController& { return spawn_controller_; } + // Wave execution (delegated) + auto getWaveRunner() -> WaveRunner& { return wave_runner_; } + [[nodiscard]] auto getWaveRunner() const -> const WaveRunner& { return wave_runner_; } - private: + private: const StageSystemConfig* config_; // Non-owning pointer - SpawnController spawn_controller_; + WaveRunner wave_runner_; EstatStage estat_{EstatStage::LEVEL_START}; - uint8_t stage_actual_{1}; // 1-10 - float timer_transicio_{0.0F}; // Timer for LEVEL_START/LEVEL_COMPLETED (3.0s → 0.0s) + uint8_t stage_actual_{1}; // 1-10 + float timer_transicio_{0.0F}; // Timer for LEVEL_START/LEVEL_COMPLETED (3.0s → 0.0s) std::string missatge_level_start_actual_; // Missatge seleccionat per al level actual // State transitions @@ -59,6 +59,6 @@ class StageManager { static void processPlaying(float delta_time, bool pause_spawn); void processLevelCompleted(float delta_time); void loadStage(uint8_t stage_id); -}; + }; } // namespace StageSystem diff --git a/source/game/stage_system/wave_runner.cpp b/source/game/stage_system/wave_runner.cpp new file mode 100644 index 0000000..85c1819 --- /dev/null +++ b/source/game/stage_system/wave_runner.cpp @@ -0,0 +1,161 @@ +// wave_runner.cpp - Implementació de l'executor d'onades +// © 2026 JailDesigner + +#include "wave_runner.hpp" + +#include +#include +#include +#include + +#include "core/types.hpp" +#include "game/entities/enemy.hpp" +#include "stage_config.hpp" + +namespace StageSystem { + + void WaveRunner::configure(const StageConfig* config) { + config_ = config; + } + + void WaveRunner::start() { + reset(); + if ((config_ == nullptr) || config_->waves.empty()) { + std::cerr << "[WaveRunner] Error: config null o sense onades" << '\n'; + all_waves_emitted_ = true; + return; + } + std::cout << "[WaveRunner] Stage " << static_cast(config_->stage_id) + << ": " << config_->waves.size() << " onades" << '\n'; + } + + void WaveRunner::reset() { + wave_index_ = 0; + wave_elapsed_ = 0.0F; + spawns_emitted_ = 0; + all_waves_emitted_ = false; + } + + void WaveRunner::update(float delta_time, std::array& orni_array, bool pausar) { + if (pausar || config_ == nullptr || all_waves_emitted_) { + // Si ja s'han emès totes, encara hem de poder avaluar stageComplete. + return; + } + + const WaveConfig* wave = currentWave(); + if (wave == nullptr) { + all_waves_emitted_ = true; + return; + } + + wave_elapsed_ += delta_time; + + emitPendingSpawns(orni_array); + + if (shouldAdvance(orni_array)) { + advanceWave(); + } + } + + auto WaveRunner::stageComplete(const std::array& orni_array) const -> bool { + return all_waves_emitted_ && getAliveEnemyCount(orni_array) == 0; + } + + auto WaveRunner::getAliveEnemyCount(const std::array& orni_array) -> uint8_t { + uint8_t count = 0; + for (const auto& enemy : orni_array) { + if (enemy.isActive()) { + count++; + } + } + return count; + } + + auto WaveRunner::currentWave() const -> const WaveConfig* { + if (config_ == nullptr || wave_index_ >= config_->waves.size()) { + return nullptr; + } + return &config_->waves[wave_index_]; + } + + void WaveRunner::emitPendingSpawns(std::array& orni_array) { + const WaveConfig* wave = currentWave(); + if (wave == nullptr) { + return; + } + + // Spawn[i] toca a t = i * spawn_interval (i=0 → t=0). + while (spawns_emitted_ < wave->spawn.size()) { + const float SPAWN_T = static_cast(spawns_emitted_) * wave->spawn_interval; + if (wave_elapsed_ < SPAWN_T) { + break; + } + + // Busca un slot lliure a l'arena. Si no n'hi ha, ho intentem el següent frame. + bool emitted = false; + for (auto& enemy : orni_array) { + if (!enemy.isActive()) { + spawnEnemy(enemy, wave->spawn[spawns_emitted_]); + emitted = true; + break; + } + } + if (!emitted) { + break; + } + spawns_emitted_++; + } + } + + void WaveRunner::spawnEnemy(Enemy& enemy, EnemyType type) { + enemy.init(type, ship_position_); + applyMultipliers(enemy); + } + + void WaveRunner::applyMultipliers(Enemy& enemy) const { + if (config_ == nullptr) { + return; + } + enemy.setVelocity(enemy.getBaseVelocity() * config_->multiplicadors.velocity); + enemy.setRotation(enemy.getBaseRotation() * config_->multiplicadors.rotation); + enemy.setTrackingStrength(config_->multiplicadors.tracking_strength); + } + + auto WaveRunner::shouldAdvance(const std::array& orni_array) const -> bool { + const WaveConfig* wave = currentWave(); + if (wave == nullptr) { + return false; + } + + // Una wave només pot avançar després d'haver emès tots els seus spawns. + const bool ALL_SPAWNED = spawns_emitted_ >= wave->spawn.size(); + + if (wave->next.has_timeout && wave_elapsed_ >= wave->next.timeout) { + // El timeout NO requereix all_spawned: si la wave triga més que el seu + // propi timeout (cas patològic amb spawn_interval massa gran), forcem + // avançar igualment per no encallar-nos. + return true; + } + + if (wave->next.on_all_dead) { + return ALL_SPAWNED && getAliveEnemyCount(orni_array) == 0; + } + + return false; + } + + void WaveRunner::advanceWave() { + wave_index_++; + wave_elapsed_ = 0.0F; + spawns_emitted_ = 0; + + if (config_ == nullptr || wave_index_ >= config_->waves.size()) { + all_waves_emitted_ = true; + std::cout << "[WaveRunner] Totes les onades emeses" << '\n'; + return; + } + std::cout << "[WaveRunner] Avança a onada " << static_cast(wave_index_ + 1) + << "/" << config_->waves.size() << '\n'; + } + +} // namespace StageSystem diff --git a/source/game/stage_system/wave_runner.hpp b/source/game/stage_system/wave_runner.hpp new file mode 100644 index 0000000..73bff55 --- /dev/null +++ b/source/game/stage_system/wave_runner.hpp @@ -0,0 +1,55 @@ +// wave_runner.hpp - Executor d'onades per a un stage +// © 2026 JailDesigner + +#pragma once + +#include +#include + +#include "core/types.hpp" +#include "game/entities/enemy.hpp" +#include "stage_config.hpp" + +namespace StageSystem { + + // Estat intern de la wave actual: quants spawns ja emesos i temps elapsed. + // Una sola wave "viva" alhora a nivell d'emissió, però els enemics + // d'onades anteriors compten per al `all_dead` (model "arena": tot el que + // hi ha viu compta). + class WaveRunner { + public: + WaveRunner() = default; + + void configure(const StageConfig* config); + void start(); + void reset(); + + // Update per frame. orni_array és l'arena real (max 15 simultanis). + void update(float delta_time, std::array& orni_array, bool pausar = false); + + // Stage acabat: totes les waves emeses i arena buida. + [[nodiscard]] auto stageComplete(const std::array& orni_array) const -> bool; + + void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } + + // Comptatge d'enemics vius (utilitat compartida; estàtica per no acoblar). + [[nodiscard]] static auto getAliveEnemyCount(const std::array& orni_array) -> uint8_t; + + private: + const StageConfig* config_{nullptr}; + const Vec2* ship_position_{nullptr}; + + uint8_t wave_index_{0}; // Índex de la wave actual dins config_->waves + float wave_elapsed_{0.0F}; // Segons des de l'inici de la wave actual + uint8_t spawns_emitted_{0}; // Spawns ja col·locats a l'arena d'aquesta wave + bool all_waves_emitted_{false}; // Ja no queden waves per emetre + + [[nodiscard]] auto currentWave() const -> const WaveConfig*; + void emitPendingSpawns(std::array& orni_array); + void spawnEnemy(Enemy& enemy, EnemyType type); + void applyMultipliers(Enemy& enemy) const; + [[nodiscard]] auto shouldAdvance(const std::array& orni_array) const -> bool; + void advanceWave(); + }; + +} // namespace StageSystem