feat(stages): sistema d'onades declaratives amb condicions de transició
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
|
# © 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:
|
metadata:
|
||||||
version: "1.0"
|
version: "2.0"
|
||||||
total_stages: 10
|
total_stages: 10
|
||||||
description: "Progressive difficulty curve from novice to expert"
|
description: "Wave-based progression"
|
||||||
|
|
||||||
stages:
|
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
|
- stage_id: 1
|
||||||
total_enemies: 50
|
multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon]
|
||||||
initial_delay: 0.3
|
spawn_interval: 0.6
|
||||||
spawn_interval: 0.4
|
next: all_dead
|
||||||
enemy_distribution:
|
- spawn: [pentagon, pentagon, square]
|
||||||
pentagon: 30
|
spawn_interval: 0.5
|
||||||
cuadrado: 25
|
next: all_dead
|
||||||
molinillo: 25
|
- spawn: [pentagon, pentagon, square, square]
|
||||||
big_pentagon: 20
|
spawn_interval: 0.4
|
||||||
difficulty_multipliers:
|
next: end
|
||||||
speed_multiplier: 0.7
|
|
||||||
rotation_multiplier: 0.8
|
|
||||||
tracking_strength: 0.0
|
|
||||||
|
|
||||||
# STAGE 2: Introduction to tracking enemies
|
# STAGE 2 — Apareixen molinillos.
|
||||||
- stage_id: 2
|
- stage_id: 2
|
||||||
total_enemies: 7
|
multipliers: { velocity: 0.95, rotation: 1.0, tracking: 0.4 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, pentagon]
|
||||||
initial_delay: 1.5
|
spawn_interval: 0.5
|
||||||
spawn_interval: 2.5
|
next: all_dead
|
||||||
enemy_distribution:
|
- spawn: [pinwheel]
|
||||||
pentagon: 70
|
next: all_dead
|
||||||
cuadrado: 30
|
- spawn: [pentagon, square, pinwheel]
|
||||||
molinillo: 0
|
spawn_interval: 0.6
|
||||||
difficulty_multipliers:
|
next: all_dead
|
||||||
speed_multiplier: 0.85
|
- spawn: [pinwheel, pinwheel, pentagon]
|
||||||
rotation_multiplier: 0.9
|
spawn_interval: 0.5
|
||||||
tracking_strength: 0.3
|
next: end
|
||||||
|
|
||||||
# STAGE 3: All enemy types, normal speed
|
# STAGE 3 — Primer big_pentagon (HP=10).
|
||||||
- stage_id: 3
|
- stage_id: 3
|
||||||
total_enemies: 10
|
multipliers: { velocity: 1.0, rotation: 1.0, tracking: 0.5 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, square]
|
||||||
initial_delay: 1.0
|
spawn_interval: 0.4
|
||||||
spawn_interval: 2.0
|
next: all_dead
|
||||||
enemy_distribution:
|
- spawn: [big_pentagon]
|
||||||
pentagon: 50
|
next: { all_dead: true, timeout: 12.0 }
|
||||||
cuadrado: 30
|
- spawn: [pinwheel, pinwheel]
|
||||||
molinillo: 20
|
spawn_interval: 0.5
|
||||||
difficulty_multipliers:
|
next: all_dead
|
||||||
speed_multiplier: 1.0
|
- spawn: [pentagon, square, pinwheel, pinwheel]
|
||||||
rotation_multiplier: 1.0
|
spawn_interval: 0.4
|
||||||
tracking_strength: 0.5
|
next: end
|
||||||
|
|
||||||
# STAGE 4: Increased count, faster enemies
|
# STAGE 4 — Pressió creixent: timeouts curts que poden encavalcar onades.
|
||||||
- stage_id: 4
|
- stage_id: 4
|
||||||
total_enemies: 12
|
multipliers: { velocity: 1.05, rotation: 1.1, tracking: 0.6 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, pentagon]
|
||||||
initial_delay: 0.8
|
spawn_interval: 0.3
|
||||||
spawn_interval: 1.8
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
enemy_distribution:
|
- spawn: [square, square]
|
||||||
pentagon: 40
|
spawn_interval: 0.4
|
||||||
cuadrado: 35
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
molinillo: 25
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.4
|
||||||
speed_multiplier: 1.1
|
next: all_dead
|
||||||
rotation_multiplier: 1.15
|
- spawn: [big_pentagon, pentagon, pentagon]
|
||||||
tracking_strength: 0.6
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 5: Maximum count reached
|
# STAGE 5 — Apareix la star (zigzag clon del pentagon).
|
||||||
- stage_id: 5
|
- stage_id: 5
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.1, rotation: 1.2, tracking: 0.7 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [star, star]
|
||||||
initial_delay: 0.5
|
spawn_interval: 0.4
|
||||||
spawn_interval: 1.5
|
next: all_dead
|
||||||
enemy_distribution:
|
- spawn: [pentagon, square, star]
|
||||||
pentagon: 35
|
spawn_interval: 0.4
|
||||||
cuadrado: 35
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
molinillo: 30
|
- spawn: [pinwheel, pinwheel, star, star]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.4
|
||||||
speed_multiplier: 1.2
|
next: all_dead
|
||||||
rotation_multiplier: 1.25
|
- spawn: [big_pentagon, square, square]
|
||||||
tracking_strength: 0.7
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 6: Molinillo becomes dominant
|
# STAGE 6 — Densitat alta, mix amb timeouts agressius.
|
||||||
- stage_id: 6
|
- stage_id: 6
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.15, rotation: 1.25, tracking: 0.8 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pinwheel, pentagon, pinwheel]
|
||||||
initial_delay: 0.3
|
spawn_interval: 0.3
|
||||||
spawn_interval: 1.3
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
enemy_distribution:
|
- spawn: [square, square, star]
|
||||||
pentagon: 30
|
spawn_interval: 0.4
|
||||||
cuadrado: 30
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
molinillo: 40
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.3
|
||||||
speed_multiplier: 1.3
|
next: all_dead
|
||||||
rotation_multiplier: 1.4
|
- spawn: [big_pentagon, pinwheel, pinwheel]
|
||||||
tracking_strength: 0.8
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 7: High intensity, fast spawns
|
# STAGE 7 — Tiradors i agressivitat.
|
||||||
- stage_id: 7
|
- stage_id: 7
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.25, rotation: 1.35, tracking: 0.9 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [square, square, square]
|
||||||
initial_delay: 0.2
|
spawn_interval: 0.5
|
||||||
spawn_interval: 1.0
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
enemy_distribution:
|
- spawn: [pinwheel, pinwheel, pentagon, pentagon]
|
||||||
pentagon: 25
|
spawn_interval: 0.3
|
||||||
cuadrado: 30
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
molinillo: 45
|
- spawn: [star, star, star]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.4
|
||||||
speed_multiplier: 1.4
|
next: all_dead
|
||||||
rotation_multiplier: 1.5
|
- spawn: [big_pentagon, pinwheel, pinwheel, square]
|
||||||
tracking_strength: 0.9
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 8: Expert level, 50% molinillos
|
# STAGE 8 — Pressió constant.
|
||||||
- stage_id: 8
|
- stage_id: 8
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.35, rotation: 1.45, tracking: 1.0 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
initial_delay: 0.1
|
spawn_interval: 0.3
|
||||||
spawn_interval: 0.8
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
enemy_distribution:
|
- spawn: [square, square, star, star]
|
||||||
pentagon: 20
|
spawn_interval: 0.3
|
||||||
cuadrado: 30
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
molinillo: 50
|
- spawn: [big_pentagon]
|
||||||
difficulty_multipliers:
|
next: { all_dead: true, timeout: 8.0 }
|
||||||
speed_multiplier: 1.5
|
- spawn: [pinwheel, pinwheel, square, star, pentagon]
|
||||||
rotation_multiplier: 1.6
|
spawn_interval: 0.3
|
||||||
tracking_strength: 1.0
|
next: end
|
||||||
|
|
||||||
# STAGE 9: Near-maximum difficulty
|
# STAGE 9 — Quasi-final.
|
||||||
- stage_id: 9
|
- stage_id: 9
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.5, rotation: 1.6, tracking: 1.1 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pinwheel, pinwheel, star, star]
|
||||||
initial_delay: 0.0
|
spawn_interval: 0.3
|
||||||
spawn_interval: 0.6
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
enemy_distribution:
|
- spawn: [big_pentagon, square, square]
|
||||||
pentagon: 15
|
spawn_interval: 0.4
|
||||||
cuadrado: 25
|
next: { all_dead: true, timeout: 8.0 }
|
||||||
molinillo: 60
|
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.3
|
||||||
speed_multiplier: 1.6
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
rotation_multiplier: 1.7
|
- spawn: [big_pentagon, pinwheel, pinwheel, square, star]
|
||||||
tracking_strength: 1.1
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 10: Final challenge, 70% molinillos
|
# STAGE 10 — Repte final.
|
||||||
- stage_id: 10
|
- stage_id: 10
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.7, rotation: 1.8, tracking: 1.2 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
|
||||||
initial_delay: 0.0
|
spawn_interval: 0.25
|
||||||
spawn_interval: 0.5
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
enemy_distribution:
|
- spawn: [big_pentagon, square, star]
|
||||||
pentagon: 10
|
spawn_interval: 0.4
|
||||||
cuadrado: 20
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
molinillo: 70
|
- spawn: [pinwheel, pinwheel, star, star, square]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.3
|
||||||
speed_multiplier: 1.8
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
rotation_multiplier: 2.0
|
- spawn: [big_pentagon, big_pentagon, pinwheel, pinwheel, star]
|
||||||
tracking_strength: 1.2
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
stage_manager_->init();
|
stage_manager_->init();
|
||||||
|
|
||||||
// Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
|
// 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
|
// Inicialitzar timers de muerte per player
|
||||||
hit_timer_per_player_[0] = 0.0F;
|
hit_timer_per_player_[0] = 0.0F;
|
||||||
@@ -513,11 +513,11 @@ void GameScene::runStageLevelStart(float delta_time) {
|
|||||||
|
|
||||||
void GameScene::runStagePlaying(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);
|
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);
|
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();
|
stage_manager_->markStageCompleted();
|
||||||
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
|
||||||
return;
|
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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/entities/enemy.hpp"
|
||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
|
|
||||||
// Tipo de mode de spawn
|
// Multiplicadors de dificultat aplicats a tots els enemics del stage.
|
||||||
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
|
|
||||||
struct MultiplicadorsDificultat {
|
struct MultiplicadorsDificultat {
|
||||||
float velocity; // 0.5-2.0 típic
|
float velocity{1.0F};
|
||||||
float rotation; // 0.5-2.0 típic
|
float rotation{1.0F};
|
||||||
float tracking_strength; // 0.0-1.5 (aplicat a Square)
|
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
|
// Metadades del file YAML
|
||||||
struct MetadataStages {
|
struct MetadataStages {
|
||||||
std::string version;
|
std::string version;
|
||||||
uint8_t total_stages;
|
uint8_t total_stages{0};
|
||||||
std::string descripcio;
|
std::string descripcio;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Configuración completa de un stage
|
// Configuració completa d'un stage
|
||||||
struct StageConfig {
|
struct StageConfig {
|
||||||
uint8_t stage_id; // 1-10
|
uint8_t stage_id{0};
|
||||||
uint8_t total_enemies; // 1-200 (el cap simultani en pantalla el marca MAX_ORNIS)
|
|
||||||
ConfigSpawn config_spawn;
|
|
||||||
DistribucioEnemics distribucio;
|
|
||||||
MultiplicadorsDificultat multiplicadors;
|
MultiplicadorsDificultat multiplicadors;
|
||||||
|
std::vector<WaveConfig> waves;
|
||||||
|
|
||||||
// Validació
|
|
||||||
[[nodiscard]] auto isValid() const -> bool {
|
[[nodiscard]] auto isValid() const -> bool {
|
||||||
// stage_id es uint8_t: el rango superior (<=255) está garantizado por
|
if (stage_id == 0 || waves.empty()) {
|
||||||
// el tipo; basta con confirmar que no es 0 (sentinela "sin asignar").
|
return false;
|
||||||
return stage_id >= 1 &&
|
}
|
||||||
total_enemies > 0 && total_enemies <= 200 &&
|
return std::ranges::all_of(waves, [](const WaveConfig& w) {
|
||||||
distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star + distribucio.big_pentagon == 100;
|
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 {
|
struct StageSystemConfig {
|
||||||
MetadataStages metadata;
|
MetadataStages metadata;
|
||||||
std::vector<StageConfig> stages; // Índex [0] = stage 1
|
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* {
|
[[nodiscard]] auto findStage(uint8_t stage_id) const -> const StageConfig* {
|
||||||
if (stage_id < 1 || stage_id > stages.size()) {
|
if (stage_id < 1 || stage_id > stages.size()) {
|
||||||
return nullptr;
|
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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#include "stage_loader.hpp"
|
#include "stage_loader.hpp"
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -15,6 +14,7 @@
|
|||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/enemy.hpp"
|
||||||
#include "stage_config.hpp"
|
#include "stage_config.hpp"
|
||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
@@ -27,22 +27,18 @@ namespace StageSystem {
|
|||||||
normalized = normalized.substr(5);
|
normalized = normalized.substr(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load from resource system
|
|
||||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
||||||
if (data.empty()) {
|
if (data.empty()) {
|
||||||
std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n';
|
std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to string
|
|
||||||
std::string yaml_content(data.begin(), data.end());
|
std::string yaml_content(data.begin(), data.end());
|
||||||
std::stringstream stream(yaml_content);
|
std::stringstream stream(yaml_content);
|
||||||
|
|
||||||
// Parse YAML
|
|
||||||
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
||||||
auto config = std::make_unique<StageSystemConfig>();
|
auto config = std::make_unique<StageSystemConfig>();
|
||||||
|
|
||||||
// Parse metadata
|
|
||||||
if (!yaml.contains("metadata")) {
|
if (!yaml.contains("metadata")) {
|
||||||
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n';
|
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -51,12 +47,10 @@ namespace StageSystem {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse stages
|
|
||||||
if (!yaml.contains("stages")) {
|
if (!yaml.contains("stages")) {
|
||||||
std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n';
|
std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!yaml["stages"].is_sequence()) {
|
if (!yaml["stages"].is_sequence()) {
|
||||||
std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n';
|
std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -67,10 +61,9 @@ namespace StageSystem {
|
|||||||
if (!parseStage(stage_yaml, stage)) {
|
if (!parseStage(stage_yaml, stage)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
config->stages.push_back(stage);
|
config->stages.push_back(std::move(stage));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar configuración
|
|
||||||
if (!validateConfig(*config)) {
|
if (!validateConfig(*config)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -107,29 +100,35 @@ namespace StageSystem {
|
|||||||
|
|
||||||
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
|
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") ||
|
if (!yaml.contains("stage_id") || !yaml.contains("waves")) {
|
||||||
!yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") ||
|
std::cerr << "[StageLoader] Error: stage incompleta (cal stage_id i waves)" << '\n';
|
||||||
!yaml.contains("difficulty_multipliers")) {
|
|
||||||
std::cerr << "[StageLoader] Error: stage incompleta" << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
stage.stage_id = yaml["stage_id"].get_value<uint8_t>();
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (!parseDistribution(yaml["enemy_distribution"], stage.distribucio)) {
|
for (const auto& wave_yaml : yaml["waves"]) {
|
||||||
return false;
|
WaveConfig wave;
|
||||||
}
|
if (!parseWave(wave_yaml, wave)) {
|
||||||
if (!parseMultipliers(yaml["difficulty_multipliers"], stage.multiplicadors)) {
|
return false;
|
||||||
return false;
|
}
|
||||||
|
stage.waves.push_back(std::move(wave));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stage.isValid()) {
|
if (!stage.isValid()) {
|
||||||
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
|
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
|
||||||
<< " no es vàlid" << '\n';
|
<< " no és vàlid" << '\n';
|
||||||
return false;
|
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 {
|
auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
|
if (yaml.contains("velocity")) {
|
||||||
!yaml.contains("tracking_strength")) {
|
mult.velocity = yaml["velocity"].get_value<float>();
|
||||||
std::cerr << "[StageLoader] Error: difficulty_multipliers incompleta" << '\n';
|
}
|
||||||
return false;
|
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) {
|
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) {
|
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) {
|
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;
|
return true;
|
||||||
@@ -219,34 +168,110 @@ namespace StageSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
|
auto StageLoader::parseWave(const fkyaml::node& yaml, WaveConfig& wave) -> bool {
|
||||||
if (mode_str == "progressive") {
|
try {
|
||||||
return ModeSpawn::PROGRESSIVE;
|
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 true;
|
||||||
return ModeSpawn::WAVE;
|
}
|
||||||
|
|
||||||
|
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 {
|
auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
|
||||||
if (config.stages.empty()) {
|
if (config.stages.empty()) {
|
||||||
std::cerr << "[StageLoader] Error: sin stage carregat" << '\n';
|
std::cerr << "[StageLoader] Error: cap stage carregat" << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.stages.size() != config.metadata.total_stages) {
|
if (config.stages.size() != config.metadata.total_stages) {
|
||||||
std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size()
|
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';
|
<< static_cast<int>(config.metadata.total_stages) << ")" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar stage_id consecutius
|
|
||||||
for (size_t i = 0; i < config.stages.size(); i++) {
|
for (size_t i = 0; i < config.stages.size(); i++) {
|
||||||
if (config.stages[i].stage_id != i + 1) {
|
if (config.stages[i].stage_id != i + 1) {
|
||||||
std::cerr << "[StageLoader] Error: stage_id no consecutius (esperat "
|
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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -11,23 +11,21 @@
|
|||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
|
|
||||||
class StageLoader {
|
class StageLoader {
|
||||||
public:
|
public:
|
||||||
// Carregar configuración desde file YAML
|
// Carregar configuració des de file YAML.
|
||||||
// Retorna nullptr si hay errors
|
// Retorna nullptr si hi ha errors.
|
||||||
static auto load(const std::string& path) -> std::unique_ptr<StageSystemConfig>;
|
static auto load(const std::string& path) -> std::unique_ptr<StageSystemConfig>;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Parsing helpers (implementats en .cpp)
|
|
||||||
static auto parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool;
|
static auto parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool;
|
||||||
static auto parseStage(const fkyaml::node& yaml, StageConfig& stage) -> 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 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;
|
static auto validateConfig(const StageSystemConfig& config) -> bool;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace StageSystem
|
} // namespace StageSystem
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ namespace StageSystem {
|
|||||||
// Note: The actual enemy array update happens in GameScene::update()
|
// Note: The actual enemy array update happens in GameScene::update()
|
||||||
// This is just for internal timekeeping
|
// This is just for internal timekeeping
|
||||||
(void)delta_time; // Spawn controller is updated externally
|
(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) {
|
void StageManager::processLevelCompleted(float delta_time) {
|
||||||
@@ -162,12 +162,11 @@ namespace StageSystem {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure spawn controller
|
wave_runner_.configure(stage_config);
|
||||||
spawn_controller_.configure(stage_config);
|
wave_runner_.start();
|
||||||
spawn_controller_.start();
|
|
||||||
|
|
||||||
std::cout << "[StageManager] Carregat stage " << static_cast<int>(stage_id) << ": "
|
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
|
} // namespace StageSystem
|
||||||
|
|||||||
@@ -6,21 +6,21 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "spawn_controller.hpp"
|
|
||||||
#include "stage_config.hpp"
|
#include "stage_config.hpp"
|
||||||
|
#include "wave_runner.hpp"
|
||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
|
|
||||||
// Estats del stage system
|
// Estats del stage system
|
||||||
enum class EstatStage : std::uint8_t {
|
enum class EstatStage : std::uint8_t {
|
||||||
INIT_HUD, // Animación inicial del HUD (3s)
|
INIT_HUD, // Animación inicial del HUD (3s)
|
||||||
LEVEL_START, // Pantalla "ENEMY INCOMING" (3s)
|
LEVEL_START, // Pantalla "ENEMY INCOMING" (3s)
|
||||||
PLAYING, // Gameplay normal
|
PLAYING, // Gameplay normal
|
||||||
LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s)
|
LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s)
|
||||||
};
|
};
|
||||||
|
|
||||||
class StageManager {
|
class StageManager {
|
||||||
public:
|
public:
|
||||||
explicit StageManager(const StageSystemConfig* config);
|
explicit StageManager(const StageSystemConfig* config);
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
@@ -28,7 +28,7 @@ class StageManager {
|
|||||||
void update(float delta_time, bool pause_spawn = false);
|
void update(float delta_time, bool pause_spawn = false);
|
||||||
|
|
||||||
// Stage progression
|
// 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?
|
[[nodiscard]] auto isGameComplete() const -> bool; // All 10 stages done?
|
||||||
|
|
||||||
// Current state queries
|
// Current state queries
|
||||||
@@ -38,17 +38,17 @@ class StageManager {
|
|||||||
[[nodiscard]] auto getTransitionTimer() const -> float { return timer_transicio_; }
|
[[nodiscard]] auto getTransitionTimer() const -> float { return timer_transicio_; }
|
||||||
[[nodiscard]] auto getLevelStartMessage() const -> const std::string& { return missatge_level_start_actual_; }
|
[[nodiscard]] auto getLevelStartMessage() const -> const std::string& { return missatge_level_start_actual_; }
|
||||||
|
|
||||||
// Spawn control (delegate to SpawnController)
|
// Wave execution (delegated)
|
||||||
auto getSpawnController() -> SpawnController& { return spawn_controller_; }
|
auto getWaveRunner() -> WaveRunner& { return wave_runner_; }
|
||||||
[[nodiscard]] auto getSpawnController() const -> const SpawnController& { return spawn_controller_; }
|
[[nodiscard]] auto getWaveRunner() const -> const WaveRunner& { return wave_runner_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const StageSystemConfig* config_; // Non-owning pointer
|
const StageSystemConfig* config_; // Non-owning pointer
|
||||||
SpawnController spawn_controller_;
|
WaveRunner wave_runner_;
|
||||||
|
|
||||||
EstatStage estat_{EstatStage::LEVEL_START};
|
EstatStage estat_{EstatStage::LEVEL_START};
|
||||||
uint8_t stage_actual_{1}; // 1-10
|
uint8_t stage_actual_{1}; // 1-10
|
||||||
float timer_transicio_{0.0F}; // Timer for LEVEL_START/LEVEL_COMPLETED (3.0s → 0.0s)
|
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
|
std::string missatge_level_start_actual_; // Missatge seleccionat per al level actual
|
||||||
|
|
||||||
// State transitions
|
// State transitions
|
||||||
@@ -59,6 +59,6 @@ class StageManager {
|
|||||||
static void processPlaying(float delta_time, bool pause_spawn);
|
static void processPlaying(float delta_time, bool pause_spawn);
|
||||||
void processLevelCompleted(float delta_time);
|
void processLevelCompleted(float delta_time);
|
||||||
void loadStage(uint8_t stage_id);
|
void loadStage(uint8_t stage_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace StageSystem
|
} // 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