Merge branch 'feat/wave-based-stages': sistema d'onades declaratives per fase

This commit is contained in:
2026-05-26 13:37:24 +02:00
11 changed files with 573 additions and 560 deletions
+157 -144
View File
@@ -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
+4 -4
View File
@@ -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;
@@ -1,174 +0,0 @@
// spawn_controller.cpp - Implementació del controlador de spawn
// © 2026 JailDesigner
#include "spawn_controller.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <utility>
#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<int>(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<Enemy, 15>& 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<Enemy, 15>& 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<Enemy, 15>& 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<uint8_t>(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
@@ -1,59 +0,0 @@
// spawn_controller.hpp - Controlador de spawn de enemigos
// © 2026 JailDesigner
#pragma once
#include <array>
#include <cstdint>
#include <vector>
#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<Enemy, 15>& orni_array, bool pausar = false);
// Status queries
[[nodiscard]] auto allEnemiesSpawned() const -> bool;
[[nodiscard]] auto allEnemiesDestroyed(const std::array<Enemy, 15>& orni_array) const -> bool;
// Estático: solo recorre el array pasado; no consulta estado del controller.
[[nodiscard]] static auto getAliveEnemyCount(const std::array<Enemy, 15>& 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<SpawnEvent> 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
+39 -43
View File
@@ -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 <algorithm>
#include <cstdint>
#include <string>
#include <vector>
#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<EnemyType> 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<WaveConfig> 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<StageConfig> 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;
+121 -96
View File
@@ -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 <cstddef>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>
@@ -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<uint8_t> 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<StageSystemConfig>();
// 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<uint8_t>();
stage.total_enemies = yaml["total_enemies"].get_value<uint8_t>();
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<int>(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<std::string>();
config.mode = parseSpawnMode(mode_str);
config.delay_inicial = yaml["initial_delay"].get_value<float>();
config.interval_spawn = yaml["spawn_interval"].get_value<float>();
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<uint8_t>();
dist.cuadrado = yaml["cuadrado"].get_value<uint8_t>();
dist.molinillo = yaml["molinillo"].get_value<uint8_t>();
// 'star' i 'big_pentagon' són opcionals per compatibilitat amb stages antics (default 0).
dist.star = yaml.contains("star") ? yaml["star"].get_value<uint8_t>() : 0;
dist.big_pentagon = yaml.contains("big_pentagon") ? yaml["big_pentagon"].get_value<uint8_t>() : 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<float>();
}
if (yaml.contains("rotation")) {
mult.rotation = yaml["rotation"].get_value<float>();
}
if (yaml.contains("tracking")) {
mult.tracking_strength = yaml["tracking"].get_value<float>();
}
mult.velocity = yaml["speed_multiplier"].get_value<float>();
mult.rotation = yaml["rotation_multiplier"].get_value<float>();
mult.tracking_strength = yaml["tracking_strength"].get_value<float>();
// 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<std::string>();
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<float>()
: 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<std::string>();
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<bool>();
}
if (yaml.contains("timeout")) {
next.has_timeout = true;
next.timeout = yaml["timeout"].get_value<float>();
}
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<int>(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 "
+10 -12
View File
@@ -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<StageSystemConfig>;
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
+7 -9
View File
@@ -127,11 +127,10 @@ namespace StageSystem {
}
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
// No-op: el WaveRunner s'actualitza des de GameScene::runStagePlaying,
// no des d'ací. La signatura es manté per simetria amb les altres process*.
(void)delta_time;
(void)pause_spawn;
}
void StageManager::processLevelCompleted(float delta_time) {
@@ -162,12 +161,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<int>(stage_id) << ": "
<< static_cast<int>(stage_config->total_enemies) << " enemigos" << '\n';
<< stage_config->waves.size() << " onades" << '\n';
}
} // namespace StageSystem
+19 -19
View File
@@ -6,21 +6,21 @@
#include <cstdint>
#include <string>
#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
+161
View File
@@ -0,0 +1,161 @@
// wave_runner.cpp - Implementació de l'executor d'onades
// © 2026 JailDesigner
#include "wave_runner.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
#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<int>(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<Enemy, 15>& 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<Enemy, 15>& orni_array) const -> bool {
return all_waves_emitted_ && getAliveEnemyCount(orni_array) == 0;
}
auto WaveRunner::getAliveEnemyCount(const std::array<Enemy, 15>& 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<Enemy, 15>& 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<float>(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<Enemy, 15>& 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<int>(wave_index_ + 1)
<< "/" << config_->waves.size() << '\n';
}
} // namespace StageSystem
+55
View File
@@ -0,0 +1,55 @@
// wave_runner.hpp - Executor d'onades per a un stage
// © 2026 JailDesigner
#pragma once
#include <array>
#include <cstdint>
#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<Enemy, 15>& orni_array, bool pausar = false);
// Stage acabat: totes les waves emeses i arena buida.
[[nodiscard]] auto stageComplete(const std::array<Enemy, 15>& 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<Enemy, 15>& 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<Enemy, 15>& orni_array);
void spawnEnemy(Enemy& enemy, EnemyType type);
void applyMultipliers(Enemy& enemy) const;
[[nodiscard]] auto shouldAdvance(const std::array<Enemy, 15>& orni_array) const -> bool;
void advanceWave();
};
} // namespace StageSystem