afegida progresió

This commit is contained in:
2025-12-03 22:19:44 +01:00
parent a3aeed4b7c
commit 1023cde1be
14 changed files with 1109 additions and 58 deletions

View File

@@ -1,7 +1,7 @@
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(orni VERSION 0.4.0)
project(orni VERSION 0.5.0)
# Info del proyecto
set(PROJECT_LONG_NAME "Orni Attack")

168
data/stages/stages.yaml Normal file
View File

@@ -0,0 +1,168 @@
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
# © 2025 Orni Attack
metadata:
version: "1.0"
total_stages: 10
description: "Progressive difficulty curve from novice to expert"
stages:
# STAGE 1: Tutorial - Only pentagons, slow speed
- stage_id: 1
total_enemies: 5
spawn_config:
mode: "progressive"
initial_delay: 2.0
spawn_interval: 3.0
enemy_distribution:
pentagon: 100
quadrat: 0
molinillo: 0
difficulty_multipliers:
speed_multiplier: 0.7
rotation_multiplier: 0.8
tracking_strength: 0.0
# STAGE 2: Introduction to tracking enemies
- stage_id: 2
total_enemies: 7
spawn_config:
mode: "progressive"
initial_delay: 1.5
spawn_interval: 2.5
enemy_distribution:
pentagon: 70
quadrat: 30
molinillo: 0
difficulty_multipliers:
speed_multiplier: 0.85
rotation_multiplier: 0.9
tracking_strength: 0.3
# STAGE 3: All enemy types, normal speed
- stage_id: 3
total_enemies: 10
spawn_config:
mode: "progressive"
initial_delay: 1.0
spawn_interval: 2.0
enemy_distribution:
pentagon: 50
quadrat: 30
molinillo: 20
difficulty_multipliers:
speed_multiplier: 1.0
rotation_multiplier: 1.0
tracking_strength: 0.5
# STAGE 4: Increased count, faster enemies
- stage_id: 4
total_enemies: 12
spawn_config:
mode: "progressive"
initial_delay: 0.8
spawn_interval: 1.8
enemy_distribution:
pentagon: 40
quadrat: 35
molinillo: 25
difficulty_multipliers:
speed_multiplier: 1.1
rotation_multiplier: 1.15
tracking_strength: 0.6
# STAGE 5: Maximum count reached
- stage_id: 5
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.5
spawn_interval: 1.5
enemy_distribution:
pentagon: 35
quadrat: 35
molinillo: 30
difficulty_multipliers:
speed_multiplier: 1.2
rotation_multiplier: 1.25
tracking_strength: 0.7
# STAGE 6: Molinillo becomes dominant
- stage_id: 6
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.3
spawn_interval: 1.3
enemy_distribution:
pentagon: 30
quadrat: 30
molinillo: 40
difficulty_multipliers:
speed_multiplier: 1.3
rotation_multiplier: 1.4
tracking_strength: 0.8
# STAGE 7: High intensity, fast spawns
- stage_id: 7
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.2
spawn_interval: 1.0
enemy_distribution:
pentagon: 25
quadrat: 30
molinillo: 45
difficulty_multipliers:
speed_multiplier: 1.4
rotation_multiplier: 1.5
tracking_strength: 0.9
# STAGE 8: Expert level, 50% molinillos
- stage_id: 8
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.1
spawn_interval: 0.8
enemy_distribution:
pentagon: 20
quadrat: 30
molinillo: 50
difficulty_multipliers:
speed_multiplier: 1.5
rotation_multiplier: 1.6
tracking_strength: 1.0
# STAGE 9: Near-maximum difficulty
- stage_id: 9
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.0
spawn_interval: 0.6
enemy_distribution:
pentagon: 15
quadrat: 25
molinillo: 60
difficulty_multipliers:
speed_multiplier: 1.6
rotation_multiplier: 1.7
tracking_strength: 1.1
# STAGE 10: Final challenge, 70% molinillos
- stage_id: 10
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.0
spawn_interval: 0.5
enemy_distribution:
pentagon: 10
quadrat: 20
molinillo: 70
difficulty_multipliers:
speed_multiplier: 1.8
rotation_multiplier: 2.0
tracking_strength: 1.2

View File

@@ -79,6 +79,7 @@ constexpr int STARTING_LIVES = 3; // Initial lives
constexpr float DEATH_DURATION = 3.0f; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0f; // Seconds to display game over
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80f; // 80% hitbox (generous)
constexpr float STAGE_TRANSITION_DURATION = 3.0f; // Seconds for LEVEL_START/COMPLETED transitions
} // namespace Game
// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp)

View File

