20 Commits

Author SHA1 Message Date
64ab08973c efecte maquina d'escriure per als textos d'entrada de fase 2025-12-09 19:38:29 +01:00
94a7a38cdd afegit sistema de punts 2025-12-09 16:56:07 +01:00
76165e4345 limitada la velocitat angular dels debris i transformada en velocitat lineal tangencial 2025-12-09 13:38:18 +01:00
767a1f6af8 incrementada velocitat base angular dels enemics 2025-12-09 13:18:24 +01:00
20ca024100 les bales ara son redones 2025-12-09 12:58:44 +01:00
3c3857c1b2 debris hereten velocitat angular 2025-12-09 12:30:03 +01:00
523342fed9 canvis en el inici i final de fase 2025-12-09 11:45:28 +01:00
217ca58b1a millorat el spawn d'enemics: perimetre de seguretat i animació amb invulnerabilitat 2025-12-09 10:21:42 +01:00
ec6565bf71 debris hereta brillantor i velocitat 2025-12-09 09:25:46 +01:00
cd7f06f3a1 corregit el comptador de FPS 2025-12-08 22:13:26 +01:00
8886873ed5 corregida la posició dels fitxers en el .dmg 2025-12-08 21:55:49 +01:00
a41e696b69 afegit resources.pack y prefixe a les rutes de recursos 2025-12-08 21:48:52 +01:00
4b7cbd88bb nou icon per a la release sorpresa 2025-12-04 18:38:30 +01:00
789cbbc593 afegida veu: good job commander
calibrats els volumnes de musica i efectes
afegida forma: ship2.shp
canviat tamany de textos de canvi de pantalla
2025-12-04 18:27:39 +01:00
1dd87c0707 corregit: al pulsar per a jugar, el titol deixava d'animar-se 2025-12-04 12:00:08 +01:00
330044e10f millorada la gestio d'escenes i opcions 2025-12-04 11:51:41 +01:00
f8c5207d5c corregida la posicio del titol al inici 2025-12-04 08:52:07 +01:00
2caaa29124 afegit fade in al starfield de TITOL 2025-12-04 08:24:08 +01:00
cdc4d07394 animacio del titol als 10 segons 2025-12-04 08:00:13 +01:00
1023cde1be afegida progresió 2025-12-03 22:19:44 +01:00
59 changed files with 3398 additions and 281 deletions

10
.gitignore vendored
View File

@@ -17,6 +17,16 @@ asteroids
*.exe
*.out
*.app
tools/pack_resources/pack_resources
tools/pack_resources/pack_resources.exe
# Releases
*.zip
*.tar.gz
*.dmg
# Generated resources
resources.pack
# Compiled Object files
*.o

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")
@@ -73,6 +73,11 @@ target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-O2 -ffunctio
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<CONFIG:DEBUG>:_DEBUG>)
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:RELEASE_BUILD>)
# Definir MACOS_BUNDLE si es un bundle de macOS
if(APPLE AND MACOSX_BUNDLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUNDLE)
endif()
# Configuración específica para cada plataforma
if(WIN32)
target_compile_definitions(${PROJECT_NAME} PRIVATE WINDOWS_BUILD)

View File

@@ -58,6 +58,25 @@ else
UNAME_S := $(shell uname -s)
endif
# ==============================================================================
# PACKING TOOL
# ==============================================================================
PACK_TOOL := tools/pack_resources/pack_resources
# ==============================================================================
# DEFAULT GOAL
# ==============================================================================
.DEFAULT_GOAL := all
.PHONY: pack_tool resources.pack
pack_tool:
@$(MAKE) -C tools/pack_resources
resources.pack: pack_tool
@echo "Creating resources.pack..."
@./$(PACK_TOOL) data resources.pack
# ==============================================================================
# TARGETS
# ==============================================================================
@@ -67,8 +86,8 @@ endif
# BUILD TARGETS (delegate to CMake)
# ==============================================================================
# Default target: build with CMake
all: $(TARGET_FILE)
# Default target: build with CMake + resources
all: resources.pack $(TARGET_FILE)
$(TARGET_FILE):
@cmake -B build -DCMAKE_BUILD_TYPE=Release
@@ -76,10 +95,10 @@ $(TARGET_FILE):
@echo "Build successful: $(TARGET_FILE)"
# Debug build
debug:
debug: resources.pack
@cmake -B build -DCMAKE_BUILD_TYPE=Debug
@cmake --build build
@echo "Debug build successful: $(TARGET_FILE)_debug"
@echo "Debug build successful: $(TARGET_FILE)"
# ==============================================================================
# RELEASE PACKAGING TARGETS
@@ -87,7 +106,7 @@ debug:
# macOS Release (Apple Silicon)
.PHONY: macos_release
macos_release:
macos_release: pack_tool resources.pack
@echo "Creating macOS release - Version: $(VERSION)"
# Check/install create-dmg
@@ -104,8 +123,8 @@ macos_release:
@$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
@$(MKDIR) Frameworks
# Copy resources
@cp -r resources "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources/"
# Copy resources.pack to Resources
@cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources/"
@ditto release/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks/SDL3.framework"
@ditto release/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework Frameworks/SDL3.framework
@@ -150,6 +169,8 @@ macos_release:
--icon-size 96 \
--text-size 12 \
--icon "$(APP_NAME).app" 278 102 \
--icon "LICENSE" 441 102 \
--icon "README.md" 604 102 \
--app-drop-link 115 102 \
--hide-extension "$(APP_NAME).app" \
"$(MACOS_ARM_RELEASE)" \
@@ -313,6 +334,8 @@ else
@$(RMFILE) $(TARGET_FILE) $(TARGET_FILE)_debug
@$(RMDIR) build $(RELEASE_FOLDER)
@$(RMFILE) *.dmg *.zip *.tar.gz 2>/dev/null || true
@$(RMFILE) resources.pack 2>/dev/null || true
@$(MAKE) -C tools/pack_resources clean 2>/dev/null || true
endif
@echo "Clean complete"

View File

@@ -6,14 +6,18 @@ name: bullet
scale: 1.0
center: 0, 0
# Pentàgon petit radi=5 (1/4 del enemic)
# Pentàgon regular amb 72° entre punts
# Cercle (octàgon regular radi=3)
# 8 punts equidistants (45° entre ells) per aproximar un cercle
# Començant a angle=-90° (amunt), rotant sentit horari
#
# Conversió polar→cartesià (radi=5, SDL: Y creix cap avall):
# angle=-90°: (0.00, -5.00)
# angle=-18°: (4.76, -1.55)
# angle=54°: (2.94, 4.05)
# angle=126°: (-2.94, 4.05)
# angle=198°: (-4.76, -1.55)
# Conversió polar→cartesià (radi=3, SDL: Y creix cap avall):
# angle=-90°: (0.00, -3.00)
# angle=-45°: (2.12, -2.12)
# angle=0°: (3.00, 0.00)
# angle=45°: (2.12, 2.12)
# angle=90°: (0.00, 3.00)
# angle=135°: (-2.12, 2.12)
# angle=180°: (-3.00, 0.00)
# angle=225°: (-2.12, -2.12)
polyline: 0,-5 4.76,-1.55 2.94,4.05 -2.94,4.05 -4.76,-1.55 0,-5
polyline: 0,-3 2.12,-2.12 3,0 2.12,2.12 0,3 -2.12,2.12 -3,0 -2.12,-2.12 0,-3

24
data/shapes/ship2.shp Normal file
View File

@@ -0,0 +1,24 @@
# ship2.shp - Nau del jugador (triangle amb base còncava - punta de fletxa)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
name: ship2
scale: 1.0
center: 0, 0
# Triangle amb base còncava tipus "punta de fletxa"
# Punts originals (polar):
# p1: r=12, angle=270° (3π/2) → punta amunt
# p2: r=12, angle=45° (π/4) → base dreta-darrere
# p3: r=12, angle=135° (3π/4) → base esquerra-darrere
#
# MODIFICACIÓ: afegit p4 al mig de la base, desplaçat cap al centre
# p4: (0, 4) → punt central de la base, cap endins
#
# Conversió polar→cartesià (angle-90° perquè origen visual és amunt):
# p1: (0, -12) → punta
# p2: (8.49, 8.49) → base dreta
# p4: (0, 4) → base centre (cap endins)
# p3: (-8.49, 8.49) → base esquerra
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

After

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -3,6 +3,8 @@
#include "core/audio/audio_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include <iostream>
// Inicialització de variables estàtiques
@@ -19,17 +21,28 @@ JA_Sound_t* AudioCache::getSound(const std::string& name) {
return it->second;
}
// Cache miss - cargar archivo
std::string fullpath = resolveSoundPath(name);
JA_Sound_t* sound = JA_LoadSound(fullpath.c_str());
// Normalize path: "laser_shoot.wav" → "sounds/laser_shoot.wav"
std::string normalized = name;
if (normalized.find("sounds/") != 0 && normalized.find('/') == std::string::npos) {
normalized = "sounds/" + normalized;
}
// Load from resource system
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
if (data.empty()) {
std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << normalized << std::endl;
return nullptr;
}
// Load sound from memory
JA_Sound_t* sound = JA_LoadSound(data.data(), static_cast<uint32_t>(data.size()));
if (sound == nullptr) {
std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << fullpath
std::cerr << "[AudioCache] Error: no s'ha pogut decodificar " << normalized
<< std::endl;
return nullptr;
}
std::cout << "[AudioCache] Sound loaded: " << name << std::endl;
std::cout << "[AudioCache] Sound loaded: " << normalized << std::endl;
sounds_[name] = sound;
return sound;
}
@@ -42,17 +55,28 @@ JA_Music_t* AudioCache::getMusic(const std::string& name) {
return it->second;
}
// Cache miss - cargar archivo
std::string fullpath = resolveMusicPath(name);
JA_Music_t* music = JA_LoadMusic(fullpath.c_str());
// Normalize path: "title.ogg" → "music/title.ogg"
std::string normalized = name;
if (normalized.find("music/") != 0 && normalized.find('/') == std::string::npos) {
normalized = "music/" + normalized;
}
// Load from resource system
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
if (data.empty()) {
std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << normalized << std::endl;
return nullptr;
}
// Load music from memory
JA_Music_t* music = JA_LoadMusic(data.data(), static_cast<uint32_t>(data.size()));
if (music == nullptr) {
std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << fullpath
std::cerr << "[AudioCache] Error: no s'ha pogut decodificar " << normalized
<< std::endl;
return nullptr;
}
std::cout << "[AudioCache] Music loaded: " << name << std::endl;
std::cout << "[AudioCache] Music loaded: " << normalized << std::endl;
musics_[name] = music;
return music;
}

View File

