Merge branch 'feat/wave-based-stages': sistema d'onades declaratives per fase
This commit is contained in:
+157
-144
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user