@@ -24,7 +24,8 @@ Enemic::Enemic(SDL_Renderer* renderer)
brightness_(Defaults::Brightness::ENEMIC),
tipus_(TipusEnemic::PENTAGON),
tracking_timer_(0.0f),
ship_position_(nullptr) {
ship_position_(nullptr),
tracking_strength_(0.5f) { // Default tracking strength
// [NUEVO] Forma es carrega a inicialitzar() segons el tipus
// Constructor no carrega forma per permetre tipus diferents
}
@@ -201,8 +202,8 @@ void Enemic::comportament_quadrat(float delta_time) {
while (angle_diff > Constants::PI) angle_diff -= 2.0f * Constants::PI;
while (angle_diff < -Constants::PI) angle_diff += 2.0f * Constants::PI;
// Apply tracking strength
angle_ += angle_diff * Defaults::Enemies::Quadrat::TRACKING_STRENGTH;
// Apply tracking strength (uses member variable, defaults to 0.5)
angle_ += angle_diff * tracking_strength_;
}
}
@@ -394,3 +395,29 @@ float Enemic::calcular_escala_actual() const {
return escala;
}
// [NEW] Stage system API implementations
float Enemic::get_base_velocity() const {
switch (tipus_) {
case TipusEnemic::PENTAGON:
return Defaults::Enemies::Pentagon::VELOCITAT;
case TipusEnemic::QUADRAT:
return Defaults::Enemies::Quadrat::VELOCITAT;
case TipusEnemic::MOLINILLO:
return Defaults::Enemies::Molinillo::VELOCITAT;
}
return 0.0f;
}
float Enemic::get_base_rotation() const {
// Return the base rotation speed (drotacio_base if available, otherwise current drotacio_)
return animacio_.drotacio_base != 0.0f ? animacio_.drotacio_base : drotacio_;
}
void Enemic::set_tracking_strength(float strength) {
// Only applies to QUADRAT type
if (tipus_ == TipusEnemic::QUADRAT) {
tracking_strength_ = strength;
}
}

View File