@@ -70,7 +70,7 @@ constexpr int MAX_IPUNTS = 30;
constexpr float SHIP_RADIUS = 12.0f;
constexpr float ENEMY_RADIUS = 20.0f;
constexpr float BULLET_RADIUS = 5.0f;
constexpr float BULLET_RADIUS = 3.0f;
} // namespace Entities
// Game rules (lives, respawn, game over)
@@ -79,6 +79,13 @@ 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)
// Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0f; // Duración total
constexpr float LEVEL_START_TYPING_RATIO = 0.3f; // 30% escribiendo, 70% mostrando
// Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!")
constexpr float LEVEL_COMPLETED_DURATION = 3.0f; // Duración total
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0f; // 0.0 = sin typewriter (directo)
} // namespace Game
// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp)
@@ -101,6 +108,16 @@ constexpr float ROTACIO_MAX = 0.3f; // Rotació màxima (rad/s ~17.2°/
constexpr float TEMPS_VIDA = 2.0f; // Duració màxima (segons) - enemy/bullet debris
constexpr float TEMPS_VIDA_NAU = 3.0f; // Ship debris lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 0.5f; // Reducció de mida (factor/s)
// Herència de velocitat angular (trayectorias curvas)
constexpr float FACTOR_HERENCIA_MIN = 0.7f; // Mínimo 70% del drotacio heredat
constexpr float FACTOR_HERENCIA_MAX = 1.0f; // Màxim 100% del drotacio heredat
constexpr float FRICCIO_ANGULAR = 0.5f; // Desacceleració angular (rad/s²)
// Angular velocity cap for trajectory inheritance
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
constexpr float VELOCITAT_ROT_MAX = 1.5f; // rad/s (~86°/s)
} // namespace Debris
} // namespace Physics
@@ -138,7 +155,7 @@ namespace Brightness {
// Brillantor estàtica per entitats de joc (0.0-1.0)
constexpr float NAU = 1.0f; // Màxima visibilitat (jugador)
constexpr float ENEMIC = 0.7f; // 30% més tènue (destaca menys)
constexpr float BALA = 0.9f; // Destacada però menys que nau
constexpr float BALA = 1.0f; // Brillo a tope (màxima visibilitat)
// Starfield: gradient segons distància al centre
// distancia_centre: 0.0 (centre) → 1.0 (vora pantalla)
@@ -168,10 +185,11 @@ constexpr int FADE_DURATION_MS = 1000; // Fade out duration
// Efectes de so (sons puntuals)
namespace Sound {
constexpr float VOLUME = 1.0F; // Volumen efectos
constexpr bool ENABLED = true; // Sonidos habilitados
constexpr const char* EXPLOSION = "explosion.wav"; // Explosión
constexpr const char* LASER = "laser_shoot.wav"; // Disparo
constexpr float VOLUME = 1.0F; // Volumen efectos
constexpr bool ENABLED = true; // Sonidos habilitados
constexpr const char* EXPLOSION = "explosion.wav"; // Explosión
constexpr const char* LASER = "laser_shoot.wav"; // Disparo
constexpr const char* GOOD_JOB_COMMANDER = "good_job_commander.wav"; // Voz: "Good job, commander"
} // namespace Sound
// Enemy type configuration (tipus d'enemics)
@@ -181,8 +199,8 @@ namespace Pentagon {
constexpr float VELOCITAT = 35.0f; // px/s (slightly slower)
constexpr float CANVI_ANGLE_PROB = 0.20f; // 20% per wall hit (frequent zigzag)
constexpr float CANVI_ANGLE_MAX = 1.0f; // Max random angle change (rad)
constexpr float DROTACIO_MIN = 0.5f; // Min visual rotation (rad/s)
constexpr float DROTACIO_MAX = 2.5f; // Max visual rotation (rad/s)
constexpr float DROTACIO_MIN = 0.75f; // Min visual rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 3.75f; // Max visual rotation (rad/s) [+50%]
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon
@@ -191,8 +209,8 @@ namespace Quadrat {
constexpr float VELOCITAT = 40.0f; // px/s (medium speed)
constexpr float TRACKING_STRENGTH = 0.5f; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0f; // Seconds between angle updates
constexpr float DROTACIO_MIN = 0.2f; // Slow rotation
constexpr float DROTACIO_MAX = 1.0f;
constexpr float DROTACIO_MIN = 0.3f; // Slow rotation [+50%]
constexpr float DROTACIO_MAX = 1.5f; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Quadrat
@@ -201,8 +219,8 @@ namespace Molinillo {
constexpr float VELOCITAT = 50.0f; // px/s (fastest)
constexpr float CANVI_ANGLE_PROB = 0.05f; // 5% per wall hit (rare direction change)
constexpr float CANVI_ANGLE_MAX = 0.3f; // Small angle adjustments
constexpr float DROTACIO_MIN = 2.0f; // Base rotation (rad/s)
constexpr float DROTACIO_MAX = 4.0f;
constexpr float DROTACIO_MIN = 3.0f; // Base rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 6.0f; // [+50%]
constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0f; // Spin-up multiplier when near ship
constexpr float PROXIMITY_DISTANCE = 100.0f; // Distance threshold (px)
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
@@ -220,11 +238,45 @@ constexpr float PALPITACIO_FREQ_MIN = 1.5f; // Min frequency (Hz)
constexpr float PALPITACIO_FREQ_MAX = 3.0f; // Max frequency (Hz)
// Rotation acceleration
constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.005f; // 0.5% chance per second
constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02f; // 2% chance per second [4x more frequent]
constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0f; // Min transition time
constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0f; // Max transition time
constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.5f; // Min speed multiplier
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 2.5f; // Max speed multiplier
constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3f; // Min speed multiplier [more dramatic]
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0f; // Max speed multiplier [more dramatic]
} // namespace Animation
// Spawn safety and invulnerability system
namespace Spawn {
// Safe spawn distance from player
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0f; // 3x ship radius
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
// Invulnerability system
constexpr float INVULNERABILITY_DURATION = 3.0f; // Seconds
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3f; // Dim
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7f; // Normal (same as Defaults::Brightness::ENEMIC)
constexpr float INVULNERABILITY_SCALE_START = 0.0f; // Invisible
constexpr float INVULNERABILITY_SCALE_END = 1.0f; // Full size
} // namespace Spawn
// Scoring system (puntuació per tipus d'enemic)
namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentàgon (esquivador, 35 px/s)
constexpr int QUADRAT_SCORE = 150; // Quadrat (perseguidor, 40 px/s)
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring
} // namespace Enemies
// Floating score numbers (números flotants de puntuació)
namespace FloatingScore {
constexpr float LIFETIME = 2.0f; // Duració màxima (segons)
constexpr float VELOCITY_Y = -30.0f; // Velocitat vertical (px/s, negatiu = amunt)
constexpr float VELOCITY_X = 0.0f; // Velocitat horizontal (px/s)
constexpr float SCALE = 0.75f; // Escala del text (0.75 = 75% del marcador)
constexpr float SPACING = 0.0f; // Espaiat entre caràcters
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
} // namespace FloatingScore
} // namespace Defaults

View File

@@ -32,6 +32,9 @@ class Shape {
// Carregar forma des de fitxer .shp
bool carregar(const std::string& filepath);
// Parsejar forma des de buffer de memòria (per al sistema de recursos)
bool parsejar_fitxer(const std::string& contingut);
// Getters
const std::vector<ShapePrimitive>& get_primitives() const {
return primitives_;
@@ -50,9 +53,6 @@ class Shape {
float escala_defecte_; // Escala per defecte (normalment 1.0)
std::string nom_; // Nom de la forma (per depuració)
// Parsejador del fitxer
bool parsejar_fitxer(const std::string& contingut);
// Helpers privats per parsejar
std::string trim(const std::string& str) const;
bool starts_with(const std::string& str, const std::string& prefix) const;

View File

@@ -3,6 +3,8 @@
#include "core/graphics/shape_loader.hpp"
#include "core/resources/resource_helper.hpp"
#include <iostream>
namespace Graphics {
@@ -19,28 +21,40 @@ std::shared_ptr<Shape> ShapeLoader::load(const std::string& filename) {
return it->second; // Cache hit
}
// Resolve full path
std::string fullpath = resolve_path(filename);
// Normalize path: "ship.shp" → "shapes/ship.shp"
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
std::string normalized = filename;
if (normalized.find("shapes/") != 0) {
// Doesn't start with "shapes/", so add it
normalized = "shapes/" + normalized;
}
// Create and load shape
// Load from resource system
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
if (data.empty()) {
std::cerr << "[ShapeLoader] Error: no s'ha pogut carregar " << normalized
<< std::endl;
return nullptr;
}
// Convert bytes to string and parse
std::string file_content(data.begin(), data.end());
auto shape = std::make_shared<Shape>();
if (!shape->carregar(fullpath)) {
std::cerr << "[ShapeLoader] Error: no s'ha pogut carregar " << filename
if (!shape->parsejar_fitxer(file_content)) {
std::cerr << "[ShapeLoader] Error: no s'ha pogut parsejar " << normalized
<< std::endl;
return nullptr;
}
// Verify shape is valid
if (!shape->es_valida()) {
std::cerr << "[ShapeLoader] Error: forma invàlida " << filename
<< std::endl;
std::cerr << "[ShapeLoader] Error: forma invàlida " << normalized << std::endl;
return nullptr;
}
// Cache and return
std::cout << "[ShapeLoader] Carregat: " << filename << " ("
<< shape->get_nom() << ", " << shape->get_num_primitives()
<< " primitives)" << std::endl;
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->get_nom()
<< ", " << shape->get_num_primitives() << " primitives)" << std::endl;
cache_[filename] = shape;
return shape;

View File

@@ -8,6 +8,7 @@
#include <iostream>
#include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/rendering/shape_renderer.hpp"
namespace Graphics {
@@ -21,11 +22,11 @@ Starfield::Starfield(SDL_Renderer* renderer,
punt_fuga_(punt_fuga),
area_(area),
densitat_(densitat) {
// Carregar forma d'estrella
shape_estrella_ = std::make_shared<Shape>("data/shapes/star.shp");
// Carregar forma d'estrella amb ShapeLoader
shape_estrella_ = ShapeLoader::load("star.shp");
if (!shape_estrella_->es_valida()) {
std::cerr << "ERROR: No s'ha pogut carregar data/shapes/star.shp" << std::endl;
if (!shape_estrella_ || !shape_estrella_->es_valida()) {
std::cerr << "ERROR: No s'ha pogut carregar star.shp" << std::endl;
return;
}

View File

@@ -181,7 +181,7 @@ bool VectorText::is_supported(char c) const {
return chars_.find(c) != chars_.end();
}
void VectorText::render(const std::string& text, const Punt& posicio, float escala, float spacing) {
void VectorText::render(const std::string& text, const Punt& posicio, float escala, float spacing, float brightness) {
if (!renderer_) {
return;
}
@@ -223,7 +223,7 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca
// Ajustar Y para que posicio represente esquina superior izquierda
// (render_shape espera el centro, así que sumamos la mitad de la altura)
Punt char_pos = {current_x, posicio.y + char_height_scaled / 2.0f};
Rendering::render_shape(renderer_, it->second, char_pos, 0.0f, escala, true);
Rendering::render_shape(renderer_, it->second, char_pos, 0.0f, escala, true, 1.0f, brightness);
// Avanzar posición
current_x += char_width_scaled + spacing_scaled;

View File

@@ -24,7 +24,8 @@ class VectorText {
// - posicio: posición inicial (esquina superior izquierda)
// - escala: factor de escala (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a escala 1.0)
void render(const std::string& text, const Punt& posicio, float escala = 1.0f, float spacing = 2.0f);
// - brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor)
void render(const std::string& text, const Punt& posicio, float escala = 1.0f, float spacing = 2.0f, float brightness = 1.0f);
// Calcular ancho total de un string (útil para centrado)
float get_text_width(const std::string& text, float escala = 1.0f, float spacing = 2.0f) const;

View File

@@ -433,11 +433,13 @@ void SDLManager::updateFPS(float delta_time) {
fps_accumulator_ = 0.0f;
// Actualitzar títol de la finestra
std::string title = std::format("{} v{} ({}) - {} FPS",
std::string vsync_state = (Options::rendering.vsync == 1) ? "ON" : "OFF";
std::string title = std::format("{} v{} ({}) - {} FPS - VSync: {}",
Project::LONG_NAME,
Project::VERSION,
Project::COPYRIGHT,
fps_display_);
fps_display_,
vsync_state);
if (finestra_) {
SDL_SetWindowTitle(finestra_, title.c_str());
@@ -462,6 +464,10 @@ void SDLManager::toggleVSync() {
SDL_SetRenderVSync(renderer_, Options::rendering.vsync);
}
// Reset FPS counter para evitar valores mixtos entre regímenes
fps_accumulator_ = 0.0f;
fps_frame_count_ = 0;
// Guardar configuració
Options::saveToFile();
}

View File

@@ -14,8 +14,7 @@
class SDLManager {
public:
SDLManager(); // Constructor per defecte (usa Defaults::)
SDLManager(int width, int height,
bool fullscreen); // Constructor amb configuració
SDLManager(int width, int height, bool fullscreen); // Constructor amb configuració
~SDLManager();
// No permetre còpia ni assignació
@@ -27,8 +26,7 @@ class SDLManager {
void decreaseWindowSize(); // F1: -100px
void toggleFullscreen(); // F3
void toggleVSync(); // F4
bool
handleWindowEvent(const SDL_Event& event); // Per a SDL_EVENT_WINDOW_RESIZED
bool handleWindowEvent(const SDL_Event& event); // Per a SDL_EVENT_WINDOW_RESIZED
// Funcions principals (renderitzat)
void neteja(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);

View File

@@ -0,0 +1,83 @@
// resource_helper.cpp - Implementació de funcions d'ajuda
// © 2025 Port a C++20 amb SDL3
#include "resource_helper.hpp"
#include "resource_loader.hpp"
#include <algorithm>
#include <iostream>
namespace Resource {
namespace Helper {
// Inicialitzar el sistema de recursos
bool initializeResourceSystem(const std::string& pack_file, bool fallback) {
return Loader::get().initialize(pack_file, fallback);
}
// Carregar un fitxer
std::vector<uint8_t> loadFile(const std::string& filepath) {
// Normalitzar la ruta
std::string normalized = normalizePath(filepath);
// Carregar del sistema de recursos
return Loader::get().loadResource(normalized);
}
// Comprovar si existeix un fitxer
bool fileExists(const std::string& filepath) {
std::string normalized = normalizePath(filepath);
return Loader::get().resourceExists(normalized);
}
// Obtenir ruta normalitzada per al paquet
// Elimina prefixos "data/", rutes absolutes, etc.
std::string getPackPath(const std::string& asset_path) {
std::string path = asset_path;
// Eliminar rutes absolutes (detectar / o C:\ al principi)
if (!path.empty() && path[0] == '/') {
// Buscar "data/" i agafar el que ve després
size_t data_pos = path.find("/data/");
if (data_pos != std::string::npos) {
path = path.substr(data_pos + 6); // Saltar "/data/"
}
}
// Eliminar "./" i "../" del principi
while (path.starts_with("./")) {
path = path.substr(2);
}
while (path.starts_with("../")) {
path = path.substr(3);
}
// Eliminar "data/" del principi
if (path.starts_with("data/")) {
path = path.substr(5);
}
// Eliminar "Resources/" (macOS bundles)
if (path.starts_with("Resources/")) {
path = path.substr(10);
}
// Convertir barres invertides a normals
std::ranges::replace(path, '\\', '/');
return path;
}
// Normalitzar ruta (alias de getPackPath)
std::string normalizePath(const std::string& path) {
return getPackPath(path);
}
// Comprovar si hi ha paquet carregat
bool isPackLoaded() {
return Loader::get().isPackLoaded();
}
} // namespace Helper
} // namespace Resource

View File

@@ -0,0 +1,28 @@
// resource_helper.hpp - Funcions d'ajuda per gestió de recursos
// © 2025 Port a C++20 amb SDL3
// API simplificada i normalització de rutes
#pragma once
#include <string>
#include <vector>
namespace Resource {
namespace Helper {
// Inicialització del sistema
bool initializeResourceSystem(const std::string& pack_file, bool fallback);
// Càrrega de fitxers
std::vector<uint8_t> loadFile(const std::string& filepath);
bool fileExists(const std::string& filepath);
// Normalització de rutes
std::string getPackPath(const std::string& asset_path);
std::string normalizePath(const std::string& path);
// Estat
bool isPackLoaded();
} // namespace Helper
} // namespace Resource

View File

@@ -0,0 +1,143 @@
// resource_loader.cpp - Implementació del carregador de recursos
// © 2025 Port a C++20 amb SDL3
#include "resource_loader.hpp"
#include <filesystem>
#include <fstream>
#include <iostream>
namespace Resource {
// Singleton
Loader& Loader::get() {
static Loader instance;
return instance;
}
// Inicialitzar el sistema de recursos
bool Loader::initialize(const std::string& pack_file, bool enable_fallback) {
fallback_enabled_ = enable_fallback;
// Intentar carregar el paquet
pack_ = std::make_unique<Pack>();
if (!pack_->loadPack(pack_file)) {
if (!fallback_enabled_) {
std::cerr << "[ResourceLoader] ERROR FATAL: No es pot carregar " << pack_file
<< " i el fallback està desactivat\n";
return false;
}
std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de fitxers\n";
pack_.reset(); // No hi ha paquet
return true;
}
std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n";
return true;
}
// Carregar un recurs
std::vector<uint8_t> Loader::loadResource(const std::string& filename) {
// Intentar carregar del paquet primer
if (pack_) {
if (pack_->hasResource(filename)) {
auto data = pack_->getResource(filename);
if (!data.empty()) {
return data;
}
std::cerr << "[ResourceLoader] Advertència: recurs buit al paquet: " << filename
<< "\n";
}
// Si no està al paquet i no hi ha fallback, falla
if (!fallback_enabled_) {
std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: "
<< filename << "\n";
return {};
}
}
// Fallback al sistema de fitxers
if (fallback_enabled_) {
return loadFromFilesystem(filename);
}
return {};
}
// Comprovar si existeix un recurs
bool Loader::resourceExists(const std::string& filename) {
// Comprovar al paquet
if (pack_ && pack_->hasResource(filename)) {
return true;
}
// Comprovar al sistema de fitxers si està activat el fallback
if (fallback_enabled_) {
std::string fullpath = base_path_.empty() ? "data/" + filename : base_path_ + "/data/" + filename;
return std::filesystem::exists(fullpath);
}
return false;
}
// Validar el paquet
bool Loader::validatePack() {
if (!pack_) {
std::cerr << "[ResourceLoader] Advertència: no hi ha paquet carregat per validar\n";
return false;
}
return pack_->validatePack();
}
// Comprovar si hi ha paquet carregat
bool Loader::isPackLoaded() const {
return pack_ != nullptr;
}
// Establir la ruta base
void Loader::setBasePath(const std::string& path) {
base_path_ = path;
std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
}
// Obtenir la ruta base
std::string Loader::getBasePath() const {
return base_path_;
}
// Carregar des del sistema de fitxers (fallback)
std::vector<uint8_t> Loader::loadFromFilesystem(const std::string& filename) {
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
// We need to prepend base_path + "data/"
std::string fullpath;
if (base_path_.empty()) {
fullpath = "data/" + filename;
} else {
fullpath = base_path_ + "/data/" + filename;
}
std::ifstream file(fullpath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ResourceLoader] Error: no es pot obrir " << fullpath << "\n";
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "[ResourceLoader] Error: no es pot llegir " << fullpath << "\n";
return {};
}
std::cout << "[ResourceLoader] Carregat des del sistema de fitxers: " << fullpath << "\n";
return data;
}
} // namespace Resource

View File

@@ -0,0 +1,53 @@
// resource_loader.hpp - Carregador de recursos (Singleton)
// © 2025 Port a C++20 amb SDL3
// Coordina càrrega des del paquet i/o sistema de fitxers
#pragma once
#include "resource_pack.hpp"
#include <memory>
#include <string>
#include <vector>
namespace Resource {
// Singleton per gestionar la càrrega de recursos
class Loader {
public:
// Singleton
static Loader& get();
// Inicialització
bool initialize(const std::string& pack_file, bool enable_fallback);
// Càrrega de recursos
std::vector<uint8_t> loadResource(const std::string& filename);
bool resourceExists(const std::string& filename);
// Validació
bool validatePack();
bool isPackLoaded() const;
// Estat
void setBasePath(const std::string& path);
std::string getBasePath() const;
private:
Loader() = default;
~Loader() = default;
// No es pot copiar ni moure
Loader(const Loader&) = delete;
Loader& operator=(const Loader&) = delete;
// Dades
std::unique_ptr<Pack> pack_;
bool fallback_enabled_ = false;
std::string base_path_;
// Funcions auxiliars
std::vector<uint8_t> loadFromFilesystem(const std::string& filename);
};
} // namespace Resource

View File

@@ -0,0 +1,309 @@
// resource_pack.cpp - Implementació del sistema d'empaquetament
// © 2025 Port a C++20 amb SDL3
#include "resource_pack.hpp"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
namespace Resource {
// Calcular checksum CRC32 simplificat
uint32_t Pack::calculateChecksum(const std::vector<uint8_t>& data) const {
uint32_t checksum = 0x12345678;
for (unsigned char byte : data) {
checksum = ((checksum << 5) + checksum) + byte;
}
return checksum;
}
// Encriptació XOR (simètrica)
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
return;
}
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= key[i % key.length()];
}
}
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
// XOR és simètric
encryptData(data, key);
}
// Llegir fitxer complet a memòria
std::vector<uint8_t> Pack::readFile(const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
return {};
}
return data;
}
// Afegir un fitxer individual al paquet
bool Pack::addFile(const std::string& filepath, const std::string& pack_name) {
auto file_data = readFile(filepath);
if (file_data.empty()) {
return false;
}
ResourceEntry entry{
.filename = pack_name,
.offset = data_.size(),
.size = file_data.size(),
.checksum = calculateChecksum(file_data)};
// Afegir dades al bloc de dades
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry;
std::cout << "[ResourcePack] Afegit: " << pack_name << " (" << file_data.size()
<< " bytes)\n";
return true;
}
// Afegir tots els fitxers d'un directori recursivament
bool Pack::addDirectory(const std::string& dir_path,
const std::string& base_path) {
namespace fs = std::filesystem;
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
return false;
}
std::string current_base = base_path.empty() ? "" : base_path + "/";
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
if (!entry.is_regular_file()) {
continue;
}
std::string full_path = entry.path().string();
std::string relative_path = entry.path().lexically_relative(dir_path).string();
// Convertir barres invertides a normals (Windows)
std::ranges::replace(relative_path, '\\', '/');
// Saltar fitxers de desenvolupament
if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != std::string::npos ||
relative_path.find(".git") != std::string::npos) {
std::cout << "[ResourcePack] Saltant: " << relative_path << '\n';
continue;
}
std::string pack_name = current_base + relative_path;
addFile(full_path, pack_name);
}
return true;
}
// Guardar paquet a disc
bool Pack::savePack(const std::string& pack_file) {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
return false;
}
// Escriure capçalera
file.write(MAGIC_HEADER, 4);
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
// Escriure nombre de recursos
auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
// Escriure metadades de recursos
for (const auto& [name, entry] : resources_) {
// Nom del fitxer
auto name_len = static_cast<uint32_t>(entry.filename.length());
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
file.write(entry.filename.c_str(), name_len);
// Offset, mida, checksum
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
}
// Encriptar dades
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
// Escriure mida de dades i dades encriptades
auto data_size = static_cast<uint64_t>(encrypted_data.size());
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size()
<< " recursos, " << data_size << " bytes)\n";
return true;
}
// Carregar paquet des de disc
bool Pack::loadPack(const std::string& pack_file) {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
return false;
}
// Llegir capçalera
char magic[4];
file.read(magic, 4);
if (std::string(magic, 4) != MAGIC_HEADER) {
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER
<< ")\n";
return false;
}
uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) {
std::cerr << "[ResourcePack] Error: versió incompatible (esperava " << VERSION
<< ", trobat " << version << ")\n";
return false;
}
// Llegir nombre de recursos
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
// Llegir metadades de recursos
resources_.clear();
for (uint32_t i = 0; i < resource_count; ++i) {
// Nom del fitxer
uint32_t name_len;
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
std::string filename(name_len, '\0');
file.read(&filename[0], name_len);
// Offset, mida, checksum
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
// Llegir dades encriptades
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
// Desencriptar
decryptData(data_, DEFAULT_ENCRYPT_KEY);
std::cout << "[ResourcePack] Carregat: " << pack_file << " (" << resources_.size()
<< " recursos)\n";
return true;
}
// Obtenir un recurs del paquet
std::vector<uint8_t> Pack::getResource(const std::string& filename) {
auto it = resources_.find(filename);
if (it == resources_.end()) {
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
return {};
}
const auto& entry = it->second;
// Extreure dades
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
return {};
}
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
// Verificar checksum
uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
<< ")\n";
// No falla, però adverteix
}
return resource_data;
}
// Comprovar si existeix un recurs
bool Pack::hasResource(const std::string& filename) const {
return resources_.find(filename) != resources_.end();
}
// Obtenir llista de tots els recursos
std::vector<std::string> Pack::getResourceList() const {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [name, entry] : resources_) {
list.push_back(name);
}
std::ranges::sort(list);
return list;
}
// Validar integritat del paquet
bool Pack::validatePack() const {
bool valid = true;
for (const auto& [name, entry] : resources_) {
// Verificar offset i mida
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té offset/mida invàlid\n";
valid = false;
continue;
}
// Extreure i verificar checksum
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té checksum invàlid\n";
valid = false;
}
}
if (valid) {
std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n";
}
return valid;
}
} // namespace Resource

