From 1023cde1be3723a662c0e51bfc09e48eb0381d27 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 3 Dec 2025 22:19:44 +0100 Subject: [PATCH] =?UTF-8?q?afegida=20progresi=C3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 2 +- data/stages/stages.yaml | 168 +++++++++++++ source/core/defaults.hpp | 1 + source/game/entities/enemic.cpp | 33 ++- source/game/entities/enemic.hpp | 11 + source/game/escenes/escena_joc.cpp | 170 +++++++++---- source/game/escenes/escena_joc.hpp | 10 + source/game/stage_system/spawn_controller.cpp | 167 ++++++++++++ source/game/stage_system/spawn_controller.hpp | 54 ++++ source/game/stage_system/stage_config.hpp | 84 +++++++ source/game/stage_system/stage_loader.cpp | 238 ++++++++++++++++++ source/game/stage_system/stage_loader.hpp | 32 +++ source/game/stage_system/stage_manager.cpp | 138 ++++++++++ source/game/stage_system/stage_manager.hpp | 59 +++++ 14 files changed, 1109 insertions(+), 58 deletions(-) create mode 100644 data/stages/stages.yaml create mode 100644 source/game/stage_system/spawn_controller.cpp create mode 100644 source/game/stage_system/spawn_controller.hpp create mode 100644 source/game/stage_system/stage_config.hpp create mode 100644 source/game/stage_system/stage_loader.cpp create mode 100644 source/game/stage_system/stage_loader.hpp create mode 100644 source/game/stage_system/stage_manager.cpp create mode 100644 source/game/stage_system/stage_manager.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 446a1c4..534fb67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/data/stages/stages.yaml b/data/stages/stages.yaml new file mode 100644 index 0000000..e6a24ac --- /dev/null +++ b/data/stages/stages.yaml @@ -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 diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index 0394862..1f3d08a 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -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) diff --git a/source/game/entities/enemic.cpp b/source/game/entities/enemic.cpp index fe62ed8..8a43400 100644 --- a/source/game/entities/enemic.cpp +++ b/source/game/entities/enemic.cpp @@ -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; + } +} diff --git a/source/game/entities/enemic.hpp b/source/game/entities/enemic.hpp index b9e3dcd..dae6104 100644 --- a/source/game/entities/enemic.hpp +++ b/source/game/entities/enemic.hpp @@ -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); diff --git a/source/game/escenes/escena_joc.cpp b/source/game/escenes/escena_joc.cpp index 39cb454..88561f3 100644 --- a/source/game/escenes/escena_joc.cpp +++ b/source/game/escenes/escena_joc.cpp @@ -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(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(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(x), static_cast(y)}; + text_.render(missatge, pos, escala, spacing); +} diff --git a/source/game/escenes/escena_joc.hpp b/source/game/escenes/escena_joc.hpp index a87e0df..bcb6ad9 100644 --- a/source/game/escenes/escena_joc.hpp +++ b/source/game/escenes/escena_joc.hpp @@ -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 + // 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 stage_config_; + std::unique_ptr 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 diff --git a/source/game/stage_system/spawn_controller.cpp b/source/game/stage_system/spawn_controller.cpp new file mode 100644 index 0000000..36bab48 --- /dev/null +++ b/source/game/stage_system/spawn_controller.cpp @@ -0,0 +1,167 @@ +// spawn_controller.cpp - Implementació del controlador de spawn +// © 2025 Orni Attack + +#include "spawn_controller.hpp" + +#include +#include + +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(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& 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& 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& 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(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 diff --git a/source/game/stage_system/spawn_controller.hpp b/source/game/stage_system/spawn_controller.hpp new file mode 100644 index 0000000..20dbfe7 --- /dev/null +++ b/source/game/stage_system/spawn_controller.hpp @@ -0,0 +1,54 @@ +// spawn_controller.hpp - Controlador de spawn d'enemics +// © 2025 Orni Attack + +#pragma once + +#include +#include +#include + +#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& orni_array, bool pausar = false); + + // Status queries + bool tots_enemics_spawnejats() const; + bool tots_enemics_destruits(const std::array& orni_array) const; + uint8_t get_enemics_vius(const std::array& orni_array) const; + uint8_t get_enemics_spawnejats() const; + + private: + const ConfigStage* config_; // Non-owning pointer to current stage config + std::vector 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 diff --git a/source/game/stage_system/stage_config.hpp b/source/game/stage_system/stage_config.hpp new file mode 100644 index 0000000..26ff3d4 --- /dev/null +++ b/source/game/stage_system/stage_config.hpp @@ -0,0 +1,84 @@ +// stage_config.hpp - Estructures de dades per configuració d'stages +// © 2025 Orni Attack + +#pragma once + +#include +#include +#include + +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 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 diff --git a/source/game/stage_system/stage_loader.cpp b/source/game/stage_system/stage_loader.cpp new file mode 100644 index 0000000..8432429 --- /dev/null +++ b/source/game/stage_system/stage_loader.cpp @@ -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 +#include +#include + +namespace StageSystem { + +std::unique_ptr 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(); + + // 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(); + meta.total_stages = yaml["total_stages"].get_value(); + meta.descripcio = yaml.contains("description") + ? yaml["description"].get_value() + : ""; + + 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(); + stage.total_enemics = yaml["total_enemies"].get_value(); + + 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(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(); + config.mode = parse_spawn_mode(mode_str); + config.delay_inicial = yaml["initial_delay"].get_value(); + config.interval_spawn = yaml["spawn_interval"].get_value(); + + return true; + } catch (const std::exception& e) { + std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << 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(); + dist.quadrat = yaml["quadrat"].get_value(); + dist.molinillo = yaml["molinillo"].get_value(); + + // 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(); + mult.rotacio = yaml["rotation_multiplier"].get_value(); + mult.tracking_strength = yaml["tracking_strength"].get_value(); + + // 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(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(config.stages[i].stage_id) + << ")" << std::endl; + return false; + } + } + + return true; +} + +} // namespace StageSystem diff --git a/source/game/stage_system/stage_loader.hpp b/source/game/stage_system/stage_loader.hpp new file mode 100644 index 0000000..0b725af --- /dev/null +++ b/source/game/stage_system/stage_loader.hpp @@ -0,0 +1,32 @@ +// stage_loader.hpp - Carregador de configuració YAML +// © 2025 Orni Attack + +#pragma once + +#include +#include +#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 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 diff --git a/source/game/stage_system/stage_manager.cpp b/source/game/stage_system/stage_manager.cpp new file mode 100644 index 0000000..f23e2c4 --- /dev/null +++ b/source/game/stage_system/stage_manager.cpp @@ -0,0 +1,138 @@ +// stage_manager.cpp - Implementació del gestor d'stages +// © 2025 Orni Attack + +#include "stage_manager.hpp" + +#include + +#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(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(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(stage_id) + << std::endl; + return; + } + + // Configure spawn controller + spawn_controller_.configurar(stage_config); + spawn_controller_.iniciar(); + + std::cout << "[StageManager] Carregat stage " << static_cast(stage_id) << ": " + << static_cast(stage_config->total_enemics) << " enemics" << std::endl; +} + +} // namespace StageSystem diff --git a/source/game/stage_system/stage_manager.hpp b/source/game/stage_system/stage_manager.hpp new file mode 100644 index 0000000..d65125f --- /dev/null +++ b/source/game/stage_system/stage_manager.hpp @@ -0,0 +1,59 @@ +// stage_manager.hpp - Gestor d'estat i progressió d'stages +// © 2025 Orni Attack + +#pragma once + +#include +#include + +#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