@@ -52,6 +52,16 @@ class Enemic {
// Set ship position reference for tracking behavior
void set_ship_position(const Punt* ship_pos) { ship_position_ = ship_pos; }
// [NEW] Getters for stage system (base stats)
float get_base_velocity() const;
float get_base_rotation() const;
TipusEnemic get_tipus() const { return tipus_; }
// [NEW] Setters for difficulty multipliers (stage system)
void set_velocity(float vel) { velocitat_ = vel; }
void set_rotation(float rot) { drotacio_ = rot; animacio_.drotacio_base = rot; }
void set_tracking_strength(float strength);
private:
SDL_Renderer* renderer_;
@@ -76,6 +86,7 @@ class Enemic {
// [NEW] Behavior state (type-specific)
float tracking_timer_; // For Quadrat: time since last angle update
const Punt* ship_position_; // Pointer to ship position (for tracking)
float tracking_strength_; // For Quadrat: tracking intensity (0.0-1.5), default 0.5
// [EXISTING] Private methods
void mou(float delta_time);

View File

@@ -15,6 +15,7 @@
#include "core/rendering/line_renderer.hpp"
#include "core/system/gestor_escenes.hpp"
#include "core/system/global_events.hpp"
#include "game/stage_system/stage_loader.hpp"
EscenaJoc::EscenaJoc(SDLManager& sdl)
: sdl_(sdl),
@@ -105,6 +106,19 @@ void EscenaJoc::inicialitzar() {
// Basat en el codi Pascal original: line 376
std::srand(static_cast<unsigned>(std::time(nullptr)));
// [NEW] Load stage configuration (only once)
if (!stage_config_) {
stage_config_ = StageSystem::StageLoader::carregar("data/stages/stages.yaml");
if (!stage_config_) {
std::cerr << "[EscenaJoc] Error: no s'ha pogut carregar stages.yaml" << std::endl;
// Continue without stage system (will crash, but helps debugging)
}
}
// [NEW] Initialize stage manager
stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get());
stage_manager_->inicialitzar();
// Inicialitzar estat de col·lisió
itocado_ = 0;
@@ -119,22 +133,11 @@ void EscenaJoc::inicialitzar() {
// Inicialitzar nau
nau_.inicialitzar();
// Inicialitzar enemics (ORNIs) amb tipus aleatoris
// [MODIFIED] Initialize enemies as inactive (stage system will spawn them)
for (auto& enemy : orni_) {
// Random type distribution: ~40% Pentagon, ~30% Quadrat, ~30% Molinillo
int rand_val = std::rand() % 10;
TipusEnemic tipus;
if (rand_val < 4) {
tipus = TipusEnemic::PENTAGON;
} else if (rand_val < 7) {
tipus = TipusEnemic::QUADRAT;
} else {
tipus = TipusEnemic::MOLINILLO;
}
enemy.inicialitzar(tipus);
enemy = Enemic(sdl_.obte_renderer());
enemy.set_ship_position(&nau_.get_centre()); // Set ship reference for tracking
// DON'T call enemy.inicialitzar() here - stage system handles spawning
}
// Inicialitzar bales
@@ -210,28 +213,53 @@ void EscenaJoc::actualitzar(float delta_time) {
return;
}
// *** NORMAL GAMEPLAY ***
// *** STAGE SYSTEM STATE MACHINE ***
// Update ship (input + physics)
nau_.processar_input(delta_time);
nau_.actualitzar(delta_time);
StageSystem::EstatStage estat = stage_manager_->get_estat();
// Update enemy movement and rotation
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
switch (estat) {
case StageSystem::EstatStage::LEVEL_START:
// Frozen gameplay, countdown timer only
stage_manager_->actualitzar(delta_time);
break;
case StageSystem::EstatStage::PLAYING: {
// [NEW] Update stage manager (spawns enemies, pass pause flag)
bool pausar_spawn = (itocado_ > 0.0f); // Pause during death animation
stage_manager_->get_spawn_controller().actualitzar(delta_time, orni_, pausar_spawn);
// [NEW] Check stage completion (only when not in death sequence)
if (itocado_ == 0.0f) {
auto& spawn_ctrl = stage_manager_->get_spawn_controller();
if (spawn_ctrl.tots_enemics_destruits(orni_)) {
stage_manager_->stage_completat();
break;
}
}
// [EXISTING] Normal gameplay
nau_.processar_input(delta_time);
nau_.actualitzar(delta_time);
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
detectar_col·lisions_bales_enemics();
detectar_col·lisio_nau_enemics();
debris_manager_.actualitzar(delta_time);
break;
}
case StageSystem::EstatStage::LEVEL_COMPLETED:
// Frozen gameplay, countdown timer only
stage_manager_->actualitzar(delta_time);
break;
}
// Update bullet movement
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
// Detect collisions
detectar_col·lisions_bales_enemics();
detectar_col·lisio_nau_enemics(); // New collision check
// Update debris
debris_manager_.actualitzar(delta_time);
}
void EscenaJoc::dibuixar() {
@@ -270,26 +298,38 @@ void EscenaJoc::dibuixar() {
return;
}
// During death sequence, don't draw ship (debris draws automatically)
if (itocado_ == 0.0f) {
nau_.dibuixar();
// [NEW] Stage state rendering
StageSystem::EstatStage estat = stage_manager_->get_estat();
switch (estat) {
case StageSystem::EstatStage::LEVEL_START:
dibuixar_missatge_stage(StageSystem::Constants::MISSATGE_LEVEL_START);
dibuixar_marcador();
break;
case StageSystem::EstatStage::PLAYING:
// [EXISTING] Normal rendering
if (itocado_ == 0.0f) {
nau_.dibuixar();
}
for (const auto& enemy : orni_) {
enemy.dibuixar();
}
for (const auto& bala : bales_) {
bala.dibuixar();
}
debris_manager_.dibuixar();
dibuixar_marcador();
break;
case StageSystem::EstatStage::LEVEL_COMPLETED:
dibuixar_missatge_stage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED);
dibuixar_marcador();
break;
}
// Draw enemies (always)
for (const auto& enemy : orni_) {
enemy.dibuixar();
}
// Draw bullets (always)
for (const auto& bala : bales_) {
bala.dibuixar();
}
// Draw debris
debris_manager_.dibuixar();
// Draw scoreboard
dibuixar_marcador();
}
void EscenaJoc::processar_input(const SDL_Event& event) {
@@ -399,8 +439,13 @@ void EscenaJoc::dibuixar_marges() const {
}
void EscenaJoc::dibuixar_marcador() {
// Display actual lives count (user requested "LIFES" plural English)
std::string text = "SCORE: 01000 LIFES: " + std::to_string(num_vides_) + " LEVEL: 01";
// [MODIFIED] Display current stage number from stage manager
uint8_t stage_num = stage_manager_->get_stage_actual();
std::string stage_str = (stage_num < 10) ? "0" + std::to_string(stage_num)
: std::to_string(stage_num);
std::string text = "SCORE: 01000 LIFES: " + std::to_string(num_vides_) +
" LEVEL: " + stage_str;
// Paràmetres de renderització
const float escala = 0.85f;
@@ -513,3 +558,20 @@ void EscenaJoc::detectar_col·lisio_nau_enemics() {
}
}
}
// [NEW] Stage system helper methods
void EscenaJoc::dibuixar_missatge_stage(const std::string& missatge) {
constexpr float escala = 1.5f;
constexpr float spacing = 3.0f;
float text_width = text_.get_text_width(missatge, escala, spacing);
float text_height = text_.get_text_height(escala);
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
float x = play_area.x + (play_area.w - text_width) / 2.0f;
float y = play_area.y + (play_area.h - text_height) / 2.0f;
Punt pos = {static_cast<float>(x), static_cast<float>(y)};
text_.render(missatge, pos, escala, spacing);
}

View File

@@ -15,10 +15,13 @@
#include "../entities/bala.hpp"
#include "../entities/enemic.hpp"
#include "../entities/nau.hpp"
#include "../stage_system/stage_manager.hpp"
#include "core/graphics/vector_text.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/types.hpp"
#include <memory>
// Classe principal del joc (escena)
class EscenaJoc {
public:
@@ -53,12 +56,19 @@ class EscenaJoc {
// Text vectorial
Graphics::VectorText text_;
// [NEW] Stage system
std::unique_ptr<StageSystem::ConfigSistemaStages> stage_config_;
std::unique_ptr<StageSystem::StageManager> stage_manager_;
// Funcions privades
void tocado();
void detectar_col·lisions_bales_enemics(); // Col·lisions bala-enemic
void detectar_col·lisio_nau_enemics(); // Ship-enemy collision detection
void dibuixar_marges() const; // Dibuixar vores de la zona de joc
void dibuixar_marcador(); // Dibuixar marcador de puntuació
// [NEW] Stage system helpers
void dibuixar_missatge_stage(const std::string& missatge);
};
#endif // ESCENA_JOC_HPP

View File

@@ -0,0 +1,167 @@
// spawn_controller.cpp - Implementació del controlador de spawn
// © 2025 Orni Attack
#include "spawn_controller.hpp"
#include <cstdlib>
#include <iostream>
namespace StageSystem {
SpawnController::SpawnController()
: config_(nullptr), temps_transcorregut_(0.0f), index_spawn_actual_(0) {}
void SpawnController::configurar(const ConfigStage* config) {
config_ = config;
}
void SpawnController::iniciar() {
if (!config_) {
std::cerr << "[SpawnController] Error: config_ és null" << std::endl;
return;
}
reset();
generar_spawn_events();
std::cout << "[SpawnController] Stage " << static_cast<int>(config_->stage_id)
<< ": generats " << spawn_queue_.size() << " spawn events" << std::endl;
}
void SpawnController::reset() {
spawn_queue_.clear();
temps_transcorregut_ = 0.0f;
index_spawn_actual_ = 0;
}
void SpawnController::actualitzar(float delta_time, std::array<Enemic, 15>& orni_array, bool pausar) {
if (!config_ || 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& enemic : orni_array) {
if (!enemic.esta_actiu()) {
spawn_enemic(enemic, event.tipus);
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;
}
}
}
bool SpawnController::tots_enemics_spawnejats() const {
return index_spawn_actual_ >= spawn_queue_.size();
}
bool SpawnController::tots_enemics_destruits(const std::array<Enemic, 15>& orni_array) const {
if (!tots_enemics_spawnejats()) {
return false;
}
for (const auto& enemic : orni_array) {
if (enemic.esta_actiu()) {
return false;
}
}
return true;
}
uint8_t SpawnController::get_enemics_vius(const std::array<Enemic, 15>& orni_array) const {
uint8_t count = 0;
for (const auto& enemic : orni_array) {
if (enemic.esta_actiu()) {
count++;
}
}
return count;
}
uint8_t SpawnController::get_enemics_spawnejats() const {
return static_cast<uint8_t>(index_spawn_actual_);
}
void SpawnController::generar_spawn_events() {
if (!config_) {
return;
}
for (uint8_t i = 0; i < config_->total_enemics; i++) {
float spawn_time = config_->config_spawn.delay_inicial +
(i * config_->config_spawn.interval_spawn);
TipusEnemic tipus = seleccionar_tipus_aleatori();
spawn_queue_.push_back({spawn_time, tipus, false});
}
}
TipusEnemic SpawnController::seleccionar_tipus_aleatori() const {
if (!config_) {
return TipusEnemic::PENTAGON;
}
// Weighted random selection based on distribution
int rand_val = std::rand() % 100;
if (rand_val < config_->distribucio.pentagon) {
return TipusEnemic::PENTAGON;
} else if (rand_val < config_->distribucio.pentagon + config_->distribucio.quadrat) {
return TipusEnemic::QUADRAT;
} else {
return TipusEnemic::MOLINILLO;
}
}
void SpawnController::spawn_enemic(Enemic& enemic, TipusEnemic tipus) {
// Initialize enemy
enemic.inicialitzar(tipus);
// Apply difficulty multipliers
aplicar_multiplicadors(enemic);
}
void SpawnController::aplicar_multiplicadors(Enemic& enemic) const {
if (!config_) {
return;
}
// Apply velocity multiplier
float base_vel = enemic.get_base_velocity();
enemic.set_velocity(base_vel * config_->multiplicadors.velocitat);
// Apply rotation multiplier
float base_rot = enemic.get_base_rotation();
enemic.set_rotation(base_rot * config_->multiplicadors.rotacio);
// Apply tracking strength (only affects QUADRAT)
enemic.set_tracking_strength(config_->multiplicadors.tracking_strength);
}
} // namespace StageSystem

View File

@@ -0,0 +1,54 @@
// spawn_controller.hpp - Controlador de spawn d'enemics
// © 2025 Orni Attack
#pragma once
#include <array>
#include <cstdint>
#include <vector>
#include "core/types.hpp"
#include "game/entities/enemic.hpp"
#include "stage_config.hpp"
namespace StageSystem {
// Informació de spawn planificat
struct SpawnEvent {
float temps_spawn; // Temps absolut (segons) per spawnejar
TipusEnemic tipus; // Tipus d'enemic
bool spawnejat; // Ja s'ha processat?
};
class SpawnController {
public:
SpawnController();
// Configuration
void configurar(const ConfigStage* config); // Set stage config
void iniciar(); // Generate spawn schedule
void reset(); // Clear all pending spawns
// Update
void actualitzar(float delta_time, std::array<Enemic, 15>& orni_array, bool pausar = false);
// Status queries
bool tots_enemics_spawnejats() const;
bool tots_enemics_destruits(const std::array<Enemic, 15>& orni_array) const;
uint8_t get_enemics_vius(const std::array<Enemic, 15>& orni_array) const;
uint8_t get_enemics_spawnejats() const;
private:
const ConfigStage* config_; // Non-owning pointer to current stage config
std::vector<SpawnEvent> spawn_queue_;
float temps_transcorregut_; // Elapsed time since stage start
uint8_t index_spawn_actual_; // Next spawn to process
// Spawn generation
void generar_spawn_events();
TipusEnemic seleccionar_tipus_aleatori() const;
void spawn_enemic(Enemic& enemic, TipusEnemic tipus);
void aplicar_multiplicadors(Enemic& enemic) const;
};
} // namespace StageSystem

View File

@@ -0,0 +1,84 @@
// stage_config.hpp - Estructures de dades per configuració d'stages
// © 2025 Orni Attack
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace StageSystem {
// Tipus de mode de spawn
enum class ModeSpawn {
PROGRESSIVE, // Spawn progressiu amb intervals
IMMEDIATE, // Tots els enemics de cop
WAVE // Onades de 3-5 enemics (futura extensió)
};
// Configuració de spawn
struct ConfigSpawn {
ModeSpawn mode;
float delay_inicial; // Segons abans del primer spawn
float interval_spawn; // Segons entre spawns consecutius
};
// Distribució de tipus d'enemics (percentatges)
struct DistribucioEnemics {
uint8_t pentagon; // 0-100
uint8_t quadrat; // 0-100
uint8_t molinillo; // 0-100
// Suma ha de ser 100, validat en StageLoader
};
// Multiplicadors de dificultat
struct MultiplicadorsDificultat {
float velocitat; // 0.5-2.0 típic
float rotacio; // 0.5-2.0 típic
float tracking_strength; // 0.0-1.5 (aplicat a Quadrat)
};
// Metadades del fitxer YAML
struct MetadataStages {
std::string version;
uint8_t total_stages;
std::string descripcio;
};
// Configuració completa d'un stage
struct ConfigStage {
uint8_t stage_id; // 1-10
uint8_t total_enemics; // 5-15
ConfigSpawn config_spawn;
DistribucioEnemics distribucio;
MultiplicadorsDificultat multiplicadors;
// Validació
bool es_valid() const {
return stage_id >= 1 && stage_id <= 255 &&
total_enemics > 0 && total_enemics <= 15 &&
distribucio.pentagon + distribucio.quadrat + distribucio.molinillo == 100;
}
};
// Configuració completa del sistema (carregada des de YAML)
struct ConfigSistemaStages {
MetadataStages metadata;
std::vector<ConfigStage> stages; // Índex [0] = stage 1
// Obtenir configuració d'un stage específic
const ConfigStage* obte_stage(uint8_t stage_id) const {
if (stage_id < 1 || stage_id > stages.size()) {
return nullptr;
}
return &stages[stage_id - 1];
}
};
// Constants per missatges de transició
namespace Constants {
constexpr const char* MISSATGE_LEVEL_START = "ENEMY INCOMING";
constexpr const char* MISSATGE_LEVEL_COMPLETED = "GOOD JOB COMMANDER!";
}
} // namespace StageSystem

View File

@@ -0,0 +1,238 @@
// stage_loader.cpp - Implementació del carregador de configuració YAML
// © 2025 Orni Attack
#include "stage_loader.hpp"
#include "external/fkyaml_node.hpp"
#include <iostream>
#include <fstream>
#include <sstream>
namespace StageSystem {
std::unique_ptr<ConfigSistemaStages> StageLoader::carregar(const std::string& path) {
try {
// Llegir fitxer YAML
std::ifstream file(path);
if (!file.is_open()) {
std::cerr << "[StageLoader] Error: no es pot obrir el fitxer " << path << std::endl;
return nullptr;
}
// Parse YAML
fkyaml::node yaml = fkyaml::node::deserialize(file);
auto config = std::make_unique<ConfigSistemaStages>();
// Parse metadata
if (!yaml.contains("metadata")) {
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << std::endl;
return nullptr;
}
if (!parse_metadata(yaml["metadata"], config->metadata)) {
return nullptr;
}
// Parse stages
if (!yaml.contains("stages")) {
std::cerr << "[StageLoader] Error: falta camp 'stages'" << std::endl;
return nullptr;
}
if (!yaml["stages"].is_sequence()) {
std::cerr << "[StageLoader] Error: 'stages' ha de ser una llista" << std::endl;
return nullptr;
}
for (const auto& stage_yaml : yaml["stages"]) {
ConfigStage stage;
if (!parse_stage(stage_yaml, stage)) {
return nullptr;
}
config->stages.push_back(stage);
}
// Validar configuració
if (!validar_config(*config)) {
return nullptr;
}
std::cout << "[StageLoader] Carregats " << config->stages.size()
<< " stages correctament" << std::endl;
return config;
} catch (const std::exception& e) {
std::cerr << "[StageLoader] Excepció: " << e.what() << std::endl;
return nullptr;
}
}
bool StageLoader::parse_metadata(const fkyaml::node& yaml, MetadataStages& meta) {
try {
if (!yaml.contains("version") || !yaml.contains("total_stages")) {
std::cerr << "[StageLoader] Error: metadata incompleta" << std::endl;
return false;
}
meta.version = yaml["version"].get_value<std::string>();
meta.total_stages = yaml["total_stages"].get_value<uint8_t>();
meta.descripcio = yaml.contains("description")
? yaml["description"].get_value<std::string>()
: "";
return true;
} catch (const std::exception& e) {
std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << std::endl;
return false;
}
}
bool StageLoader::parse_stage(const fkyaml::node& yaml, ConfigStage& stage) {
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" << std::endl;
return false;
}
stage.stage_id = yaml["stage_id"].get_value<uint8_t>();
stage.total_enemics = yaml["total_enemies"].get_value<uint8_t>();
if (!parse_spawn_config(yaml["spawn_config"], stage.config_spawn)) {
return false;
}
if (!parse_distribution(yaml["enemy_distribution"], stage.distribucio)) {
return false;
}
if (!parse_multipliers(yaml["difficulty_multipliers"], stage.multiplicadors)) {
return false;
}
if (!stage.es_valid()) {
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
<< " no és vàlid" << std::endl;
return false;
}
return true;
} catch (const std::exception& e) {
std::cerr << "[StageLoader] Error parsing stage: " << e.what() << std::endl;
return false;
}
}
bool StageLoader::parse_spawn_config(const fkyaml::node& yaml, ConfigSpawn& config) {
try {
if (!yaml.contains("mode") || !yaml.contains("initial_delay") ||
!yaml.contains("spawn_interval")) {
std::cerr << "[StageLoader] Error: spawn_config incompleta" << std::endl;
return false;
}
std::string mode_str = yaml["mode"].get_value<std::string>();
config.mode = parse_spawn_mode(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() << std::endl;
return false;
}
}
bool StageLoader::parse_distribution(const fkyaml::node& yaml, DistribucioEnemics& dist) {
try {
if (!yaml.contains("pentagon") || !yaml.contains("quadrat") ||
!yaml.contains("molinillo")) {
std::cerr << "[StageLoader] Error: enemy_distribution incompleta" << std::endl;
return false;
}
dist.pentagon = yaml["pentagon"].get_value<uint8_t>();
dist.quadrat = yaml["quadrat"].get_value<uint8_t>();
dist.molinillo = yaml["molinillo"].get_value<uint8_t>();
// Validar que suma 100
int sum = dist.pentagon + dist.quadrat + dist.molinillo;
if (sum != 100) {
std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << std::endl;
return false;
}
return true;
} catch (const std::exception& e) {
std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << std::endl;
return false;
}
}
bool StageLoader::parse_multipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) {
try {
if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
!yaml.contains("tracking_strength")) {
std::cerr << "[StageLoader] Error: difficulty_multipliers incompleta" << std::endl;
return false;
}
mult.velocitat = yaml["speed_multiplier"].get_value<float>();
mult.rotacio = yaml["rotation_multiplier"].get_value<float>();
mult.tracking_strength = yaml["tracking_strength"].get_value<float>();
// Validar rangs raonables
if (mult.velocitat < 0.1f || mult.velocitat > 5.0f) {
std::cerr << "[StageLoader] Warning: speed_multiplier fora de rang (0.1-5.0)" << std::endl;
}
if (mult.rotacio < 0.1f || mult.rotacio > 5.0f) {
std::cerr << "[StageLoader] Warning: rotation_multiplier fora de rang (0.1-5.0)" << std::endl;
}
if (mult.tracking_strength < 0.0f || mult.tracking_strength > 2.0f) {
std::cerr << "[StageLoader] Warning: tracking_strength fora de rang (0.0-2.0)" << std::endl;
}
return true;
} catch (const std::exception& e) {
std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << std::endl;
return false;
}
}
ModeSpawn StageLoader::parse_spawn_mode(const std::string& mode_str) {
if (mode_str == "progressive") {
return ModeSpawn::PROGRESSIVE;
} else if (mode_str == "immediate") {
return ModeSpawn::IMMEDIATE;
} else if (mode_str == "wave") {
return ModeSpawn::WAVE;
} else {
std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str
<< "', usant PROGRESSIVE" << std::endl;
return ModeSpawn::PROGRESSIVE;
}
}
bool StageLoader::validar_config(const ConfigSistemaStages& config) {
if (config.stages.empty()) {
std::cerr << "[StageLoader] Error: cap stage carregat" << std::endl;
return false;
}
if (config.stages.size() != config.metadata.total_stages) {
std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size()
<< ") no coincideix amb metadata.total_stages ("
<< static_cast<int>(config.metadata.total_stages) << ")" << std::endl;
}
// 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 "
<< i + 1 << ", trobat " << static_cast<int>(config.stages[i].stage_id)
<< ")" << std::endl;
return false;
}
}
return true;
}
} // namespace StageSystem