View File

@@ -0,0 +1,67 @@
// resource_pack.hpp - Sistema d'empaquetament de recursos
// © 2025 Port a C++20 amb SDL3
// Basat en el sistema de "pollo" amb adaptacions per Orni Attack
#pragma once
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace Resource {
// Capçalera del fitxer de paquet
struct PackHeader {
char magic[4]; // "ORNI"
uint32_t version; // Versió del format (1)
};
// Entrada de recurs dins el paquet
struct ResourceEntry {
std::string filename; // Nom del recurs (amb barres normals)
uint64_t offset; // Posició dins el bloc de dades
uint64_t size; // Mida en bytes
uint32_t checksum; // Checksum CRC32 per verificació
};
// Classe principal per gestionar paquets de recursos
class Pack {
public:
Pack() = default;
~Pack() = default;
// Afegir fitxers al paquet
bool addFile(const std::string& filepath, const std::string& pack_name);
bool addDirectory(const std::string& dir_path, const std::string& base_path = "");
// Guardar i carregar paquets
bool savePack(const std::string& pack_file);
bool loadPack(const std::string& pack_file);
// Accés a recursos
std::vector<uint8_t> getResource(const std::string& filename);
bool hasResource(const std::string& filename) const;
std::vector<std::string> getResourceList() const;
// Validació
bool validatePack() const;
private:
// Constants
static constexpr const char* MAGIC_HEADER = "ORNI";
static constexpr uint32_t VERSION = 1;
static constexpr const char* DEFAULT_ENCRYPT_KEY = "ORNI_RESOURCES_2025";
// Dades del paquet
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
// Funcions auxiliars
std::vector<uint8_t> readFile(const std::string& filepath);
uint32_t calculateChecksum(const std::vector<uint8_t>& data) const;
void encryptData(std::vector<uint8_t>& data, const std::string& key);
void decryptData(std::vector<uint8_t>& data, const std::string& key);
};
} // namespace Resource

View File

@@ -0,0 +1,70 @@
// context_escenes.hpp - Sistema de gestió d'escenes i context de transicions
// © 2025 Port a C++20
#pragma once
namespace GestorEscenes {
// Context de transició entre escenes
// Conté l'escena destinació i opcions específiques per aquella escena
class ContextEscenes {
public:
// Tipus d'escena del joc
enum class Escena {
LOGO, // Pantalla d'inici (logo JAILGAMES)
TITOL, // Pantalla de títol amb menú
JOC, // Joc principal (Asteroids)
EIXIR // Sortir del programa
};
// Opcions específiques per a cada escena
enum class Opcio {
NONE, // Sense opcions especials (comportament per defecte)
JUMP_TO_TITLE_MAIN, // TITOL: Saltar directament a MAIN (starfield instantani)
// MODE_DEMO, // JOC: Mode demostració amb IA (futur)
};
// Constructor inicial amb escena LOGO i sense opcions
ContextEscenes()
: escena_desti_(Escena::LOGO),
opcio_(Opcio::NONE) {}
// Canviar escena amb opció específica
void canviar_escena(Escena nova_escena, Opcio opcio = Opcio::NONE) {
escena_desti_ = nova_escena;
opcio_ = opcio;
}
// Consultar escena destinació
[[nodiscard]] auto escena_desti() const -> Escena {
return escena_desti_;
}
// Consultar opció actual
[[nodiscard]] auto opcio() const -> Opcio {
return opcio_;
}
// Consumir opció (retorna valor i reseteja a NONE)
// Utilitzar quan l'escena processa l'opció
[[nodiscard]] auto consumir_opcio() -> Opcio {
Opcio valor = opcio_;
opcio_ = Opcio::NONE;
return valor;
}
// Reset opció a NONE (sense retornar valor)
void reset_opcio() {
opcio_ = Opcio::NONE;
}
private:
Escena escena_desti_; // Escena a la qual transicionar
Opcio opcio_; // Opció específica per l'escena
};
// Variable global inline per gestionar l'escena actual (backward compatibility)
// Sincronitzada amb context.escena_desti() pel Director
inline ContextEscenes::Escena actual = ContextEscenes::Escena::LOGO;
} // namespace GestorEscenes

View File

@@ -11,11 +11,14 @@
#include "core/audio/audio_cache.hpp"
#include "core/defaults.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_loader.hpp"
#include "core/utils/path_utils.hpp"
#include "game/escenes/escena_joc.hpp"
#include "game/escenes/escena_logo.hpp"
#include "game/escenes/escena_titol.hpp"
#include "game/options.hpp"
#include "gestor_escenes.hpp"
#include "context_escenes.hpp"
#include "project.h"
#ifndef _WIN32
@@ -23,6 +26,10 @@
#include <unistd.h>
#endif
// Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes;
using Escena = ContextEscenes::Escena;
// Constructor
Director::Director(std::vector<std::string> const& args) {
std::cout << "Orni Attack - Inici\n";
@@ -33,6 +40,44 @@ Director::Director(std::vector<std::string> const& args) {
// Comprovar arguments del programa
executable_path_ = checkProgramArguments(args);
// Inicialitzar sistema de rutes
Utils::initializePathSystem(args[0].c_str());
// Obtenir ruta base dels recursos
std::string resource_base = Utils::getResourceBasePath();
// Inicialitzar sistema de recursos
#ifdef RELEASE_BUILD
// Mode release: paquet obligatori, sense fallback
std::string pack_path = resource_base + "/resources.pack";
if (!Resource::Helper::initializeResourceSystem(pack_path, false)) {
std::cerr << "ERROR FATAL: No es pot carregar " << pack_path << "\n";
std::cerr << "El joc no pot continuar sense els recursos.\n";
std::exit(1);
}
// Validar integritat del paquet
if (!Resource::Loader::get().validatePack()) {
std::cerr << "ERROR FATAL: El paquet de recursos està corromput\n";
std::exit(1);
}
std::cout << "Sistema de recursos inicialitzat (mode release)\n";
#else
// Mode desenvolupament: intentar paquet amb fallback a data/
std::string pack_path = resource_base + "/resources.pack";
Resource::Helper::initializeResourceSystem(pack_path, true);
if (Resource::Helper::isPackLoaded()) {
std::cout << "Sistema de recursos inicialitzat (mode dev amb paquet)\n";
} else {
std::cout << "Sistema de recursos inicialitzat (mode dev, fallback a data/)\n";
}
// Establir ruta base per al fallback
Resource::Loader::get().setBasePath(resource_base);
#endif
// Crear carpetes del sistema
createSystemFolder("jailgames");
createSystemFolder(std::string("jailgames/") + Project::NAME);
@@ -161,6 +206,8 @@ auto Director::run() -> int {
// Inicialitzar sistema d'audio
Audio::init();
Audio::get()->setMusicVolume(1.0);
Audio::get()->setSoundVolume(0.4);
// Precachejar música per evitar lag al començar
AudioCache::getMusic("title.ogg");
@@ -169,23 +216,35 @@ auto Director::run() -> int {
<< AudioCache::getMusicCacheSize() << " fitxers\n";
}
// Crear context d'escenes
ContextEscenes context;
#ifdef _DEBUG
context.canviar_escena(Escena::JOC);
#else
context.canviar_escena(Escena::LOGO);
#endif
// Bucle principal de gestió d'escenes
while (GestorEscenes::actual != GestorEscenes::Escena::EIXIR) {
switch (GestorEscenes::actual) {
case GestorEscenes::Escena::LOGO: {
EscenaLogo logo(sdl);
while (context.escena_desti() != Escena::EIXIR) {
// Sincronitzar GestorEscenes::actual amb context
// (altres sistemes encara poden llegir GestorEscenes::actual)
GestorEscenes::actual = context.escena_desti();
switch (context.escena_desti()) {
case Escena::LOGO: {
EscenaLogo logo(sdl, context);
logo.executar();
break;
}
case GestorEscenes::Escena::TITOL: {
EscenaTitol titol(sdl);
case Escena::TITOL: {
EscenaTitol titol(sdl, context);
titol.executar();
break;
}
case GestorEscenes::Escena::JOC: {
EscenaJoc joc(sdl);
case Escena::JOC: {
EscenaJoc joc(sdl, context);
joc.executar();
break;
}
@@ -195,5 +254,8 @@ auto Director::run() -> int {
}
}
// Sincronitzar final amb GestorEscenes::actual
GestorEscenes::actual = Escena::EIXIR;
return 0;
}

View File

@@ -1,17 +0,0 @@
// gestor_escenes.hpp - Sistema de gestió d'escenes
// Basat en el patró del projecte "pollo"
// © 2025 Port a C++20
#pragma once
namespace GestorEscenes {
enum class Escena {
LOGO, // Pantalla d'inici (2 segons negre)
TITOL, // Pantalla de títol amb menú
JOC, // Joc principal
EIXIR // Sortir del programa
};
// Variable global inline per gestionar l'escena actual
inline Escena actual = Escena::LOGO;
} // namespace GestorEscenes

View File

@@ -5,11 +5,15 @@
#include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "gestor_escenes.hpp"
#include "context_escenes.hpp"
// Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes;
using Escena = ContextEscenes::Escena;
namespace GlobalEvents {
bool handle(const SDL_Event& event, SDLManager& sdl) {
bool handle(const SDL_Event& event, SDLManager& sdl, ContextEscenes& context) {
// Tecles globals de finestra (F1/F2/F3)
if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.key) {
@@ -26,7 +30,8 @@ bool handle(const SDL_Event& event, SDLManager& sdl) {
sdl.toggleVSync();
return true;
case SDLK_ESCAPE:
GestorEscenes::actual = GestorEscenes::Escena::EIXIR;
context.canviar_escena(Escena::EIXIR);
GestorEscenes::actual = Escena::EIXIR;
return true;
default:
break;
@@ -35,7 +40,8 @@ bool handle(const SDL_Event& event, SDLManager& sdl) {
// Tancar finestra
if (event.type == SDL_EVENT_QUIT) {
GestorEscenes::actual = GestorEscenes::Escena::EIXIR;
context.canviar_escena(Escena::EIXIR);
GestorEscenes::actual = Escena::EIXIR;
return true;
}

View File

@@ -6,11 +6,12 @@
#include <SDL3/SDL.h>
// Forward declaration
// Forward declarations
class SDLManager;
namespace GestorEscenes { class ContextEscenes; }
namespace GlobalEvents {
// Processa events globals (F1/F2/F3/ESC/QUIT)
// Retorna true si l'event ha estat processat i no cal seguir processant-lo
bool handle(const SDL_Event& event, SDLManager& sdl);
bool handle(const SDL_Event& event, SDLManager& sdl, GestorEscenes::ContextEscenes& context);
} // namespace GlobalEvents

View File

@@ -0,0 +1,92 @@
// path_utils.cpp - Implementació de utilitats de rutes
// © 2025 Port a C++20 amb SDL3
#include "path_utils.hpp"
#include <algorithm>
#include <filesystem>
#include <iostream>
namespace Utils {
// Variables globals per guardar argv[0]
static std::string executable_path_;
static std::string executable_directory_;
// Inicialitzar el sistema de rutes amb argv[0]
void initializePathSystem(const char* argv0) {
if (!argv0) {
std::cerr << "[PathUtils] ADVERTÈNCIA: argv[0] és nullptr\n";
executable_path_ = "";
executable_directory_ = ".";
return;
}
executable_path_ = argv0;
// Extreure el directori
std::filesystem::path path(argv0);
executable_directory_ = path.parent_path().string();
if (executable_directory_.empty()) {
executable_directory_ = ".";
}
std::cout << "[PathUtils] Executable: " << executable_path_ << "\n";
std::cout << "[PathUtils] Directori: " << executable_directory_ << "\n";
}
// Obtenir el directori de l'executable
std::string getExecutableDirectory() {
if (executable_directory_.empty()) {
std::cerr << "[PathUtils] ADVERTÈNCIA: Sistema de rutes no inicialitzat\n";
return ".";
}
return executable_directory_;
}
// Detectar si estem dins un bundle de macOS
bool isMacOSBundle() {
#ifdef MACOS_BUNDLE
return true;
#else
// Detecció en temps d'execució
// Cercar ".app/Contents/MacOS" a la ruta de l'executable
std::string exe_dir = getExecutableDirectory();
return exe_dir.find(".app/Contents/MacOS") != std::string::npos;
#endif
}
// Obtenir la ruta base dels recursos
std::string getResourceBasePath() {
std::string exe_dir = getExecutableDirectory();
if (isMacOSBundle()) {
// Bundle de macOS: recursos a ../Resources des de MacOS/
std::cout << "[PathUtils] Detectat bundle de macOS\n";
return exe_dir + "/../Resources";
} else {
// Executable normal: recursos al mateix directori
return exe_dir;
}
}
// Normalitzar ruta (convertir barres, etc.)
std::string normalizePath(const std::string& path) {
std::string normalized = path;
// Convertir barres invertides a normals
std::ranges::replace(normalized, '\\', '/');
// Simplificar rutes amb filesystem
try {
std::filesystem::path fs_path(normalized);
normalized = fs_path.lexically_normal().string();
} catch (const std::exception& e) {
std::cerr << "[PathUtils] Error normalitzant ruta: " << e.what() << "\n";
}
return normalized;
}
} // namespace Utils

View File

@@ -0,0 +1,24 @@
// path_utils.hpp - Utilitats de gestió de rutes
// © 2025 Port a C++20 amb SDL3
// Detecció de directoris i bundles multiplataforma
#pragma once
#include <string>
namespace Utils {
// Inicialització amb argv[0]
void initializePathSystem(const char* argv0);
// Obtenció de rutes
std::string getExecutableDirectory();
std::string getResourceBasePath();
// Detecció de plataforma
bool isMacOSBundle();
// Normalització
std::string normalizePath(const std::string& path);
} // namespace Utils

View File

@@ -18,8 +18,9 @@ struct Debris {
float acceleracio; // Acceleració negativa (fricció) en px/s²
// Rotació
float angle_rotacio; // Angle de rotació acumulat (radians)
float velocitat_rot; // Velocitat de rotació en rad/s
float angle_rotacio; // Angle de rotació acumulat (radians)
float velocitat_rot; // Velocitat de rotació de TRAYECTORIA (rad/s)
float velocitat_rot_visual; // Velocitat de rotació VISUAL del segment (rad/s)
// Estat de vida
float temps_vida; // Temps transcorregut (segons)
@@ -28,6 +29,9 @@ struct Debris {
// Shrinking (reducció de distància entre punts)
float factor_shrink; // Factor de reducció per segon (0.0-1.0)
// Rendering
float brightness; // Factor de brillantor (0.0-1.0, heretat de l'objecte original)
};
} // namespace Effects

View File

@@ -47,7 +47,11 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
const Punt& centre,
float angle,
float escala,
float velocitat_base) {
float velocitat_base,
float brightness,
const Punt& velocitat_objecte,
float velocitat_angular,
float factor_herencia_visual) {
if (!shape || !shape->es_valida()) {
return;
}
@@ -94,29 +98,92 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
debris->p1 = world_p1;
debris->p2 = world_p2;
// 4. Calcular direcció perpendicular
Punt direccio = calcular_direccio_perpendicular(world_p1, world_p2);
// 4. Calcular direcció d'explosió (radial, des del centre cap a fora)
Punt direccio = calcular_direccio_explosio(world_p1, world_p2, centre);
// 5. Velocitat inicial (base ± variació aleatòria)
// 5. Velocitat inicial (base ± variació aleatòria + velocitat heretada)
float speed =
velocitat_base +
((std::rand() / static_cast<float>(RAND_MAX)) * 2.0f - 1.0f) *
Defaults::Physics::Debris::VARIACIO_VELOCITAT;
debris->velocitat.x = direccio.x * speed;
debris->velocitat.y = direccio.y * speed;
// Heredar velocitat de l'objecte original (suma vectorial)
debris->velocitat.x = direccio.x * speed + velocitat_objecte.x;
debris->velocitat.y = direccio.y * speed + velocitat_objecte.y;
debris->acceleracio = Defaults::Physics::Debris::ACCELERACIO;
// 6. Rotació lenta aleatòria
debris->velocitat_rot =
Defaults::Physics::Debris::ROTACIO_MIN +
(std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::ROTACIO_MAX -
Defaults::Physics::Debris::ROTACIO_MIN);
// 6. Herència de velocitat angular amb cap + conversió d'excés
// 50% probabilitat de rotació en sentit contrari
if (std::rand() % 2 == 0) {
debris->velocitat_rot = -debris->velocitat_rot;
// 6a. Rotació de TRAYECTORIA amb cap + conversió tangencial
if (std::abs(velocitat_angular) > 0.01f) {
// FASE 1: Aplicar herència i variació (igual que abans)
float factor_herencia =
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
(std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX -
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN);
float velocitat_ang_heretada = velocitat_angular * factor_herencia;
float variacio =
(std::rand() / static_cast<float>(RAND_MAX)) * 0.2f - 0.1f;
velocitat_ang_heretada *= (1.0f + variacio);
// FASE 2: Aplicar cap i calcular excés
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
float abs_ang = std::abs(velocitat_ang_heretada);
float sign_ang = (velocitat_ang_heretada >= 0.0f) ? 1.0f : -1.0f;
if (abs_ang > CAP) {
// Excés: convertir a velocitat tangencial
float excess = abs_ang - CAP;
// Radi de la forma (enemics = 20 px)
float radius = 20.0f;
// Velocitat tangencial = ω_excés × radi
float v_tangential = excess * radius;
// Direcció tangencial: perpendicular a la radial (90° CCW)
// Si direccio = (dx, dy), tangent = (-dy, dx)
float tangent_x = -direccio.y;
float tangent_y = direccio.x;
// Afegir velocitat tangencial (suma vectorial)
debris->velocitat.x += tangent_x * v_tangential;
debris->velocitat.y += tangent_y * v_tangential;
// Aplicar cap a velocitat angular (preservar signe)
debris->velocitat_rot = sign_ang * CAP;
} else {
// Per sota del cap: comportament normal
debris->velocitat_rot = velocitat_ang_heretada;
}
} else {
debris->velocitat_rot = 0.0f; // Nave: sin curvas
}
// 6b. Rotació VISUAL (proporcional según factor_herencia_visual)
if (factor_herencia_visual > 0.01f && std::abs(velocitat_angular) > 0.01f) {
// Heredar rotación visual con factor proporcional
debris->velocitat_rot_visual = debris->velocitat_rot * factor_herencia_visual;
// Variació aleatòria petita (±5%) per naturalitat
float variacio_visual =
(std::rand() / static_cast<float>(RAND_MAX)) * 0.1f - 0.05f;
debris->velocitat_rot_visual *= (1.0f + variacio_visual);
} else {
// Rotació visual aleatòria (factor = 0.0 o sin velocidad angular)
debris->velocitat_rot_visual =
Defaults::Physics::Debris::ROTACIO_MIN +
(std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::ROTACIO_MAX -
Defaults::Physics::Debris::ROTACIO_MIN);
// 50% probabilitat de rotació en sentit contrari
if (std::rand() % 2 == 0) {
debris->velocitat_rot_visual = -debris->velocitat_rot_visual;
}
}
debris->angle_rotacio = 0.0f;
@@ -126,7 +193,10 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
debris->temps_max = Defaults::Physics::Debris::TEMPS_VIDA;
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
// 8. Activar
// 8. Heredar brightness
debris->brightness = brightness;
// 9. Activar
debris->actiu = true;
}
}
@@ -169,6 +239,35 @@ void DebrisManager::actualitzar(float delta_time) {
debris.velocitat.y = 0.0f;
}
// 2b. Rotar vector de velocitat (trayectoria curva)
if (std::abs(debris.velocitat_rot) > 0.01f) {
// Calcular angle de rotació aquest frame
float dangle = debris.velocitat_rot * delta_time;
// Rotar vector de velocitat usant matriu de rotació 2D
float vel_x_old = debris.velocitat.x;
float vel_y_old = debris.velocitat.y;
float cos_a = std::cos(dangle);
float sin_a = std::sin(dangle);
debris.velocitat.x = vel_x_old * cos_a - vel_y_old * sin_a;
debris.velocitat.y = vel_x_old * sin_a + vel_y_old * cos_a;
}
// 2c. Aplicar fricció angular (desacceleració gradual)
if (std::abs(debris.velocitat_rot) > 0.01f) {
float sign = (debris.velocitat_rot > 0) ? 1.0f : -1.0f;
float reduccion =
Defaults::Physics::Debris::FRICCIO_ANGULAR * delta_time;
debris.velocitat_rot -= sign * reduccion;
// Evitar canvi de signe (no pot passar de CW a CCW)
if ((debris.velocitat_rot > 0) != (sign > 0)) {
debris.velocitat_rot = 0.0f;
}
}
// 3. Calcular centre del segment
Punt centre = {(debris.p1.x + debris.p2.x) / 2.0f,
(debris.p1.y + debris.p2.y) / 2.0f};
@@ -177,8 +276,8 @@ void DebrisManager::actualitzar(float delta_time) {
centre.x += debris.velocitat.x * delta_time;
centre.y += debris.velocitat.y * delta_time;
// 5. Actualitzar rotació
debris.angle_rotacio += debris.velocitat_rot * delta_time;
// 5. Actualitzar rotació VISUAL
debris.angle_rotacio += debris.velocitat_rot_visual * delta_time;
// 6. Aplicar shrinking (reducció de distància entre punts)
float shrink_factor =
@@ -206,8 +305,14 @@ void DebrisManager::dibuixar() const {
if (!debris.actiu)
continue;
// Dibuixar segment de línia
Rendering::linea(renderer_, static_cast<int>(debris.p1.x), static_cast<int>(debris.p1.y), static_cast<int>(debris.p2.x), static_cast<int>(debris.p2.y), true);
// Dibuixar segment de línia amb brightness heretat
Rendering::linea(renderer_,
static_cast<int>(debris.p1.x),
static_cast<int>(debris.p1.y),
static_cast<int>(debris.p2.x),
static_cast<int>(debris.p2.y),
true,
debris.brightness);
}
}
@@ -220,16 +325,22 @@ Debris* DebrisManager::trobar_slot_lliure() {
return nullptr; // Pool ple
}
Punt DebrisManager::calcular_direccio_perpendicular(const Punt& p1,
const Punt& p2) const {
// 1. Calcular vector de la línia (p1 → p2)
float dx = p2.x - p1.x;
float dy = p2.y - p1.y;
Punt DebrisManager::calcular_direccio_explosio(const Punt& p1,
const Punt& p2,
const Punt& centre_objecte) const {
// 1. Calcular centre del segment
float centro_seg_x = (p1.x + p2.x) / 2.0f;
float centro_seg_y = (p1.y + p2.y) / 2.0f;
// 2. Normalitzar (obtenir vector unitari)
// 2. Calcular vector des del centre de l'objecte cap al centre del segment
// Això garanteix que la direcció sempre apunte cap a fora (direcció radial)
float dx = centro_seg_x - centre_objecte.x;
float dy = centro_seg_y - centre_objecte.y;
// 3. Normalitzar (obtenir vector unitari)
float length = std::sqrt(dx * dx + dy * dy);
if (length < 0.001f) {
// Línia degenerada, retornar direcció aleatòria
// Segment al centre (cas extrem molt improbable), retornar direcció aleatòria
float angle_rand =
(std::rand() / static_cast<float>(RAND_MAX)) * 2.0f * Defaults::Math::PI;
return {std::cos(angle_rand), std::sin(angle_rand)};
@@ -238,26 +349,15 @@ Punt DebrisManager::calcular_direccio_perpendicular(const Punt& p1,
dx /= length;
dy /= length;
// 3. Rotar 90° (perpendicular)
// Rotació 90° sentit antihorari: (x,y) → (-y, x)
float perp_x = -dy;
float perp_y = dx;
// 4. Afegir variació aleatòria petita (±15°)
// 4. Afegir variació aleatòria petita (±15°) per varietat visual
float angle_variacio =
((std::rand() % 30) - 15) * Defaults::Math::PI / 180.0f;
float cos_v = std::cos(angle_variacio);
float sin_v = std::sin(angle_variacio);
float final_x = perp_x * cos_v - perp_y * sin_v;
float final_y = perp_x * sin_v + perp_y * cos_v;
// 5. Afegir ± direcció aleatòria (50% probabilitat d'invertir)
if (std::rand() % 2 == 0) {
final_x = -final_x;
final_y = -final_y;
}
float final_x = dx * cos_v - dy * sin_v;
float final_y = dx * sin_v + dy * cos_v;
return {final_x, final_y};
}

View File

@@ -25,11 +25,19 @@ class DebrisManager {
// - angle: orientació de l'objecte (radians)
// - escala: escala de l'objecte (1.0 = normal)
// - velocitat_base: velocitat inicial dels fragments (px/s)
// - brightness: factor de brillantor heretat (0.0-1.0, per defecte 1.0)
// - velocitat_objecte: velocitat de l'objecte que explota (px/s, per defecte 0)
// - velocitat_angular: velocitat angular heretada (rad/s, per defecte 0)
// - factor_herencia_visual: factor de herència rotació visual (0.0-1.0, per defecte 0.0)
void explotar(const std::shared_ptr<Graphics::Shape>& shape,
const Punt& centre,
float angle,
float escala,
float velocitat_base);
float velocitat_base,
float brightness = 1.0f,
const Punt& velocitat_objecte = {0.0f, 0.0f},
float velocitat_angular = 0.0f,
float factor_herencia_visual = 0.0f);
// Actualitzar tots els fragments actius
void actualitzar(float delta_time);
@@ -56,8 +64,8 @@ class DebrisManager {
// Trobar primer slot inactiu
Debris* trobar_slot_lliure();
// Calcular direcció perpendicular a un segment
Punt calcular_direccio_perpendicular(const Punt& p1, const Punt& p2) const;
// Calcular direcció d'explosió (radial, des del centre cap al segment)
Punt calcular_direccio_explosio(const Punt& p1, const Punt& p2, const Punt& centre_objecte) const;
};
} // namespace Effects

View File

@@ -0,0 +1,99 @@
// gestor_puntuacio_flotant.cpp - Implementació del gestor de números flotants
// © 2025 Port a C++20 amb SDL3
#include "gestor_puntuacio_flotant.hpp"
#include <string>
namespace Effects {
GestorPuntuacioFlotant::GestorPuntuacioFlotant(SDL_Renderer* renderer)
: renderer_(renderer), text_(renderer) {
// Inicialitzar tots els slots com inactius
for (auto& pf : pool_) {
pf.actiu = false;
}
}
void GestorPuntuacioFlotant::crear(int punts, const Punt& posicio) {
// 1. Trobar slot lliure
PuntuacioFlotant* pf = trobar_slot_lliure();
if (!pf)
return; // Pool ple (improbable)
// 2. Inicialitzar puntuació flotant
pf->text = std::to_string(punts);
pf->posicio = posicio;
pf->velocitat = {Defaults::FloatingScore::VELOCITY_X,
Defaults::FloatingScore::VELOCITY_Y};
pf->temps_vida = 0.0f;
pf->temps_max = Defaults::FloatingScore::LIFETIME;
pf->brightness = 1.0f;
pf->actiu = true;
}
void GestorPuntuacioFlotant::actualitzar(float delta_time) {
for (auto& pf : pool_) {
if (!pf.actiu)
continue;
// 1. Actualitzar posició (deriva cap amunt)
pf.posicio.x += pf.velocitat.x * delta_time;
pf.posicio.y += pf.velocitat.y * delta_time;
// 2. Actualitzar temps de vida
pf.temps_vida += delta_time;
// 3. Calcular brightness (fade lineal)
float progress = pf.temps_vida / pf.temps_max; // 0.0 → 1.0
pf.brightness = 1.0f - progress; // 1.0 → 0.0
// 4. Desactivar quan acaba el temps
if (pf.temps_vida >= pf.temps_max) {
pf.actiu = false;
}
}
}
void GestorPuntuacioFlotant::dibuixar() {
for (const auto& pf : pool_) {
if (!pf.actiu)
continue;
// 1. Calcular dimensions del text per centrar-lo
constexpr float escala = Defaults::FloatingScore::SCALE;
constexpr float spacing = Defaults::FloatingScore::SPACING;
float text_width = text_.get_text_width(pf.text, escala, spacing);
// 2. Centrar text sobre la posició
Punt render_pos = {pf.posicio.x - text_width / 2.0f, pf.posicio.y};
// 3. Renderitzar amb brightness (fade)
text_.render(pf.text, render_pos, escala, spacing, pf.brightness);
}
}
void GestorPuntuacioFlotant::reiniciar() {
for (auto& pf : pool_) {
pf.actiu = false;
}
}
int GestorPuntuacioFlotant::get_num_actius() const {
int count = 0;
for (const auto& pf : pool_) {
if (pf.actiu)
count++;
}
return count;
}
PuntuacioFlotant* GestorPuntuacioFlotant::trobar_slot_lliure() {
for (auto& pf : pool_) {
if (!pf.actiu)
return &pf;
}
return nullptr; // Pool ple
}
} // namespace Effects

View File

@@ -0,0 +1,54 @@
// gestor_puntuacio_flotant.hpp - Gestor de números de puntuació flotants
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include "core/defaults.hpp"
#include "core/graphics/vector_text.hpp"
#include "core/types.hpp"
#include "puntuacio_flotant.hpp"
namespace Effects {
// Gestor de números de puntuació flotants
// Manté un pool de PuntuacioFlotant i gestiona el seu cicle de vida
class GestorPuntuacioFlotant {
public:
explicit GestorPuntuacioFlotant(SDL_Renderer* renderer);
// Crear número flotant
// - punts: valor numèric (100, 150, 200)
// - posicio: on apareix (normalment centre d'enemic destruït)
void crear(int punts, const Punt& posicio);
// Actualitzar tots els números actius
void actualitzar(float delta_time);
// Dibuixar tots els números actius
void dibuixar();
// Reiniciar tots (neteja)
void reiniciar();
// Obtenir número actius (debug)
int get_num_actius() const;
private:
SDL_Renderer* renderer_;
Graphics::VectorText text_; // Sistema de text vectorial
// Pool de números flotants (màxim concurrent)
// Màxim 15 enemics simultanis = màxim 15 números
static constexpr int MAX_PUNTUACIONS =
Defaults::FloatingScore::MAX_CONCURRENT;
std::array<PuntuacioFlotant, MAX_PUNTUACIONS> pool_;
// Trobar primer slot inactiu
PuntuacioFlotant* trobar_slot_lliure();
};
} // namespace Effects

View File

@@ -0,0 +1,33 @@
// puntuacio_flotant.hpp - Número de puntuació que apareix i desapareix
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <string>
#include "core/types.hpp"
namespace Effects {
// PuntuacioFlotant: text animat que mostra punts guanyats
// S'activa quan es destrueix un enemic i s'esvaeix després d'un temps
struct PuntuacioFlotant {
// Text a mostrar (e.g., "100", "150", "200")
std::string text;
// Posició actual (coordenades mundials)
Punt posicio;
// Animació de moviment
Punt velocitat; // px/s (normalment cap amunt: {0.0f, -30.0f})
// Animació de fade
float temps_vida; // Temps transcorregut (segons)
float temps_max; // Temps de vida màxim (segons)
float brightness; // Brillantor calculada (0.0-1.0)
// Estat
bool actiu;
};
} // namespace Effects

View File

@@ -67,8 +67,8 @@ void Bala::actualitzar(float delta_time) {
void Bala::dibuixar() const {
if (esta_ && forma_) {
// [NUEVO] Usar render_shape en lloc de rota_pol
// Les bales no roten visualment (angle sempre 0.0f)
Rendering::render_shape(renderer_, forma_, centre_, 0.0f, 1.0f, true, 1.0f, brightness_);
// Les bales roten segons l'angle de trajectòria
Rendering::render_shape(renderer_, forma_, centre_, angle_, 1.0f, true, 1.0f, brightness_);
}
}

View File

@@ -24,12 +24,14 @@ 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
timer_invulnerabilitat_(0.0f) { // Start vulnerable
// [NUEVO] Forma es carrega a inicialitzar() segons el tipus
// Constructor no carrega forma per permetre tipus diferents
}
void Enemic::inicialitzar(TipusEnemic tipus) {
void Enemic::inicialitzar(TipusEnemic tipus, const Punt* ship_pos) {
// Guardar tipus
tipus_ = tipus;
@@ -67,7 +69,7 @@ void Enemic::inicialitzar(TipusEnemic tipus) {
std::cerr << "[Enemic] Error: no s'ha pogut carregar " << shape_file << std::endl;
}
// Posició aleatòria dins de l'àrea de joc
// [MODIFIED] Posició aleatòria amb comprovació de seguretat
float min_x, max_x, min_y, max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
@@ -75,10 +77,38 @@ void Enemic::inicialitzar(TipusEnemic tipus) {
min_y,
max_y);
int range_x = static_cast<int>(max_x - min_x);
int range_y = static_cast<int>(max_y - min_y);
centre_.x = static_cast<float>((std::rand() % range_x) + static_cast<int>(min_x));
centre_.y = static_cast<float>((std::rand() % range_y) + static_cast<int>(min_y));
if (ship_pos != nullptr) {
// [NEW] Safe spawn: attempt to find position away from ship
bool found_safe_position = false;
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
float candidate_x, candidate_y;
if (intent_spawn_safe(*ship_pos, candidate_x, candidate_y)) {
centre_.x = candidate_x;
centre_.y = candidate_y;
found_safe_position = true;
break;
}
}
if (!found_safe_position) {
// Fallback: spawn anywhere (user's preference)
int range_x = static_cast<int>(max_x - min_x);
int range_y = static_cast<int>(max_y - min_y);
centre_.x = static_cast<float>((std::rand() % range_x) + static_cast<int>(min_x));
centre_.y = static_cast<float>((std::rand() % range_y) + static_cast<int>(min_y));
std::cout << "[Enemic] Advertència: spawn sense zona segura després de "
<< Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intents" << std::endl;
}
} else {
// [EXISTING] No ship position: spawn anywhere (backward compatibility)
int range_x = static_cast<int>(max_x - min_x);
int range_y = static_cast<int>(max_y - min_y);
centre_.x = static_cast<float>((std::rand() % range_x) + static_cast<int>(min_x));
centre_.y = static_cast<float>((std::rand() % range_y) + static_cast<int>(min_y));
}
// Angle aleatori de moviment
angle_ = (std::rand() % 360) * Constants::PI / 180.0f;
@@ -94,12 +124,34 @@ void Enemic::inicialitzar(TipusEnemic tipus) {
animacio_.drotacio_objetivo = drotacio_;
animacio_.drotacio_t = 1.0f; // Start without interpolating
// [NEW] Inicialitzar invulnerabilitat
timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; // 3.0s
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; // 0.3f
// Activar
esta_ = true;
}
void Enemic::actualitzar(float delta_time) {
if (esta_) {
// [NEW] Update invulnerability timer and brightness
if (timer_invulnerabilitat_ > 0.0f) {
timer_invulnerabilitat_ -= delta_time;
if (timer_invulnerabilitat_ < 0.0f) {
timer_invulnerabilitat_ = 0.0f;
}
// [NEW] Update brightness with LERP during invulnerability
float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
float t = 1.0f - t_inv; // 0.0 → 1.0
float smooth_t = t * t * (3.0f - 2.0f * t); // smoothstep
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END;
brightness_ = START + (END - START) * smooth_t;
}
// Moviment autònom
mou(delta_time);
@@ -113,8 +165,10 @@ void Enemic::actualitzar(float delta_time) {
void Enemic::dibuixar() const {
if (esta_ && forma_) {
// [NUEVO] Usar render_shape amb escala animada
// Calculate animated scale (includes invulnerability LERP)
float escala = calcular_escala_actual();
// brightness_ is already updated in actualitzar()
Rendering::render_shape(renderer_, forma_, centre_, rotacio_, escala, true, 1.0f, brightness_);
}
}
@@ -201,8 +255,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_;
}
}
@@ -387,10 +441,74 @@ void Enemic::actualitzar_rotacio_accelerada(float delta_time) {
float Enemic::calcular_escala_actual() const {
float escala = 1.0f;
if (animacio_.palpitacio_activa) {
// Add pulsating scale variation
// [NEW] Invulnerability LERP prioritza sobre palpitació
if (timer_invulnerabilitat_ > 0.0f) {
// Calculate t: 0.0 at spawn → 1.0 at end
float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
float t = 1.0f - t_inv; // 0.0 → 1.0
// Apply smoothstep: t² * (3 - 2t)
float smooth_t = t * t * (3.0f - 2.0f * t);
// LERP scale from 0.0 to 1.0
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END;
escala = START + (END - START) * smooth_t;
} else if (animacio_.palpitacio_activa) {
// [EXISTING] Palpitació només quan no invulnerable
escala += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase);
}
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;
}
}
// [NEW] Safe spawn helper - checks if position is away from ship
bool Enemic::intent_spawn_safe(const Punt& ship_pos, float& out_x, float& out_y) {
// Generate random position within safe bounds
float min_x, max_x, min_y, max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
max_x,
min_y,
max_y);
int range_x = static_cast<int>(max_x - min_x);
int range_y = static_cast<int>(max_y - min_y);
out_x = static_cast<float>((std::rand() % range_x) + static_cast<int>(min_x));
out_y = static_cast<float>((std::rand() % range_y) + static_cast<int>(min_y));
// Check Euclidean distance to ship
float dx = out_x - ship_pos.x;
float dy = out_y - ship_pos.y;
float distancia = std::sqrt(dx * dx + dy * dy);
// Return true if position is safe (>= 36px from ship)
return distancia >= Defaults::Enemies::Spawn::SAFETY_DISTANCE;
}

View File

@@ -9,6 +9,7 @@
#include "core/graphics/shape.hpp"
#include "core/types.hpp"
#include "game/constants.hpp"
// Tipus d'enemic
enum class TipusEnemic : uint8_t {
@@ -39,7 +40,7 @@ class Enemic {
: renderer_(nullptr) {}
Enemic(SDL_Renderer* renderer);
void inicialitzar(TipusEnemic tipus = TipusEnemic::PENTAGON);
void inicialitzar(TipusEnemic tipus = TipusEnemic::PENTAGON, const Punt* ship_pos = nullptr);
void actualitzar(float delta_time);
void dibuixar() const;
@@ -48,10 +49,32 @@ class Enemic {
const Punt& get_centre() const { return centre_; }
const std::shared_ptr<Graphics::Shape>& get_forma() const { return forma_; }
void destruir() { esta_ = false; }
float get_brightness() const { return brightness_; }
float get_drotacio() const { return drotacio_; }
Punt get_velocitat_vector() const {
return {
velocitat_ * std::cos(angle_ - Constants::PI / 2.0f),
velocitat_ * std::sin(angle_ - Constants::PI / 2.0f)
};
}
// 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);
// [NEW] Invulnerability queries
bool es_invulnerable() const { return timer_invulnerabilitat_ > 0.0f; }
float get_temps_invulnerabilitat() const { return timer_invulnerabilitat_; }
private:
SDL_Renderer* renderer_;
@@ -76,6 +99,10 @@ 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
// [NEW] Invulnerability state
float timer_invulnerabilitat_; // Countdown timer (seconds), 0.0f = vulnerable
// [EXISTING] Private methods
void mou(float delta_time);
@@ -88,4 +115,5 @@ class Enemic {
void comportament_quadrat(float delta_time);
void comportament_molinillo(float delta_time);
float calcular_escala_actual() const; // Returns scale with palpitation applied
bool intent_spawn_safe(const Punt& ship_pos, float& out_x, float& out_y);
};

View File

@@ -22,7 +22,7 @@ Nau::Nau(SDL_Renderer* renderer)
esta_tocada_(false),
brightness_(Defaults::Brightness::NAU) {
// [NUEVO] Carregar forma compartida des de fitxer
forma_ = Graphics::ShapeLoader::load("ship.shp");
forma_ = Graphics::ShapeLoader::load("ship2.shp");
if (!forma_ || !forma_->es_valida()) {
std::cerr << "[Nau] Error: no s'ha pogut carregar ship.shp" << std::endl;

View File

@@ -9,6 +9,7 @@
#include "core/graphics/shape.hpp"
#include "core/types.hpp"
#include "game/constants.hpp"
class Nau {
public:
@@ -26,6 +27,13 @@ class Nau {
float get_angle() const { return angle_; }
bool esta_viva() const { return !esta_tocada_; }
const std::shared_ptr<Graphics::Shape>& get_forma() const { return forma_; }
float get_brightness() const { return brightness_; }
Punt get_velocitat_vector() const {
return {
velocitat_ * std::cos(angle_ - Constants::PI / 2.0f),
velocitat_ * std::sin(angle_ - Constants::PI / 2.0f)
};
}
// Col·lisions (Fase 10)
void marcar_tocada() { esta_tocada_ = true; }

View File

@@ -13,15 +13,28 @@
#include "core/audio/audio.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/line_renderer.hpp"
#include "core/system/gestor_escenes.hpp"
#include "core/system/context_escenes.hpp"
#include "core/system/global_events.hpp"
#include "game/stage_system/stage_loader.hpp"
EscenaJoc::EscenaJoc(SDLManager& sdl)
// Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes;
using Escena = ContextEscenes::Escena;
using Opcio = ContextEscenes::Opcio;
EscenaJoc::EscenaJoc(SDLManager& sdl, ContextEscenes& context)
: sdl_(sdl),
context_(context),
debris_manager_(sdl.obte_renderer()),
gestor_puntuacio_(sdl.obte_renderer()),
nau_(sdl.obte_renderer()),
itocado_(0),
puntuacio_total_(0),
text_(sdl.obte_renderer()) {
// Consumir opcions (preparació per MODE_DEMO futur)
auto opcio = context_.consumir_opcio();
(void)opcio; // Suprimir warning de variable no usada
// Inicialitzar bales amb renderer
for (auto& bala : bales_) {
bala = Bala(sdl.obte_renderer());
@@ -42,7 +55,7 @@ void EscenaJoc::executar() {
SDL_Event event;
Uint64 last_time = SDL_GetTicks();
while (GestorEscenes::actual == GestorEscenes::Escena::JOC) {
while (GestorEscenes::actual == Escena::JOC) {
// Calcular delta_time real
Uint64 current_time = SDL_GetTicks();
float delta_time = (current_time - last_time) / 1000.0f;
@@ -67,7 +80,7 @@ void EscenaJoc::executar() {
}
// Events globals (F1/F2/F3/ESC/QUIT)
if (GlobalEvents::handle(event, sdl_)) {
if (GlobalEvents::handle(event, sdl_, context_)) {
continue;
}
@@ -105,6 +118,22 @@ 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();
// [NEW] Set ship position reference for safe spawn
stage_manager_->get_spawn_controller().set_ship_position(&nau_.get_centre());
// Inicialitzar estat de col·lisió
itocado_ = 0;
@@ -113,28 +142,21 @@ void EscenaJoc::inicialitzar() {
game_over_ = false;
game_over_timer_ = 0.0f;
// Initialize score
puntuacio_total_ = 0;
gestor_puntuacio_.reiniciar();
// Set spawn point to center of play area
Constants::obtenir_centre_zona(punt_spawn_.x, punt_spawn_.y);
// 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
@@ -155,8 +177,9 @@ void EscenaJoc::actualitzar(float delta_time) {
if (game_over_timer_ <= 0.0f) {
// Aturar música de joc abans de tornar al títol
Audio::get()->stopMusic();
// Auto-transition to title screen
GestorEscenes::actual = GestorEscenes::Escena::TITOL;
// Transició a pantalla de títol
context_.canviar_escena(Escena::TITOL);
GestorEscenes::actual = Escena::TITOL;
return;
}
@@ -170,6 +193,7 @@ void EscenaJoc::actualitzar(float delta_time) {
}
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time);
return;
}
@@ -207,31 +231,84 @@ void EscenaJoc::actualitzar(float delta_time) {
}
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(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:
// Update countdown timer
stage_manager_->actualitzar(delta_time);
// [NEW] Allow ship movement and shooting during intro
nau_.processar_input(delta_time);
nau_.actualitzar(delta_time);
// [NEW] Update bullets
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
// [NEW] Update debris
debris_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();
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
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);
gestor_puntuacio_.actualitzar(delta_time);
break;
}
case StageSystem::EstatStage::LEVEL_COMPLETED:
// Update countdown timer
stage_manager_->actualitzar(delta_time);
// [NEW] Allow ship movement and shooting during outro
nau_.processar_input(delta_time);
nau_.actualitzar(delta_time);
// [NEW] Update bullets (allow last shots to continue)
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
// [NEW] Update debris (from last destroyed enemies)
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.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() {
@@ -251,6 +328,7 @@ void EscenaJoc::dibuixar() {
}
debris_manager_.dibuixar();
gestor_puntuacio_.dibuixar();
// Draw centered "GAME OVER" text
const std::string game_over_text = "GAME OVER";
@@ -270,26 +348,69 @@ 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:
// [NEW] Draw ship if alive
if (itocado_ == 0.0f) {
nau_.dibuixar();
}
// [NEW] Draw bullets
for (const auto& bala : bales_) {
bala.dibuixar();
}
// [NEW] Draw debris
debris_manager_.dibuixar();
gestor_puntuacio_.dibuixar();
// [EXISTING] Draw intro message and score
dibuixar_missatge_stage(stage_manager_->get_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();
gestor_puntuacio_.dibuixar();
dibuixar_marcador();
break;
case StageSystem::EstatStage::LEVEL_COMPLETED:
// [NEW] Draw ship if alive
if (itocado_ == 0.0f) {
nau_.dibuixar();
}
// [NEW] Draw bullets (allow last shots to be visible)
for (const auto& bala : bales_) {
bala.dibuixar();
}
// [NEW] Draw debris (from last destroyed enemies)
debris_manager_.dibuixar();
gestor_puntuacio_.dibuixar();
// [EXISTING] Draw completion message and score
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) {
@@ -365,13 +486,20 @@ void EscenaJoc::tocado() {
// Create ship explosion
const Punt& ship_pos = nau_.get_centre();
float ship_angle = nau_.get_angle();
Punt vel_nau = nau_.get_velocitat_vector();
// Reduir a 80% la velocitat heretada per la nau (més realista)
Punt vel_nau_80 = {vel_nau.x * 0.8f, vel_nau.y * 0.8f};
debris_manager_.explotar(
nau_.get_forma(), // Ship shape (3 lines)
ship_pos, // Center position
ship_angle, // Ship orientation
1.0f, // Normal scale
Defaults::Physics::Debris::VELOCITAT_BASE // 80 px/s
Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s
nau_.get_brightness(), // Heredar brightness
vel_nau_80, // Heredar 80% velocitat
0.0f, // Nave: trayectorias rectas (sin drotacio)
0.0f // Sin herencia visual (rotación aleatoria)
);
// Start death timer (non-zero to avoid re-triggering)
@@ -399,8 +527,17 @@ 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);
// Format score with padding to 5 digits (e.g., 150 → "00150")
std::string score_str = std::to_string(puntuacio_total_);
score_str = std::string(5 - std::min(5, static_cast<int>(score_str.length())), '0') + score_str;
std::string text = "SCORE: " + score_str + " LIFES: " + std::to_string(num_vides_) +
" LEVEL: " + stage_str;
// Paràmetres de renderització
const float escala = 0.85f;
@@ -429,7 +566,7 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() {
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS; // 826.56
// Velocitat d'explosió reduïda per efecte suau
constexpr float VELOCITAT_EXPLOSIO = 50.0f; // px/s (en lloc de 80.0f per defecte)
constexpr float VELOCITAT_EXPLOSIO = 80.0f; // px/s (en lloc de 80.0f per defecte)
// Iterar per totes les bales actives
for (auto& bala : bales_) {
@@ -445,6 +582,11 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() {
continue;
}
// [NEW] Skip collision if enemy is invulnerable
if (enemic.es_invulnerable()) {
continue;
}
const Punt& pos_enemic = enemic.get_centre();
// Calcular distància quadrada (evita sqrt)
@@ -456,16 +598,41 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() {
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
// *** COL·LISIÓ DETECTADA ***
// 1. Destruir enemic (marca com inactiu)
// 1. Calculate score for enemy type
int punts = 0;
switch (enemic.get_tipus()) {
case TipusEnemic::PENTAGON:
punts = Defaults::Enemies::Scoring::PENTAGON_SCORE;
break;
case TipusEnemic::QUADRAT:
punts = Defaults::Enemies::Scoring::QUADRAT_SCORE;
break;
case TipusEnemic::MOLINILLO:
punts = Defaults::Enemies::Scoring::MOLINILLO_SCORE;
break;
}
// 2. Add to total score
puntuacio_total_ += punts;
// 3. Create floating score number
gestor_puntuacio_.crear(punts, pos_enemic);
// 4. Destruir enemic (marca com inactiu)
enemic.destruir();
// 2. Crear explosió de fragments
Punt vel_enemic = enemic.get_velocitat_vector();
debris_manager_.explotar(
enemic.get_forma(), // Forma vectorial del pentàgon
pos_enemic, // Posició central
0.0f, // Angle (enemic té rotació interna)
1.0f, // Escala normal
VELOCITAT_EXPLOSIO // 50 px/s (explosió suau)
enemic.get_forma(), // Forma vectorial del pentàgon
pos_enemic, // Posició central
0.0f, // Angle (enemic té rotació interna)
1.0f, // Escala normal
VELOCITAT_EXPLOSIO, // 50 px/s (explosió suau)
enemic.get_brightness(), // Heredar brightness
vel_enemic, // Heredar velocitat
enemic.get_drotacio(), // Heredar velocitat angular (trayectorias curvas)
0.0f // Sin herencia visual (rotación aleatoria)
);
// 3. Desactivar bala
@@ -499,6 +666,11 @@ void EscenaJoc::detectar_col·lisio_nau_enemics() {
continue;
}
// [NEW] Skip collision if enemy is invulnerable
if (enemic.es_invulnerable()) {
continue;
}
const Punt& pos_enemic = enemic.get_centre();
// Calculate squared distance (avoid sqrt)
@@ -513,3 +685,71 @@ void EscenaJoc::detectar_col·lisio_nau_enemics() {
}
}
}
// [NEW] Stage system helper methods
void EscenaJoc::dibuixar_missatge_stage(const std::string& missatge) {
constexpr float escala_base = 1.0f;
constexpr float spacing = 2.0f;
constexpr float max_width_ratio = 0.9f; // 90% del ancho disponible
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
const float max_width = play_area.w * max_width_ratio; // 558px
// ========== TYPEWRITER EFFECT (PARAMETRIZED) ==========
// Get state-specific timing configuration
float total_time;
float typing_ratio;
if (stage_manager_->get_estat() == StageSystem::EstatStage::LEVEL_START) {
total_time = Defaults::Game::LEVEL_START_DURATION;
typing_ratio = Defaults::Game::LEVEL_START_TYPING_RATIO;
} else { // LEVEL_COMPLETED
total_time = Defaults::Game::LEVEL_COMPLETED_DURATION;
typing_ratio = Defaults::Game::LEVEL_COMPLETED_TYPING_RATIO;
}
// Calculate progress from timer (0.0 at start → 1.0 at end)
float remaining_time = stage_manager_->get_timer_transicio();
float progress = 1.0f - (remaining_time / total_time);
// Determine how many characters to show
size_t visible_chars;
if (typing_ratio > 0.0f && progress < typing_ratio) {
// Typewriter phase: show partial text
float typing_progress = progress / typing_ratio; // Normalize to 0.0-1.0
visible_chars = static_cast<size_t>(missatge.length() * typing_progress);
if (visible_chars == 0 && progress > 0.0f) {
visible_chars = 1; // Show at least 1 character after first frame
}
} else {
// Display phase: show complete text
// (Either after typing phase, or immediately if typing_ratio == 0.0)
visible_chars = missatge.length();
}
// Create partial message (substring for typewriter)
std::string partial_message = missatge.substr(0, visible_chars);
// ===================================================
// Calculate text width at base scale (using FULL message for position calculation)
float text_width_at_base = text_.get_text_width(missatge, escala_base, spacing);
// Auto-scale if text exceeds max width
float escala = (text_width_at_base <= max_width)
? escala_base
: max_width / text_width_at_base;
// Recalculate dimensions with final scale (using FULL message for centering)
float full_text_width = text_.get_text_width(missatge, escala, spacing);
float text_height = text_.get_text_height(escala);
// Calculate position as if FULL text was there (for fixed position typewriter)
float x = play_area.x + (play_area.w - full_text_width) / 2.0f;
float y = play_area.y + (play_area.h * 0.25f) - (text_height / 2.0f);
// Render only the partial message (typewriter effect)
Punt pos = {x, y};
text_.render(partial_message, pos, escala, spacing);
}

View File

@@ -12,17 +12,22 @@
#include "../constants.hpp"
#include "../effects/debris_manager.hpp"
#include "../effects/gestor_puntuacio_flotant.hpp"
#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/system/context_escenes.hpp"
#include "core/types.hpp"
#include <memory>
// Classe principal del joc (escena)
class EscenaJoc {
public:
explicit EscenaJoc(SDLManager& sdl);
explicit EscenaJoc(SDLManager& sdl, GestorEscenes::ContextEscenes& context);
~EscenaJoc() = default;
void executar(); // Bucle principal de l'escena
@@ -33,9 +38,11 @@ class EscenaJoc {
private:
SDLManager& sdl_;
GestorEscenes::ContextEscenes& context_;
// Efectes visuals
Effects::DebrisManager debris_manager_;
Effects::GestorPuntuacioFlotant gestor_puntuacio_;
// Estat del joc
Nau nau_;
@@ -49,16 +56,24 @@ class EscenaJoc {
bool game_over_; // Game over state flag
float game_over_timer_; // Countdown timer for auto-return (seconds)
Punt punt_spawn_; // Configurable spawn point
int puntuacio_total_; // Current score
// 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

@@ -13,9 +13,14 @@
#include "core/graphics/shape_loader.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/gestor_escenes.hpp"
#include "core/system/context_escenes.hpp"
#include "core/system/global_events.hpp"
// Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes;
using Escena = ContextEscenes::Escena;
using Opcio = ContextEscenes::Opcio;
// Helper: calcular el progrés individual d'una lletra
// en funció del progrés global (efecte seqüencial)
static float calcular_progress_letra(size_t letra_index, size_t num_letras, float global_progress, float threshold) {
@@ -38,14 +43,20 @@ static float calcular_progress_letra(size_t letra_index, size_t num_letras, floa
}
}
EscenaLogo::EscenaLogo(SDLManager& sdl)
EscenaLogo::EscenaLogo(SDLManager& sdl, ContextEscenes& context)
: sdl_(sdl),
context_(context),
estat_actual_(EstatAnimacio::PRE_ANIMATION),
temps_estat_actual_(0.0f),
debris_manager_(std::make_unique<Effects::DebrisManager>(sdl.obte_renderer())),
lletra_explosio_index_(0),
temps_des_ultima_explosio_(0.0f) {
std::cout << "Escena Logo: Inicialitzant...\n";
// Consumir opcions (LOGO no processa opcions actualment)
auto opcio = context_.consumir_opcio();
(void)opcio; // Suprimir warning
so_reproduit_.fill(false); // Inicialitzar seguiment de sons
inicialitzar_lletres();
}
@@ -54,7 +65,7 @@ void EscenaLogo::executar() {
SDL_Event event;
Uint64 last_time = SDL_GetTicks();
while (GestorEscenes::actual == GestorEscenes::Escena::LOGO) {
while (GestorEscenes::actual == Escena::LOGO) {
// Calcular delta_time real
Uint64 current_time = SDL_GetTicks();
float delta_time = (current_time - last_time) / 1000.0f;
@@ -79,7 +90,7 @@ void EscenaLogo::executar() {
}
// Events globals (F1/F2/F3/ESC/QUIT)
if (GlobalEvents::handle(event, sdl_)) {
if (GlobalEvents::handle(event, sdl_, context_)) {
continue;
}
@@ -229,7 +240,9 @@ void EscenaLogo::actualitzar_explosions(float delta_time) {
lletra.posicio, // Posició
0.0f, // Angle (sense rotació)
ESCALA_FINAL, // Escala (lletres a escala final)
VELOCITAT_EXPLOSIO // Velocitat base
VELOCITAT_EXPLOSIO, // Velocitat base
1.0f, // Brightness màxim (per defecte)
{0.0f, 0.0f} // Sense velocitat (per defecte)
);
std::cout << "[EscenaLogo] Explota lletra " << lletra_explosio_index_ << "\n";
@@ -292,8 +305,9 @@ void EscenaLogo::actualitzar(float delta_time) {
case EstatAnimacio::POST_EXPLOSION:
if (temps_estat_actual_ >= DURACIO_POST_EXPLOSION) {
// Iniciar música de títol abans de la transició
GestorEscenes::actual = GestorEscenes::Escena::TITOL;
// Transició a pantalla de títol
context_.canviar_escena(Escena::TITOL);
GestorEscenes::actual = Escena::TITOL;
}
break;
}
@@ -394,6 +408,12 @@ void EscenaLogo::processar_events(const SDL_Event& event) {
// Qualsevol tecla o clic de ratolí salta a la pantalla de títol
if (event.type == SDL_EVENT_KEY_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
GestorEscenes::actual = GestorEscenes::Escena::TITOL;
// Utilitzar context per especificar escena i opció
context_.canviar_escena(
Escena::TITOL,
Opcio::JUMP_TO_TITLE_MAIN
);
// Backward compatibility: També actualitzar GestorEscenes::actual
GestorEscenes::actual = Escena::TITOL;
}
}

View File

@@ -13,11 +13,12 @@
#include "core/defaults.hpp"
#include "core/graphics/shape.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/context_escenes.hpp"
#include "core/types.hpp"
class EscenaLogo {
public:
explicit EscenaLogo(SDLManager& sdl);
explicit EscenaLogo(SDLManager& sdl, GestorEscenes::ContextEscenes& context);
void executar(); // Bucle principal de l'escena
private:
@@ -31,6 +32,7 @@ class EscenaLogo {
};
SDLManager& sdl_;
GestorEscenes::ContextEscenes& context_;
EstatAnimacio estat_actual_; // Estat actual de la màquina
float
temps_estat_actual_; // Temps en l'estat actual (reset en cada transició)

View File

@@ -12,22 +12,36 @@
#include "core/graphics/shape_loader.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/gestor_escenes.hpp"
#include "core/system/context_escenes.hpp"
#include "core/system/global_events.hpp"
#include "project.h"
namespace {
// Brightness del starfield (1.0 = default, >1.0 més brillant, <1.0 menys brillant)
constexpr float BRIGHTNESS_STARFIELD = 1.2f;
} // namespace
// Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes;
using Escena = ContextEscenes::Escena;
using Opcio = ContextEscenes::Opcio;
EscenaTitol::EscenaTitol(SDLManager& sdl)
EscenaTitol::EscenaTitol(SDLManager& sdl, ContextEscenes& context)
: sdl_(sdl),
context_(context),
text_(sdl.obte_renderer()),
estat_actual_(EstatTitol::INIT),
temps_acumulat_(0.0f) {
estat_actual_(EstatTitol::STARFIELD_FADE_IN),
temps_acumulat_(0.0f),
temps_animacio_(0.0f),
temps_estat_main_(0.0f),
animacio_activa_(false),
factor_lerp_(0.0f) {
std::cout << "Escena Titol: Inicialitzant...\n";
// Processar opció del context
auto opcio = context_.consumir_opcio();
if (opcio == Opcio::JUMP_TO_TITLE_MAIN) {
std::cout << "Escena Titol: Opció JUMP_TO_TITLE_MAIN activada\n";
estat_actual_ = EstatTitol::MAIN;
temps_estat_main_ = 0.0f;
}
// Crear starfield de fons
Punt centre_pantalla{
Defaults::Game::WIDTH / 2.0f,
@@ -46,8 +60,14 @@ EscenaTitol::EscenaTitol(SDLManager& sdl)
150 // densitat: 150 estrelles (50 per capa)
);
// Configurar brightness del starfield
starfield_->set_brightness(BRIGHTNESS_STARFIELD);
// Brightness depèn de l'opció
if (estat_actual_ == EstatTitol::MAIN) {
// Si saltem a MAIN, starfield instantàniament brillant
starfield_->set_brightness(BRIGHTNESS_STARFIELD);
} else {
// Flux normal: comença amb brightness 0.0 per fade-in
starfield_->set_brightness(0.0f);
}
// Inicialitzar lletres del títol "ORNI ATTACK!"
inicialitzar_titol();
@@ -198,13 +218,26 @@ void EscenaTitol::inicialitzar_titol() {
std::cout << "[EscenaTitol] Línia 2 (ATTACK!): " << lletres_attack_.size()
<< " lletres, ancho total: " << ancho_total_attack << " px\n";
// Guardar posicions originals per l'animació orbital
posicions_originals_orni_.clear();
for (const auto& lletra : lletres_orni_) {
posicions_originals_orni_.push_back(lletra.posicio);
}
posicions_originals_attack_.clear();
for (const auto& lletra : lletres_attack_) {
posicions_originals_attack_.push_back(lletra.posicio);
}
std::cout << "[EscenaTitol] Animació: Posicions originals guardades\n";
}
void EscenaTitol::executar() {
SDL_Event event;
Uint64 last_time = SDL_GetTicks();
while (GestorEscenes::actual == GestorEscenes::Escena::TITOL) {
while (GestorEscenes::actual == Escena::TITOL) {
// Calcular delta_time real
Uint64 current_time = SDL_GetTicks();
float delta_time = (current_time - last_time) / 1000.0f;
@@ -229,7 +262,7 @@ void EscenaTitol::executar() {
}
// Events globals (F1/F2/F3/F4/ESC/QUIT)
if (GlobalEvents::handle(event, sdl_)) {
if (GlobalEvents::handle(event, sdl_, context_)) {
continue;
}
@@ -269,41 +302,171 @@ void EscenaTitol::actualitzar(float delta_time) {
}
switch (estat_actual_) {
case EstatTitol::INIT:
case EstatTitol::STARFIELD_FADE_IN: {
temps_acumulat_ += delta_time;
// Calcular progrés del fade (0.0 → 1.0)
float progress = std::min(1.0f, temps_acumulat_ / DURACIO_FADE_IN);
// Lerp brightness de 0.0 a BRIGHTNESS_STARFIELD
float brightness_actual = progress * BRIGHTNESS_STARFIELD;
starfield_->set_brightness(brightness_actual);
// Transició a STARFIELD quan el fade es completa
if (temps_acumulat_ >= DURACIO_FADE_IN) {
estat_actual_ = EstatTitol::STARFIELD;
temps_acumulat_ = 0.0f; // Reset timer per al següent estat
starfield_->set_brightness(BRIGHTNESS_STARFIELD); // Assegurar valor final
}
break;
}
case EstatTitol::STARFIELD:
temps_acumulat_ += delta_time;
if (temps_acumulat_ >= DURACIO_INIT) {
estat_actual_ = EstatTitol::MAIN;
temps_estat_main_ = 0.0f; // Reset timer al entrar a MAIN
animacio_activa_ = false; // Comença estàtic
factor_lerp_ = 0.0f; // Sense animació encara
}
break;
case EstatTitol::MAIN:
// No hi ha lògica d'actualització en l'estat MAIN
break;
case EstatTitol::MAIN: {
temps_estat_main_ += delta_time;
case EstatTitol::TRANSITION:
// Fase 1: Estàtic (0-10s)
if (temps_estat_main_ < DELAY_INICI_ANIMACIO) {
factor_lerp_ = 0.0f;
animacio_activa_ = false;
}
// Fase 2: Lerp (10-12s)
else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) {
float temps_lerp = temps_estat_main_ - DELAY_INICI_ANIMACIO;
factor_lerp_ = temps_lerp / DURACIO_LERP; // 0.0 → 1.0 linealment
animacio_activa_ = true;
}
// Fase 3: Animació completa (12s+)
else {
factor_lerp_ = 1.0f;
animacio_activa_ = true;
}
// Actualitzar animació del logo
actualitzar_animacio_logo(delta_time);
break;
}
case EstatTitol::TRANSITION_TO_GAME:
temps_acumulat_ += delta_time;
// Continuar animació orbital durant la transició
actualitzar_animacio_logo(delta_time);
if (temps_acumulat_ >= DURACIO_TRANSITION) {
// Transició a JOC (la música ja s'ha parat en el fade)
GestorEscenes::actual = GestorEscenes::Escena::JOC;
GestorEscenes::actual = Escena::JOC;
}
break;
}
}
void EscenaTitol::actualitzar_animacio_logo(float delta_time) {
// Només calcular i aplicar offsets si l'animació està activa
if (animacio_activa_) {
// Acumular temps escalat
temps_animacio_ += delta_time * factor_lerp_;
// Usar amplituds i freqüències completes
float amplitude_x_actual = ORBIT_AMPLITUDE_X;
float amplitude_y_actual = ORBIT_AMPLITUDE_Y;
float frequency_x_actual = ORBIT_FREQUENCY_X;
float frequency_y_actual = ORBIT_FREQUENCY_Y;
// Calcular offset orbital
float offset_x = amplitude_x_actual * std::sin(2.0f * Defaults::Math::PI * frequency_x_actual * temps_animacio_);
float offset_y = amplitude_y_actual * std::sin(2.0f * Defaults::Math::PI * frequency_y_actual * temps_animacio_ + ORBIT_PHASE_OFFSET);
// Aplicar offset a totes les lletres de "ORNI"
for (size_t i = 0; i < lletres_orni_.size(); ++i) {
lletres_orni_[i].posicio.x = posicions_originals_orni_[i].x + static_cast<int>(std::round(offset_x));
lletres_orni_[i].posicio.y = posicions_originals_orni_[i].y + static_cast<int>(std::round(offset_y));
}
// Aplicar offset a totes les lletres de "ATTACK!"
for (size_t i = 0; i < lletres_attack_.size(); ++i) {
lletres_attack_[i].posicio.x = posicions_originals_attack_[i].x + static_cast<int>(std::round(offset_x));
lletres_attack_[i].posicio.y = posicions_originals_attack_[i].y + static_cast<int>(std::round(offset_y));
}
}
}
void EscenaTitol::dibuixar() {
// Dibuixar starfield de fons (sempre, en tots els estats)
if (starfield_) {
starfield_->dibuixar();
}
// En l'estat INIT, només mostrar starfield (sense text)
if (estat_actual_ == EstatTitol::INIT) {
// En els estats STARFIELD_FADE_IN i STARFIELD, només mostrar starfield (sense text)
if (estat_actual_ == EstatTitol::STARFIELD_FADE_IN || estat_actual_ == EstatTitol::STARFIELD) {
return;
}
// Estat MAIN i TRANSITION: Dibuixar títol i text (sobre el starfield)
if (estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::TRANSITION) {
// === Dibuixar lletres del títol "ORNI ATTACK!" ===
// Estat MAIN i TRANSITION_TO_GAME: Dibuixar títol i text (sobre el starfield)
if (estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::TRANSITION_TO_GAME) {
// === Calcular i renderitzar ombra (només si animació activa) ===
if (animacio_activa_) {
float temps_shadow = temps_animacio_ - SHADOW_DELAY;
if (temps_shadow < 0.0f) temps_shadow = 0.0f; // Evitar temps negatiu
// Usar amplituds i freqüències completes per l'ombra
float amplitude_x_shadow = ORBIT_AMPLITUDE_X;
float amplitude_y_shadow = ORBIT_AMPLITUDE_Y;
float frequency_x_shadow = ORBIT_FREQUENCY_X;
float frequency_y_shadow = ORBIT_FREQUENCY_Y;
// Calcular offset de l'ombra
float shadow_offset_x = amplitude_x_shadow * std::sin(2.0f * Defaults::Math::PI * frequency_x_shadow * temps_shadow) + SHADOW_OFFSET_X;
float shadow_offset_y = amplitude_y_shadow * std::sin(2.0f * Defaults::Math::PI * frequency_y_shadow * temps_shadow + ORBIT_PHASE_OFFSET) + SHADOW_OFFSET_Y;
// === RENDERITZAR OMBRA PRIMER (darrera del logo principal) ===
// Ombra "ORNI"
for (size_t i = 0; i < lletres_orni_.size(); ++i) {
Punt pos_shadow;
pos_shadow.x = posicions_originals_orni_[i].x + static_cast<int>(std::round(shadow_offset_x));
pos_shadow.y = posicions_originals_orni_[i].y + static_cast<int>(std::round(shadow_offset_y));
Rendering::render_shape(
sdl_.obte_renderer(),
lletres_orni_[i].forma,
pos_shadow,
0.0f,
ESCALA_TITULO,
true,
1.0f, // progress = 1.0 (totalment visible)
SHADOW_BRIGHTNESS // brightness = 0.4 (brillantor reduïda)
);
}
// Ombra "ATTACK!"
for (size_t i = 0; i < lletres_attack_.size(); ++i) {
Punt pos_shadow;
pos_shadow.x = posicions_originals_attack_[i].x + static_cast<int>(std::round(shadow_offset_x));
pos_shadow.y = posicions_originals_attack_[i].y + static_cast<int>(std::round(shadow_offset_y));
Rendering::render_shape(
sdl_.obte_renderer(),
lletres_attack_[i].forma,
pos_shadow,
0.0f,
ESCALA_TITULO,
true,
1.0f, // progress = 1.0 (totalment visible)
SHADOW_BRIGHTNESS);
}
}
// === RENDERITZAR LOGO PRINCIPAL (damunt) ===
// Dibuixar "ORNI" (línia 1)
for (const auto& lletra : lletres_orni_) {
@@ -311,10 +474,10 @@ void EscenaTitol::dibuixar() {
sdl_.obte_renderer(),
lletra.forma,
lletra.posicio,
0.0f, // sense rotació
ESCALA_TITULO, // escala 80%
true, // dibuixar
1.0f // progrés complet (totalment visible)
0.0f,
ESCALA_TITULO,
true,
1.0f // Brillantor completa
);
}
@@ -324,10 +487,10 @@ void EscenaTitol::dibuixar() {
sdl_.obte_renderer(),
lletra.forma,
lletra.posicio,
0.0f, // sense rotació
ESCALA_TITULO, // escala 80%
true, // dibuixar
1.0f // progrés complet (totalment visible)
0.0f,
ESCALA_TITULO,
true,
1.0f // Brillantor completa
);
}
@@ -338,7 +501,7 @@ void EscenaTitol::dibuixar() {
const float spacing = 2.0f; // Espai entre caràcters (usat també per copyright)
bool mostrar_text = true;
if (estat_actual_ == EstatTitol::TRANSITION) {
if (estat_actual_ == EstatTitol::TRANSITION_TO_GAME) {
// Parpelleig: sin oscil·la entre -1 i 1, volem ON quan > 0
float fase = temps_acumulat_ * BLINK_FREQUENCY * 2.0f * 3.14159f; // 2π × freq × temps
mostrar_text = (std::sin(fase) > 0.0f);
@@ -382,19 +545,33 @@ void EscenaTitol::processar_events(const SDL_Event& event) {
if (event.type == SDL_EVENT_KEY_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
switch (estat_actual_) {
case EstatTitol::INIT:
case EstatTitol::STARFIELD_FADE_IN:
// Saltar directament a MAIN (ometre fade-in i starfield)
estat_actual_ = EstatTitol::MAIN;
starfield_->set_brightness(BRIGHTNESS_STARFIELD); // Assegurar brightness final
temps_estat_main_ = 0.0f; // Reset timer per animació de títol
break;
case EstatTitol::STARFIELD:
// Saltar a MAIN
estat_actual_ = EstatTitol::MAIN;
temps_estat_main_ = 0.0f; // Reset timer
break;
case EstatTitol::MAIN:
// Utilitzar context per transició a JOC
context_.canviar_escena(Escena::JOC);
// NO actualitzar GestorEscenes::actual aquí!
// La transició es fa en l'estat TRANSITION_TO_GAME
// Iniciar transició amb fade-out de música
estat_actual_ = EstatTitol::TRANSITION;
temps_acumulat_ = 0.0f; // Reset del comptador
Audio::get()->fadeOutMusic(MUSIC_FADE); // Fade de 300ms
estat_actual_ = EstatTitol::TRANSITION_TO_GAME;
temps_acumulat_ = 0.0f; // Reset del comptador
Audio::get()->fadeOutMusic(MUSIC_FADE); // Fade
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
break;
case EstatTitol::TRANSITION:
case EstatTitol::TRANSITION_TO_GAME:
// Ignorar inputs durant la transició
break;
}

View File

@@ -14,20 +14,22 @@
#include "core/graphics/starfield.hpp"
#include "core/graphics/vector_text.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/context_escenes.hpp"
#include "core/types.hpp"
class EscenaTitol {
public:
explicit EscenaTitol(SDLManager& sdl);
explicit EscenaTitol(SDLManager& sdl, GestorEscenes::ContextEscenes& context);
~EscenaTitol(); // Destructor per aturar música
void executar(); // Bucle principal de l'escena
private:
// Màquina d'estats per la pantalla de títol
enum class EstatTitol {
INIT, // Pantalla negra inicial (2 segons)
MAIN, // Pantalla de títol amb text
TRANSITION // Transició amb fade-out de música i text parpellejant
STARFIELD_FADE_IN, // Fade-in del starfield (1.5s)
STARFIELD, // Pantalla con el campo de estrellas
MAIN, // Pantalla de títol amb text
TRANSITION_TO_GAME // Transició amb fade-out de música i text parpellejant
};
// Estructura per emmagatzemar informació de cada lletra del títol
@@ -40,6 +42,7 @@ class EscenaTitol {
};
SDLManager& sdl_;
GestorEscenes::ContextEscenes& context_;
Graphics::VectorText text_; // Sistema de text vectorial
std::unique_ptr<Graphics::Starfield> starfield_; // Camp d'estrelles de fons
EstatTitol estat_actual_; // Estat actual de la màquina
@@ -50,7 +53,19 @@ class EscenaTitol {
std::vector<LetraLogo> lletres_attack_; // Lletres de "ATTACK!" (línia 2)
float y_attack_dinamica_; // Posició Y calculada dinàmicament per "ATTACK!"
// Estat d'animació del logo
float temps_animacio_; // Temps acumulat per animació orbital
std::vector<Punt> posicions_originals_orni_; // Posicions originals de "ORNI"
std::vector<Punt> posicions_originals_attack_; // Posicions originals de "ATTACK!"
// Estat d'arrencada de l'animació
float temps_estat_main_; // Temps acumulat en estat MAIN
bool animacio_activa_; // Flag: true quan animació està activa
float factor_lerp_; // Factor de lerp actual (0.0 → 1.0)
// Constants
static constexpr float BRIGHTNESS_STARFIELD = 1.2f; // Brightness del starfield (>1.0 = més brillant)
static constexpr float DURACIO_FADE_IN = 3.0f; // Duració del fade-in del starfield (1.5 segons)
static constexpr float DURACIO_INIT = 4.0f; // Duració de l'estat INIT (2 segons)
static constexpr float DURACIO_TRANSITION = 1.5f; // Duració de la transició (1.5 segons)
static constexpr float ESCALA_TITULO = 0.6f; // Escala per les lletres del títol (50%)
@@ -60,8 +75,26 @@ class EscenaTitol {
static constexpr float BLINK_FREQUENCY = 3.0f; // Freqüència de parpelleig (3 Hz)
static constexpr int MUSIC_FADE = 1000; // Duracio del fade de la musica del titol al començar a jugar
// Constants d'animació del logo
static constexpr float ORBIT_AMPLITUDE_X = 4.0f; // Amplitud oscil·lació horitzontal (píxels)
static constexpr float ORBIT_AMPLITUDE_Y = 3.0f; // Amplitud oscil·lació vertical (píxels)
static constexpr float ORBIT_FREQUENCY_X = 0.8f; // Velocitat oscil·lació horitzontal (Hz)
static constexpr float ORBIT_FREQUENCY_Y = 1.2f; // Velocitat oscil·lació vertical (Hz)
static constexpr float ORBIT_PHASE_OFFSET = 1.57f; // Desfasament entre X i Y (90° per circular)
// Constants d'ombra del logo
static constexpr float SHADOW_DELAY = 0.5f; // Retard temporal de l'ombra (segons)
static constexpr float SHADOW_BRIGHTNESS = 0.4f; // Multiplicador de brillantor de l'ombra (0.0-1.0)
static constexpr float SHADOW_OFFSET_X = 2.0f; // Offset espacial X fix (píxels)
static constexpr float SHADOW_OFFSET_Y = 2.0f; // Offset espacial Y fix (píxels)
// Temporització de l'arrencada de l'animació
static constexpr float DELAY_INICI_ANIMACIO = 10.0f; // 10s estàtic abans d'animar
static constexpr float DURACIO_LERP = 2.0f; // 2s per arribar a amplitud completa
// Mètodes privats
void actualitzar(float delta_time);
void actualitzar_animacio_logo(float delta_time); // Actualitza l'animació orbital del logo
void dibuixar();
void processar_events(const SDL_Event& event);
void inicialitzar_titol(); // Carrega i posiciona les lletres del títol

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), ship_position_(nullptr) {}
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, 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;
}
}
}
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, const Punt* ship_pos) {
// Initialize enemy (with safe spawn if ship_pos provided)
enemic.inicialitzar(tipus, ship_pos);
// 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,58 @@
// 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;
// [NEW] Set ship position reference for safe spawn
void set_ship_position(const Punt* ship_pos) { ship_position_ = ship_pos; }
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, const Punt* ship_pos = nullptr);
void aplicar_multiplicadors(Enemic& enemic) const;
const Punt* ship_position_; // [NEW] Non-owning pointer to ship position
};
} // namespace StageSystem

View File

@@ -0,0 +1,100 @@
// stage_config.hpp - Estructures de dades per configuració d'stages
// © 2025 Orni Attack
#pragma once
#include <array>
#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 {
// Pool de missatges per inici de level (selecció aleatòria)
inline constexpr std::array<const char*, 12> MISSATGES_LEVEL_START = {
"ORNI ALERT!",
"INCOMING ORNIS!",
"ROLLING THREAT!",
"ENEMY WAVE!",
"WAVE OF ORNIS DETECTED!",
"NEXT SWARM APPROACHING!",
"BRACE FOR THE NEXT WAVE!",
"ANOTHER ATTACK INCOMING!",
"SENSORS DETECT HOSTILE ORNIS...",
"UNIDENTIFIED ROLLING OBJECTS INBOUND!",
"ENEMY FORCES MOBILIZING!",
"PREPARE FOR IMPACT!"
};
constexpr const char* MISSATGE_LEVEL_COMPLETED = "GOOD JOB COMMANDER!";
}
} // namespace StageSystem

View File

@@ -0,0 +1,251 @@
// stage_loader.cpp - Implementació del carregador de configuració YAML
// © 2025 Orni Attack
#include "stage_loader.hpp"
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include <fstream>
#include <iostream>
#include <sstream>
namespace StageSystem {
std::unique_ptr<ConfigSistemaStages> StageLoader::carregar(const std::string& path) {
try {
// Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml"
std::string normalized = path;
if (normalized.starts_with("data/")) {
normalized = normalized.substr(5);
}
// Load from resource system
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
if (data.empty()) {
std::cerr << "[StageLoader] Error: no es pot carregar " << normalized << std::endl;
return nullptr;
}
// Convert to string
std::string yaml_content(data.begin(), data.end());
std::stringstream stream(yaml_content);
// Parse YAML
fkyaml::node yaml = fkyaml::node::deserialize(stream);
auto config = std::make_unique<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,146 @@
// 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;
// Set timer based on state type
if (nou_estat == EstatStage::LEVEL_START) {
timer_transicio_ = Defaults::Game::LEVEL_START_DURATION;
} else if (nou_estat == EstatStage::LEVEL_COMPLETED) {
timer_transicio_ = Defaults::Game::LEVEL_COMPLETED_DURATION;
}
// Select random message when entering LEVEL_START
if (nou_estat == EstatStage::LEVEL_START) {
size_t index = static_cast<size_t>(std::rand()) % Constants::MISSATGES_LEVEL_START.size();
missatge_level_start_actual_ = Constants::MISSATGES_LEVEL_START[index];
}
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,61 @@
// 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_; }
const std::string& get_missatge_level_start() const { return missatge_level_start_actual_; }
// 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)
std::string missatge_level_start_actual_; // Missatge seleccionat per al level actual
// 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

View File

@@ -0,0 +1,20 @@
# Makefile per a pack_resources
# © 2025 Orni Attack
CXX = clang++
CXXFLAGS = -std=c++20 -Wall -Wextra -I../../source
TARGET = pack_resources
SOURCES = pack_resources.cpp \
../../source/core/resources/resource_pack.cpp
$(TARGET): $(SOURCES)
@echo "Compilant $(TARGET)..."
@$(CXX) $(CXXFLAGS) -o $(TARGET) $(SOURCES)
@echo "$(TARGET) compilat"
clean:
@rm -f $(TARGET)
@echo "✓ Netejat"
.PHONY: clean

View File

@@ -0,0 +1,92 @@
// pack_resources.cpp - Utilitat per crear paquets de recursos
// © 2025 Orni Attack
#include "../../source/core/resources/resource_pack.hpp"
#include <filesystem>
#include <iostream>
void print_usage(const char* program_name) {
std::cout << "Ús: " << program_name << " [opcions] [directori_entrada] [fitxer_sortida]\n";
std::cout << "\nOpcions:\n";
std::cout << " --list <fitxer> Llistar contingut d'un paquet\n";
std::cout << "\nExemples:\n";
std::cout << " " << program_name << " data resources.pack\n";
std::cout << " " << program_name << " --list resources.pack\n";
std::cout << "\nSi no s'especifiquen arguments, empaqueta 'data/' a 'resources.pack'\n";
}
int main(int argc, char* argv[]) {
std::string input_dir = "data";
std::string output_file = "resources.pack";
// Processar arguments
if (argc == 2 && std::string(argv[1]) == "--help") {
print_usage(argv[0]);
return 0;
}
// Mode --list
if (argc == 3 && std::string(argv[1]) == "--list") {
Resource::Pack pack;
if (!pack.loadPack(argv[2])) {
std::cerr << "ERROR: No es pot carregar " << argv[2] << "\n";
return 1;
}
std::cout << "Contingut de " << argv[2] << ":\n";
auto resources = pack.getResourceList();
std::cout << "Total: " << resources.size() << " recursos\n\n";
for (const auto& name : resources) {
std::cout << " " << name << "\n";
}
return 0;
}
// Mode empaquetar
if (argc >= 3) {
input_dir = argv[1];
output_file = argv[2];
}
// Verificar que existeix el directori
if (!std::filesystem::exists(input_dir)) {
std::cerr << "ERROR: Directori no trobat: " << input_dir << "\n";
return 1;
}
if (!std::filesystem::is_directory(input_dir)) {
std::cerr << "ERROR: " << input_dir << " no és un directori\n";
return 1;
}
// Crear paquet
std::cout << "Creant paquet de recursos...\n";
std::cout << " Entrada: " << input_dir << "\n";
std::cout << " Sortida: " << output_file << "\n\n";
Resource::Pack pack;
if (!pack.addDirectory(input_dir)) {
std::cerr << "ERROR: No s'ha pogut afegir el directori\n";
return 1;
}
if (!pack.savePack(output_file)) {
std::cerr << "ERROR: No s'ha pogut guardar el paquet\n";
return 1;
}
// Resum
auto resources = pack.getResourceList();
std::cout << "\n";
std::cout << "✓ Paquet creat amb èxit!\n";
std::cout << " Recursos: " << resources.size() << "\n";
// Mostrar mida del fitxer
auto file_size = std::filesystem::file_size(output_file);
std::cout << " Mida: " << (file_size / 1024) << " KB\n";
return 0;
}