View File

@@ -0,0 +1,32 @@
// stage_loader.hpp - Carregador de configuració YAML
// © 2025 Orni Attack
#pragma once
#include <memory>
#include <string>
#include "external/fkyaml_node.hpp"
#include "stage_config.hpp"
namespace StageSystem {
class StageLoader {
public:
// Carregar configuració des de fitxer YAML
// Retorna nullptr si hi ha errors
static std::unique_ptr<ConfigSistemaStages> carregar(const std::string& path);
private:
// Parsing helpers (implementats en .cpp)
static bool parse_metadata(const fkyaml::node& yaml, MetadataStages& meta);
static bool parse_stage(const fkyaml::node& yaml, ConfigStage& stage);
static bool parse_spawn_config(const fkyaml::node& yaml, ConfigSpawn& config);
static bool parse_distribution(const fkyaml::node& yaml, DistribucioEnemics& dist);
static bool parse_multipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult);
static ModeSpawn parse_spawn_mode(const std::string& mode_str);
// Validació
static bool validar_config(const ConfigSistemaStages& config);
};
} // namespace StageSystem

View File

@@ -0,0 +1,138 @@
// stage_manager.cpp - Implementació del gestor d'stages
// © 2025 Orni Attack
#include "stage_manager.hpp"
#include <iostream>
#include "core/defaults.hpp"
namespace StageSystem {
StageManager::StageManager(const ConfigSistemaStages* config)
: config_(config),
estat_(EstatStage::LEVEL_START),
stage_actual_(1),
timer_transicio_(0.0f) {
if (!config_) {
std::cerr << "[StageManager] Error: config és null" << std::endl;
}
}
void StageManager::inicialitzar() {
stage_actual_ = 1;
carregar_stage(stage_actual_);
canviar_estat(EstatStage::LEVEL_START);
std::cout << "[StageManager] Inicialitzat a stage " << static_cast<int>(stage_actual_)
<< std::endl;
}
void StageManager::actualitzar(float delta_time, bool pausar_spawn) {
switch (estat_) {
case EstatStage::LEVEL_START:
processar_level_start(delta_time);
break;
case EstatStage::PLAYING:
processar_playing(delta_time, pausar_spawn);
break;
case EstatStage::LEVEL_COMPLETED:
processar_level_completed(delta_time);
break;
}
}
void StageManager::stage_completat() {
std::cout << "[StageManager] Stage " << static_cast<int>(stage_actual_) << " completat!"
<< std::endl;
canviar_estat(EstatStage::LEVEL_COMPLETED);
}
bool StageManager::tot_completat() const {
return stage_actual_ >= config_->metadata.total_stages &&
estat_ == EstatStage::LEVEL_COMPLETED &&
timer_transicio_ <= 0.0f;
}
const ConfigStage* StageManager::get_config_actual() const {
return config_->obte_stage(stage_actual_);
}
void StageManager::canviar_estat(EstatStage nou_estat) {
estat_ = nou_estat;
// Reset transition timer for LEVEL_START and LEVEL_COMPLETED
if (nou_estat == EstatStage::LEVEL_START || nou_estat == EstatStage::LEVEL_COMPLETED) {
timer_transicio_ = Defaults::Game::STAGE_TRANSITION_DURATION;
}
std::cout << "[StageManager] Canvi d'estat: ";
switch (nou_estat) {
case EstatStage::LEVEL_START:
std::cout << "LEVEL_START";
break;
case EstatStage::PLAYING:
std::cout << "PLAYING";
break;
case EstatStage::LEVEL_COMPLETED:
std::cout << "LEVEL_COMPLETED";
break;
}
std::cout << std::endl;
}
void StageManager::processar_level_start(float delta_time) {
timer_transicio_ -= delta_time;
if (timer_transicio_ <= 0.0f) {
canviar_estat(EstatStage::PLAYING);
}
}
void StageManager::processar_playing(float delta_time, bool pausar_spawn) {
// Update spawn controller (pauses when pausar_spawn = true)
// Note: The actual enemy array update happens in EscenaJoc::actualitzar()
// This is just for internal timekeeping
(void)delta_time; // Spawn controller is updated externally
(void)pausar_spawn; // Passed to spawn_controller_.actualitzar() by EscenaJoc
}
void StageManager::processar_level_completed(float delta_time) {
timer_transicio_ -= delta_time;
if (timer_transicio_ <= 0.0f) {
// Advance to next stage
stage_actual_++;
// Loop back to stage 1 after final stage
if (stage_actual_ > config_->metadata.total_stages) {
stage_actual_ = 1;
std::cout << "[StageManager] Totes les stages completades! Tornant a stage 1"
<< std::endl;
}
// Load next stage
carregar_stage(stage_actual_);
canviar_estat(EstatStage::LEVEL_START);
}
}
void StageManager::carregar_stage(uint8_t stage_id) {
const ConfigStage* stage_config = config_->obte_stage(stage_id);
if (!stage_config) {
std::cerr << "[StageManager] Error: no es pot trobar stage " << static_cast<int>(stage_id)
<< std::endl;
return;
}
// Configure spawn controller
spawn_controller_.configurar(stage_config);
spawn_controller_.iniciar();
std::cout << "[StageManager] Carregat stage " << static_cast<int>(stage_id) << ": "
<< static_cast<int>(stage_config->total_enemics) << " enemics" << std::endl;
}
} // namespace StageSystem

View File

@@ -0,0 +1,59 @@
// stage_manager.hpp - Gestor d'estat i progressió d'stages
// © 2025 Orni Attack
#pragma once
#include <cstdint>
#include <memory>
#include "spawn_controller.hpp"
#include "stage_config.hpp"
namespace StageSystem {
// Estats del stage system
enum class EstatStage {
LEVEL_START, // Pantalla "ENEMY INCOMING" (3s)
PLAYING, // Gameplay normal
LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s)
};
class StageManager {
public:
explicit StageManager(const ConfigSistemaStages* config);
// Lifecycle
void inicialitzar(); // Reset to stage 1
void actualitzar(float delta_time, bool pausar_spawn = false);
// Stage progression
void stage_completat(); // Call when all enemies destroyed
bool tot_completat() const; // All 10 stages done?
// Current state queries
EstatStage get_estat() const { return estat_; }
uint8_t get_stage_actual() const { return stage_actual_; }
const ConfigStage* get_config_actual() const;
float get_timer_transicio() const { return timer_transicio_; }
// Spawn control (delegate to SpawnController)
SpawnController& get_spawn_controller() { return spawn_controller_; }
const SpawnController& get_spawn_controller() const { return spawn_controller_; }
private:
const ConfigSistemaStages* config_; // Non-owning pointer
SpawnController spawn_controller_;
EstatStage estat_;
uint8_t stage_actual_; // 1-10
float timer_transicio_; // Timer for LEVEL_START/LEVEL_COMPLETED (3.0s → 0.0s)
// State transitions
void canviar_estat(EstatStage nou_estat);
void processar_level_start(float delta_time);
void processar_playing(float delta_time, bool pausar_spawn);
void processar_level_completed(float delta_time);
void carregar_stage(uint8_t stage_id);
};
} // namespace StageSystem