28 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
a3aeed4b7c afegides musiques
afegit control de brillo al starfield
2025-12-03 19:27:36 +01:00
3b0354da54 afegit titol al TITOL 2025-12-03 17:40:27 +01:00
622ccd22bc afegits diferents enemics 2025-12-03 13:47:31 +01:00
1441134aea afegida colisió nau-enemics i game over 2025-12-03 12:04:44 +01:00
0500dce7aa includes amb ruta absoluta 2025-12-03 11:19:23 +01:00
9f0dfc4e24 gitignore no ha deixat versionar cap fitxer de core
afegida gestió de ratolí
2025-12-03 09:42:45 +01:00
aa66dd41c1 ja renderitza a la resolució de la finestra 2025-12-03 08:23:42 +01:00
69fb5f3cc1 Migrate to CMake-based build with packaging
Major build system refactoring:

**CMake (build authority)**:
- Auto-discovers .cpp files (GLOB_RECURSE in source/core/ and source/game/)
- No manual file list maintenance needed
- Excludes source/legacy/ automatically
- Generates build/project.h from template

**Makefile (simplified wrapper)**:
- Delegates compilation to CMake (make → cmake --build build)
- Contains 5 release packaging targets:
  * macos_release: .app bundle + .dmg (Apple Silicon)
  * linux_release: .tar.gz
  * windows_release: .zip with .exe + DLLs
  * windows_cross: cross-compile from Linux/macOS
  * rpi_release: ARM64 cross-compile
- Complex packaging logic preserved (code signing, symlinks, DMG creation)

**Benefits**:
- Add new .cpp file → automatically compiled (no manual updates)
- Single source of truth in CMakeLists.txt (no duplication)
- IDE-friendly (VSCode, CLion, etc.)
- Complete packaging support (5 platforms)

**Files changed**:
- CMakeLists.txt: GLOB_RECURSE replaces 23-file hardcoded list
- Makefile: Simplified compilation + added 5 release targets (~220 lines)
- CLAUDE.md: Updated build system documentation
- escena_titol.cpp: Fixed include path (build/project.h → project.h)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 07:36:53 +01:00
99 changed files with 7809 additions and 479 deletions

16
.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
@@ -58,9 +68,9 @@ _deps/
*.ilk
# Core dumps
core
core.*
*.core
# core
# core.*
# *.core
# macOS
.DS_Store

View File

@@ -12,59 +12,98 @@ This is **Orni Attack**, an **Asteroids-style game** originally written in **Tur
## Build System
Based on `/home/sergio/gitea/pollo` project structure.
Based on `/home/sergio/gitea/pollo` project structure, now with **CMake as build authority** and **automatic file discovery**.
### Build Commands
### Basic Build Commands
```bash
# Clean + compile
make clean && make
make # Compile (delegates to CMake)
make debug # Debug build
make clean # Clean artifacts
./orni # Run
```
# Run
./orni
### Release Packaging
# Individual targets
make linux # Linux build
make macos # macOS build
make windows # Windows build (MinGW)
```bash
make macos_release # macOS .app bundle + .dmg (Apple Silicon)
make linux_release # Linux .tar.gz
make windows_release # Windows .zip (requires MinGW on Windows)
make windows_cross # Cross-compile Windows from Linux/macOS
make rpi_release # Raspberry Pi ARM64 cross-compile
```
### Build Files
- **CMakeLists.txt** - CMake configuration (C++20, SDL3, project metadata)
- **Makefile** - Cross-platform wrapper, extracts project info from CMakeLists.txt
- **CMakeLists.txt** - CMake configuration (C++20, SDL3, auto-discovers .cpp files)
- **Makefile** - Wrapper for compilation + complex packaging recipes
- **source/project.h.in** - Template for auto-generated project.h
- **build/project.h** - Auto-generated (by CMake) with project constants
- **release/** - Platform-specific resources (icons, .rc, .plist)
- **release/** - Platform-specific resources (icons, .rc, .plist, frameworks, DLLs)
### Architecture: Hybrid CMake + Makefile
**CMake handles**: Compilation (simple, standard, IDE-friendly)
- Auto-discovers all `.cpp` files in `source/core/` and `source/game/`
- Excludes `source/legacy/` automatically
- Generates `build/project.h` from template
- Links SDL3
**Makefile handles**: Packaging (complex bash scripts)
- Delegates compilation to CMake (`make``cmake --build build`)
- Contains 5 release packaging targets (macOS, Linux, Windows, RPI, Windows-cross)
- Includes: code signing, framework symlinks, DMG creation, cross-compilation
### Project Metadata System
**Auto-generation with CMake**:
**Single source of truth** in `CMakeLists.txt`:
CMake generates `build/project.h` from `source/project.h.in` template on every compilation:
```cmake
project(orni VERSION 0.3.0)
set(PROJECT_LONG_NAME "Orni Attack")
set(PROJECT_COPYRIGHT "© 1999 Visente i Sergi, 2025 Port")
```
**Auto-generated** `build/project.h`:
```cpp
// build/project.h (generated automatically)
namespace Project {
constexpr const char* NAME = "orni"; // From project(orni ...)
constexpr const char* LONG_NAME = "Orni Attack"; // From PROJECT_LONG_NAME
constexpr const char* VERSION = "0.1.0"; // From VERSION
constexpr const char* COPYRIGHT = "© 1999..."; // From PROJECT_COPYRIGHT
constexpr const char* GIT_HASH = "abc1234"; // From git rev-parse
constexpr const char* NAME = "orni";
constexpr const char* LONG_NAME = "Orni Attack";
constexpr const char* VERSION = "0.3.0";
constexpr const char* COPYRIGHT = "© 1999 Visente i Sergi, 2025 Port";
constexpr const char* GIT_HASH = "abc1234"; // From git rev-parse
}
```
**Window title format** (dynamic, in sdl_manager.cpp):
```cpp
std::format("{} v{} ({})",
Project::LONG_NAME, // "Orni Attack"
Project::VERSION, // "0.1.0"
Project::COPYRIGHT) // "© 1999 Visente i Sergi, 2025 Port"
**Window title** (dynamic): `Orni Attack v0.3.0 (© 1999 Visente i Sergi, 2025 Port)`
### File Discovery
**Automatic** - no manual maintenance needed:
```cmake
# CMakeLists.txt automatically finds:
file(GLOB_RECURSE CORE_SOURCES "source/core/*.cpp")
file(GLOB_RECURSE GAME_SOURCES "source/game/*.cpp")
# + source/main.cpp
# - source/legacy/* (excluded)
```
Result: `Orni Attack v0.1.0 (© 1999 Visente i Sergi, 2025 Port)`
**When you create a new file** like `source/game/entities/asteroide.cpp`:
1. Just create it in the appropriate directory
2. Run `make`
3. CMake automatically detects and compiles it
**Single source of truth**: All project info in CMakeLists.txt, no hardcoded strings.
**No need to edit** Makefile or CMakeLists.txt!
### Cross-Platform Notes
- **macOS**: Requires `create-dmg` (auto-installed via Homebrew)
- **Windows**: Compile natively with MinGW or use `make windows_cross` on Linux/macOS
- **Windows cross**: Requires `x86_64-w64-mingw32-g++` toolchain
- **RPI cross**: Requires `aarch64-linux-gnu-g++` toolchain
- **Frameworks**: macOS release includes SDL3.xcframework with symlink recreation
## Architecture

View File

@@ -1,7 +1,7 @@
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(orni VERSION 0.3.0)
project(orni VERSION 0.5.0)
# Info del proyecto
set(PROJECT_LONG_NAME "Orni Attack")
@@ -31,33 +31,23 @@ endif()
# Configurar archivo de versión
configure_file(${CMAKE_SOURCE_DIR}/source/project.h.in ${CMAKE_BINARY_DIR}/project.h @ONLY)
# --- LISTA DE FUENTES ---
# --- LISTA DE FUENTES (AUTO-DESCUBRIMIENTO) ---
# Buscar automáticamente todos los archivos .cpp en core/, game/ y main.cpp
file(GLOB_RECURSE CORE_SOURCES "${CMAKE_SOURCE_DIR}/source/core/*.cpp")
file(GLOB_RECURSE GAME_SOURCES "${CMAKE_SOURCE_DIR}/source/game/*.cpp")
set(APP_SOURCES
${CORE_SOURCES}
${GAME_SOURCES}
source/main.cpp
source/core/system/director.cpp
source/core/system/global_events.cpp
source/core/rendering/sdl_manager.cpp
source/core/rendering/line_renderer.cpp
source/core/rendering/color_oscillator.cpp
source/core/rendering/polygon_renderer.cpp
source/core/rendering/primitives.cpp
source/core/rendering/shape_renderer.cpp
source/core/graphics/shape.cpp
source/core/graphics/shape_loader.cpp
source/core/graphics/vector_text.cpp
source/core/graphics/starfield.cpp
source/core/audio/audio.cpp
source/core/audio/audio_cache.cpp
source/game/options.cpp
source/game/escenes/escena_logo.cpp
source/game/escenes/escena_titol.cpp
source/game/escenes/escena_joc.cpp
source/game/entities/nau.cpp
source/game/entities/bala.cpp
source/game/entities/enemic.cpp
source/game/effects/debris_manager.cpp
)
# Excluir archivos legacy (código Pascal de referencia)
list(FILTER APP_SOURCES EXCLUDE REGEX ".*/legacy/.*")
# Log de archivos encontrados (útil para debug)
list(LENGTH APP_SOURCES APP_SOURCES_COUNT)
message(STATUS "Archivos .cpp encontrados: ${APP_SOURCES_COUNT}")
# Configuración de SDL3
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
@@ -83,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)

415
Makefile
View File

@@ -20,6 +20,15 @@ TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
RELEASE_FOLDER := $(TARGET_NAME)_release
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
# Release file names
RAW_VERSION := $(shell echo $(VERSION) | sed 's/^v//')
WINDOWS_RELEASE := $(TARGET_NAME)-$(VERSION)-windows-x64.zip
MACOS_ARM_RELEASE := $(TARGET_NAME)-$(VERSION)-macos-arm64.dmg
MACOS_INTEL_RELEASE := $(TARGET_NAME)-$(VERSION)-macos-x64.dmg
LINUX_RELEASE := $(TARGET_NAME)-$(VERSION)-linux-x64.tar.gz
RPI_RELEASE := $(TARGET_NAME)-$(VERSION)-rpi-arm64.tar.gz
APP_NAME := $(LONG_NAME)
# ==============================================================================
# VERSION
# ==============================================================================
@@ -32,152 +41,307 @@ endif
# ==============================================================================
# SOURCE FILES
# ==============================================================================
APP_SOURCES := \
source/main.cpp \
source/core/system/director.cpp \
source/core/system/global_events.cpp \
source/core/audio/audio.cpp \
source/core/audio/audio_cache.cpp \
source/core/rendering/sdl_manager.cpp \
source/core/rendering/line_renderer.cpp \
source/core/rendering/color_oscillator.cpp \
source/core/rendering/polygon_renderer.cpp \
source/core/rendering/primitives.cpp \
source/core/rendering/shape_renderer.cpp \
source/core/graphics/shape.cpp \
source/core/graphics/shape_loader.cpp \
source/core/graphics/vector_text.cpp \
source/core/graphics/starfield.cpp \
source/game/options.cpp \
source/game/escenes/escena_logo.cpp \
source/game/escenes/escena_titol.cpp \
source/game/escenes/escena_joc.cpp \
source/game/entities/nau.cpp \
source/game/entities/bala.cpp \
source/game/entities/enemic.cpp \
source/game/effects/debris_manager.cpp
# Note: Source files are now auto-discovered by CMake using GLOB_RECURSE
# No need to maintain this list manually anymore!
# ==============================================================================
# INCLUDES
# PLATFORM-SPECIFIC UTILITIES
# ==============================================================================
INCLUDES := -Isource -Ibuild
# ==============================================================================
# COMPILER FLAGS (OS-specific)
# ==============================================================================
CPP_STANDARD := c++20
ifeq ($(OS),Windows_NT)
# Windows (MinGW)
FixPath = $(subst /,\\,$1)
CXX := g++
CXXFLAGS := -std=$(CPP_STANDARD) -Wall -O2 -ffunction-sections -fdata-sections \
-Wl,--gc-sections -static-libstdc++ -static-libgcc \
-Wl,-subsystem,windows -DWINDOWS_BUILD
CXXFLAGS_DEBUG := -std=$(CPP_STANDARD) -Wall -g -D_DEBUG -DWINDOWS_BUILD
LDFLAGS := -lmingw32 -lSDL3
WINDRES := windres
RESOURCE_FILE := release/orni.res
RM := del /Q
RMFILE := del /Q
RMDIR := rmdir /S /Q
MKDIR := mkdir
else
# Unix-like systems (Linux/macOS)
FixPath = $1
CXX := g++
CXXFLAGS := -std=$(CPP_STANDARD) -Wall -O2
CXXFLAGS_DEBUG := -std=$(CPP_STANDARD) -Wall -g -D_DEBUG
LDFLAGS := -lSDL3
RMFILE := rm -f
RMDIR := rm -rf
MKDIR := mkdir -p
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
CXXFLAGS += -DLINUX_BUILD
CXXFLAGS_DEBUG += -DLINUX_BUILD
endif
ifeq ($(UNAME_S),Darwin)
CXXFLAGS += -arch arm64 -Wno-deprecated -DMACOS_BUILD
CXXFLAGS_DEBUG += -arch arm64 -Wno-deprecated -DMACOS_BUILD
endif
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
# ==============================================================================
.PHONY: all clean debug help backup
# Default target
all: $(TARGET_FILE)
# ==============================================================================
# BUILD TARGETS (delegate to CMake)
# ==============================================================================
# Generate build/project.h from template
.PHONY: generate_project_h
generate_project_h:
@$(MKDIR) build 2>/dev/null || true
@echo "Generating build/project.h..."
ifeq ($(OS),Windows_NT)
@powershell -Command \
"$$name = '$(TARGET_NAME)'; \
$$long = '$(LONG_NAME)'; \
$$ver = '$(VERSION)'.TrimStart('v'); \
$$copy = (Get-Content CMakeLists.txt | Where-Object {$$_ -match 'PROJECT_COPYRIGHT'} | ForEach-Object {if ($$_ -match '\"(.+)\"') {$$matches[1]}}); \
$$hash = try {git rev-parse --short=7 HEAD 2>$$null} catch {'unknown'}; \
(Get-Content source/project.h.in) -replace '@PROJECT_NAME@', $$name -replace '@PROJECT_LONG_NAME@', $$long -replace '@PROJECT_VERSION@', $$ver -replace '@PROJECT_COPYRIGHT@', $$copy -replace '@GIT_HASH@', $$hash | Set-Content build/project.h"
else
@PROJECT_COPYRIGHT=$$(grep 'PROJECT_COPYRIGHT' CMakeLists.txt | sed 's/.*"\(.*\)".*/\1/'); \
PROJECT_VERSION=$$(echo $(VERSION) | sed 's/^v//'); \
GIT_HASH=$$(git rev-parse --short=7 HEAD 2>/dev/null || echo "unknown"); \
sed -e "s/@PROJECT_NAME@/$(TARGET_NAME)/g" \
-e "s/@PROJECT_LONG_NAME@/$(LONG_NAME)/g" \
-e "s/@PROJECT_VERSION@/$${PROJECT_VERSION}/g" \
-e "s/@PROJECT_COPYRIGHT@/$${PROJECT_COPYRIGHT}/g" \
-e "s/@GIT_HASH@/$${GIT_HASH}/g" \
source/project.h.in > build/project.h
endif
@echo "build/project.h generated successfully"
# Default target: build with CMake + resources
all: resources.pack $(TARGET_FILE)
# Compile executable
$(TARGET_FILE): generate_project_h $(APP_SOURCES)
ifeq ($(OS),Windows_NT)
@if not exist build $(MKDIR) build
@if not exist release\\orni.res $(WINDRES) release\\orni.rc -O coff -o release\\orni.res
$(CXX) $(CXXFLAGS) $(INCLUDES) $(APP_SOURCES) $(RESOURCE_FILE) $(LDFLAGS) -o $(TARGET_FILE).exe
else
@$(MKDIR) build
$(CXX) $(CXXFLAGS) $(INCLUDES) $(APP_SOURCES) $(LDFLAGS) -o $(TARGET_FILE)
endif
@echo Compilation successful: $(TARGET_FILE)
$(TARGET_FILE):
@cmake -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build
@echo "Build successful: $(TARGET_FILE)"
# Debug build
debug: generate_project_h $(APP_SOURCES)
ifeq ($(OS),Windows_NT)
@if not exist build $(MKDIR) build
$(CXX) $(CXXFLAGS_DEBUG) $(INCLUDES) $(APP_SOURCES) $(LDFLAGS) -o $(TARGET_FILE)_debug.exe
else
@$(MKDIR) build
$(CXX) $(CXXFLAGS_DEBUG) $(INCLUDES) $(APP_SOURCES) $(LDFLAGS) -o $(TARGET_FILE)_debug
endif
@echo Debug build successful: $(TARGET_FILE)_debug
debug: resources.pack
@cmake -B build -DCMAKE_BUILD_TYPE=Debug
@cmake --build build
@echo "Debug build successful: $(TARGET_FILE)"
# ==============================================================================
# RELEASE PACKAGING TARGETS
# ==============================================================================
# macOS Release (Apple Silicon)
.PHONY: macos_release
macos_release: pack_tool resources.pack
@echo "Creating macOS release - Version: $(VERSION)"
# Check/install create-dmg
@command -v create-dmg >/dev/null || (echo "Installing create-dmg..." && brew install create-dmg)
# Clean previous releases
@$(RMDIR) "$(RELEASE_FOLDER)" 2>/dev/null || true
@$(RMDIR) Frameworks 2>/dev/null || true
@$(RMFILE) "$(MACOS_ARM_RELEASE)" 2>/dev/null || true
# Create .app structure
@$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
@$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
@$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
@$(MKDIR) Frameworks
# 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
# Recreate framework symlinks (may be broken)
@cd Frameworks/SDL3.framework && rm -f SDL3 Headers Resources && \
ln -s Versions/Current/SDL3 SDL3 && \
ln -s Versions/Current/Headers Headers && \
ln -s Versions/Current/Resources Resources
@cd Frameworks/SDL3.framework/Versions && rm -f Current && ln -s A Current
@cd "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks/SDL3.framework" && rm -f SDL3 Headers Resources && \
ln -s Versions/Current/SDL3 SDL3 && \
ln -s Versions/Current/Headers Headers && \
ln -s Versions/Current/Resources Resources
@cd "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks/SDL3.framework/Versions" && rm -f Current && ln -s A Current
@cp release/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources/"
@cp release/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/"
@cp LICENSE "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: LICENSE not found"
@cp README.md "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: README.md not found"
# Update Info.plist version
@echo "Updating Info.plist with version $(RAW_VERSION)..."
@sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>$(RAW_VERSION)</string>|;}' \
"$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
@sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>$(RAW_VERSION)</string>|;}' \
"$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
# Compile for Apple Silicon using CMake
@cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64
@cmake --build build
@cp $(TARGET_FILE) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
# Code sign
@codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app" || echo "Warning: Code signing failed"
# Create DMG
@echo "Creating DMG for Apple Silicon..."
@create-dmg \
--volname "$(APP_NAME)" \
--window-pos 200 120 \
--window-size 720 300 \
--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)" \
"$(RELEASE_FOLDER)" || true
@echo "✓ macOS release created: $(MACOS_ARM_RELEASE)"
# Cleanup
@$(RMDIR) Frameworks
@$(RMDIR) "$(RELEASE_FOLDER)"
# Linux Release
.PHONY: linux_release
linux_release:
@echo "Creating Linux release - Version: $(VERSION)"
# Clean previous
@$(RMDIR) "$(RELEASE_FOLDER)"
@$(RMFILE) "$(LINUX_RELEASE)"
# Create folder
@$(MKDIR) "$(RELEASE_FOLDER)"
# Copy resources
@cp -r resources "$(RELEASE_FOLDER)/"
@cp LICENSE "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: LICENSE not found"
@cp README.md "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: README.md not found"
# Compile with CMake
@cmake -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build
@cp $(TARGET_FILE) "$(RELEASE_FILE)"
@strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded || strip "$(RELEASE_FILE)"
# Package
@tar -czf "$(LINUX_RELEASE)" -C "$(RELEASE_FOLDER)" .
@echo "✓ Linux release created: $(LINUX_RELEASE)"
# Cleanup
@$(RMDIR) "$(RELEASE_FOLDER)"
# Windows Release (requires MinGW on Windows or cross-compiler on Linux)
.PHONY: windows_release
windows_release:
@echo "Creating Windows release - Version: $(VERSION)"
@echo "Note: This target should be run on Windows with MinGW or use windows_cross on Linux"
# Clean previous
@$(RMDIR) "$(RELEASE_FOLDER)"
@$(RMFILE) "$(WINDOWS_RELEASE)"
# Create folder
@$(MKDIR) "$(RELEASE_FOLDER)"
# Copy resources
@cp -r resources "$(RELEASE_FOLDER)/"
@cp release/dll/*.dll "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: DLLs not found"
@cp LICENSE "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: LICENSE not found"
@cp README.md "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: README.md not found"
# Compile resource file
@windres release/$(TARGET_NAME).rc -O coff -o release/$(TARGET_NAME).res 2>/dev/null || echo "Warning: windres failed"
# Compile with CMake
@cmake -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build
@cp $(TARGET_FILE).exe "$(RELEASE_FILE).exe" || cp $(TARGET_FILE) "$(RELEASE_FILE).exe"
# Package
@cd "$(RELEASE_FOLDER)" && zip -r ../$(WINDOWS_RELEASE) *
@echo "✓ Windows release created: $(WINDOWS_RELEASE)"
# Cleanup
@$(RMDIR) "$(RELEASE_FOLDER)"
# Raspberry Pi Release (cross-compilation from Linux/macOS)
.PHONY: rpi_release
rpi_release:
@echo "Creating Raspberry Pi ARM64 release - Version: $(VERSION)"
@echo "Note: Requires aarch64-linux-gnu-g++ cross-compiler"
# Check for cross-compiler
@command -v aarch64-linux-gnu-g++ >/dev/null || (echo "Error: aarch64-linux-gnu-g++ not found" && exit 1)
# Clean previous
@$(RMDIR) "$(RELEASE_FOLDER)"
@$(RMFILE) "$(RPI_RELEASE)"
# Create folder
@$(MKDIR) "$(RELEASE_FOLDER)"
# Copy resources
@cp -r resources "$(RELEASE_FOLDER)/"
@cp LICENSE "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: LICENSE not found"
@cp README.md "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: README.md not found"
# Note: Cross-compilation with CMake is complex, would need toolchain file
@echo "Warning: RPI cross-compilation requires manual setup with CMake toolchain file"
@echo "Falling back to direct g++ compilation..."
@aarch64-linux-gnu-g++ -std=c++20 -Wall -O2 -DLINUX_BUILD -DRPI_BUILD \
-Isource -Ibuild \
$$(find source/core source/game -name "*.cpp") source/main.cpp \
-lSDL3 -o "$(RELEASE_FILE)" || echo "Error: Compilation failed"
@aarch64-linux-gnu-strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded || true
# Package
@tar -czf "$(RPI_RELEASE)" -C "$(RELEASE_FOLDER)" .
@echo "✓ Raspberry Pi release created: $(RPI_RELEASE)"
# Cleanup
@$(RMDIR) "$(RELEASE_FOLDER)"
# Windows Cross-compilation (from Linux/macOS)
.PHONY: windows_cross
windows_cross:
@echo "Cross-compiling for Windows from $(UNAME_S) - Version: $(VERSION)"
# Check for cross-compiler
@command -v x86_64-w64-mingw32-g++ >/dev/null || (echo "Error: x86_64-w64-mingw32-g++ not found" && exit 1)
# Clean previous
@$(RMDIR) "$(RELEASE_FOLDER)"
@$(RMFILE) "$(WINDOWS_RELEASE)"
# Create folder
@$(MKDIR) "$(RELEASE_FOLDER)"
# Copy resources
@cp -r resources "$(RELEASE_FOLDER)/"
@cp release/dll/*.dll "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: DLLs not found in release/dll/"
@cp LICENSE "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: LICENSE not found"
@cp README.md "$(RELEASE_FOLDER)/" 2>/dev/null || echo "Warning: README.md not found"
# Compile resource file
@x86_64-w64-mingw32-windres release/$(TARGET_NAME).rc -O coff -o release/$(TARGET_NAME).res 2>/dev/null || echo "Warning: windres failed"
# Cross-compile
@echo "Compiling with MinGW cross-compiler..."
@x86_64-w64-mingw32-g++ -std=c++20 -Wall -O2 -DWINDOWS_BUILD -DRELEASE_BUILD \
-static-libstdc++ -static-libgcc -Wl,-subsystem,windows \
-Isource -Ibuild \
$$(find source/core source/game -name "*.cpp") source/main.cpp \
release/$(TARGET_NAME).res \
-lmingw32 -lSDL3 -o "$(RELEASE_FILE).exe" || echo "Error: Compilation failed"
@x86_64-w64-mingw32-strip "$(RELEASE_FILE).exe" || true
# Package
@cd "$(RELEASE_FOLDER)" && zip -r ../$(WINDOWS_RELEASE) *
@echo "✓ Windows cross-compiled release created: $(WINDOWS_RELEASE)"
# Cleanup
@$(RMDIR) "$(RELEASE_FOLDER)"
# Clean build artifacts
clean:
ifeq ($(OS),Windows_NT)
@if exist $(call FixPath,$(TARGET_FILE).exe) $(RM) $(call FixPath,$(TARGET_FILE).exe)
@if exist $(call FixPath,$(TARGET_FILE)_debug.exe) $(RM) $(call FixPath,$(TARGET_FILE)_debug.exe)
@if exist $(call FixPath,$(TARGET_FILE).exe) $(RMFILE) $(call FixPath,$(TARGET_FILE).exe)
@if exist $(call FixPath,$(TARGET_FILE)_debug.exe) $(RMFILE) $(call FixPath,$(TARGET_FILE)_debug.exe)
@if exist build $(RMDIR) build
@if exist $(RELEASE_FOLDER) $(RMDIR) $(RELEASE_FOLDER)
else
@$(RMFILE) $(TARGET_FILE)
@$(RMFILE) $(TARGET_FILE)_debug
@$(RMDIR) build
@$(RMDIR) $(RELEASE_FOLDER)
@$(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
@echo "Clean complete"
# Backup to remote server
backup:
@echo "Backing up project to maverick:/home/sergio/git-backup/asteroids..."
@echo "Backing up project to maverick:/home/sergio/git-backup/orni..."
rsync -a --delete \
--exclude='build/' \
--exclude='*.o' \
@@ -185,20 +349,31 @@ backup:
--exclude='orni' \
--exclude='orni_debug' \
--exclude='*_release/' \
$(DIR_ROOT) maverick:/home/sergio/git-backup/asteroids/
$(DIR_ROOT) maverick:/home/sergio/git-backup/orni/
@echo "Backup completed successfully"
# Help target
help:
@echo "Available targets:"
@echo " all - Build the game (default)"
@echo " debug - Build with debug symbols"
@echo " clean - Remove build artifacts"
@echo " backup - Backup project to remote server"
@echo " help - Show this help message"
@echo ""
@echo "Build:"
@echo " all - Build the game (default, delegates to CMake)"
@echo " debug - Build with debug symbols"
@echo " clean - Remove build artifacts and release packages"
@echo ""
@echo "Release Packaging:"
@echo " macos_release - Create macOS .app bundle + .dmg (Apple Silicon)"
@echo " linux_release - Create Linux .tar.gz"
@echo " windows_release - Create Windows .zip (requires MinGW on Windows)"
@echo " windows_cross - Cross-compile Windows from Linux/macOS (requires MinGW)"
@echo " rpi_release - Cross-compile for Raspberry Pi ARM64"
@echo ""
@echo "Other:"
@echo " backup - Backup project to remote server"
@echo " help - Show this help message"
@echo ""
@echo "Current configuration:"
@echo " Project: $(LONG_NAME)"
@echo " Target: $(TARGET_NAME)"
@echo " Version: $(VERSION)"
@echo " Platform: $(UNAME_S)"
@echo " C++ Standard: $(CPP_STANDARD)"

BIN
data/music/game.ogg Normal file

Binary file not shown.

BIN
data/music/title.ogg Normal file

Binary file not shown.

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

View File

@@ -0,0 +1,30 @@
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
# © 2025 Port a C++20 amb SDL3
name: enemy_pinwheel
scale: 1.0
center: 0, 0
# Molinillo: 4 triangles, un en cada quadrant
# Cada triangle comparteix el centre (0,0) i té:
# - Un vèrtex en un eix (±20, 0) o (0, ±20)
# - Un vèrtex en la diagonal del quadrant (±14.14, ±14.14)
# - El tercer vèrtex al centre (0,0)
#
# Geometria:
# Triangle 1 (quadrant superior-dret): centre → eix dret → diagonal
# Triangle 2 (quadrant superior-esq): centre → eix superior → diagonal
# Triangle 3 (quadrant inferior-esq): centre → eix esquerre → diagonal
# Triangle 4 (quadrant inferior-dret): centre → eix inferior → diagonal
# Triangle 1: quadrant superior-dret
polyline: 0,0 20,0 14.14,-14.14 0,0
# Triangle 2: quadrant superior-esquerre
polyline: 0,0 0,-20 -14.14,-14.14 0,0
# Triangle 3: quadrant inferior-esquerre
polyline: 0,0 -20,0 -14.14,14.14 0,0
# Triangle 4: quadrant inferior-dret
polyline: 0,0 0,20 14.14,14.14 0,0

View File

@@ -0,0 +1,19 @@
# enemy_square.shp - ORNI enemic (quadrat regular)
# © 2025 Port a C++20 amb SDL3
name: enemy_square
scale: 1.0
center: 0, 0
# Quadrat regular radi=20 (circumscrit)
# 4 punts equidistants al voltant d'un cercle (90° entre ells)
# Començant a angle=-90° (amunt), rotant sentit horari
#
# Angles: -90°, 0°, 90°, 180°
# Conversió polar→cartesià (SDL: Y creix cap avall):
# angle=-90°: (0.00, -20.00)
# angle=0°: (20.00, 0.00)
# angle=90°: (0.00, 20.00)
# angle=180°: (-20.00, 0.00)
polyline: 0,-20 20,0 0,20 -20,0 0,-20

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

View File

@@ -0,0 +1,10 @@
# letra_a.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_a
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,100.00 0.00,75.00 37.50,0.00 100.00,0.00 137.50,75.00 137.50,100.00 100.00,100.00 100.00,87.50 37.50,87.50 37.50,100.00 0.00,100.00
polyline: 62.50,25.00 50.00,50.00 50.00,62.50 87.50,62.50 87.50,50.00 75.00,25.00 62.50,25.00

View File

@@ -0,0 +1,9 @@
# letra_c.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_c
scale: 1.0
center: 68.75, 50.00
polyline: 12.50,100.00 0.00,87.50 0.00,12.50 12.50,0.00 125.00,0.00 137.50,12.50 137.50,37.50 100.00,37.50 100.00,25.00 37.50,25.00 37.50,75.00 100.00,75.00 100.00,62.50 137.50,62.50 137.50,87.50 125.00,100.00 12.50,100.00

View File

@@ -0,0 +1,10 @@
# letra_exclamacion.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 37.51 x 100.00 px
name: letra_exclamacion
scale: 1.0
center: 18.75, 50.00
polyline: 0.00,62.50 0.00,0.00 37.51,0.00 37.51,62.50 0.00,62.50
polyline: 0.00,100.00 0.00,75.00 37.51,75.00 37.51,100.00 0.00,100.00

View File

@@ -0,0 +1,9 @@
# letra_i.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 37.50 x 100.00 px
name: letra_i
scale: 1.0
center: 18.75, 50.00
polyline: 0.00,0.00 37.50,0.00 37.50,100.00 0.00,100.00 0.00,0.00

View File

@@ -0,0 +1,9 @@
# letra_k.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_k
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,100.00 0.00,0.00 37.50,0.00 37.50,37.50 50.00,37.50 100.00,0.00 137.50,0.00 137.50,25.00 87.06,50.00 137.50,75.00 137.50,100.00 100.00,100.00 50.00,62.50 37.50,62.50 37.50,100.00 0.00,100.00

View File

@@ -0,0 +1,9 @@
# letra_n.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_n
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,100.00 0.00,0.00 50.00,0.00 100.00,50.00 100.00,0.00 137.50,0.00 137.50,100.00 87.50,100.00 37.50,50.00 37.50,100.00 0.00,100.00

View File

@@ -0,0 +1,10 @@
# letra_o.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_o
scale: 1.0
center: 68.75, 50.00
polyline: 12.50,100.00 0.00,87.50 0.00,12.50 12.50,0.00 125.00,0.00 137.50,12.50 137.50,87.50 125.00,100.00 12.50,100.00
polyline: 100.00,25.00 37.50,25.00 37.50,75.00 100.00,75.00 100.00,25.00

View File

@@ -0,0 +1,10 @@
# letra_r.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_r
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,100.00 0.00,0.00 125.00,0.00 137.50,12.50 137.50,62.50 125.00,62.50 137.50,75.00 137.50,100.00 100.00,100.00 100.00,75.00 37.50,75.00 37.50,100.00 0.00,100.00
polyline: 37.50,50.00 100.00,50.00 100.00,25.00 37.50,25.00 37.50,50.00

View File

@@ -0,0 +1,9 @@
# letra_t.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_t
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,25.00 0.00,0.00 137.50,0.00 137.50,25.00 87.50,25.00 87.50,100.00 50.00,100.00 50.00,25.00 0.00,25.00

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.

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 7875 4016" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.792849,0,0,0.792849,84.327,350.707)">
<path d="M896,1693L896,1531.23L1219.53,1531.23L1219.53,560.632L1543.07,560.632L1543.07,1531.23L1381.3,1531.23L1381.3,1693L896,1693Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M2028.37,1369.47L2028.37,1693L1704.83,1693L1704.83,722.399L1866.6,722.399L1866.6,560.632L2351.9,560.632L2351.9,722.399L2513.67,722.399L2513.67,1693L2190.14,1693L2190.14,1369.47L2028.37,1369.47ZM2028.37,722.399L2028.37,1207.7L2190.14,1207.7L2190.14,722.399L2028.37,722.399Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<rect x="2675.44" y="560.632" width="323.534" height="1132.37" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M3160.74,560.632L3484.27,560.632L3484.27,1531.23L3807.8,1531.23L3807.8,1693L3160.74,1693L3160.74,560.632Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M4131.34,560.632L4616.64,560.632L4616.64,722.399L4293.1,722.399L4293.1,1531.23L4454.87,1531.23L4454.87,1045.93L4778.4,1045.93L4778.4,1693L4131.34,1693L4131.34,1531.23L3969.57,1531.23L3969.57,722.399L4131.34,722.399L4131.34,560.632Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M5263.71,1369.47L5263.71,1693L4940.17,1693L4940.17,722.399L5101.94,722.399L5101.94,560.632L5587.24,560.632L5587.24,722.399L5749.01,722.399L5749.01,1693L5425.47,1693L5425.47,1369.47L5263.71,1369.47ZM5263.71,722.399L5263.71,1207.7L5425.47,1207.7L5425.47,722.399L5263.71,722.399Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M6719.61,1207.7L6557.84,1207.7L6557.84,1369.47L6396.07,1369.47L6396.07,1207.7L6234.31,1207.7L6234.31,1693L5910.77,1693L5910.77,560.632L6072.54,560.632L6072.54,722.399L6234.31,722.399L6234.31,884.166L6396.07,884.166L6396.07,1045.93L6557.84,1045.93L6557.84,884.166L6719.61,884.166L6719.61,722.399L6881.37,722.399L6881.37,560.632L7043.14,560.632L7043.14,1693L6719.61,1693L6719.61,1207.7Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M7851.98,884.166L7851.98,1045.93L7528.44,1045.93L7528.44,1531.23L8013.74,1531.23L8013.74,1693L7204.91,1693L7204.91,560.632L8013.74,560.632L8013.74,722.399L7528.44,722.399L7528.44,884.166L7851.98,884.166Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M8175.51,1531.23L8499.04,1531.23L8499.04,1207.7L8337.28,1207.7L8337.28,1045.93L8175.51,1045.93L8175.51,722.399L8337.28,722.399L8337.28,560.632L8822.58,560.632L8822.58,722.399L8499.04,722.399L8499.04,1045.93L8660.81,1045.93L8660.81,1207.7L8822.58,1207.7L8822.58,1531.23L8660.81,1531.23L8660.81,1693L8175.51,1693L8175.51,1531.23Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

183
source/core/audio/audio.cpp Normal file
View File

@@ -0,0 +1,183 @@
#include "audio.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
#include <algorithm> // Para clamp
#include <iostream> // Para std::cout
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp)
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h"
// clang-format on
#include "core/audio/audio_cache.hpp" // Para AudioCache
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions
// Singleton
Audio* Audio::instance = nullptr;
// Inicializa la instancia única del singleton
void Audio::init() { Audio::instance = new Audio(); }
// Libera la instancia
void Audio::destroy() { delete Audio::instance; }
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance; }
// Constructor
Audio::Audio() { initSDLAudio(); }
// Destructor
Audio::~Audio() {
JA_Quit();
}
// Método principal
void Audio::update() {
JA_Update();
}
// Reproduce la música
void Audio::playMusic(const std::string& name, const int loop) {
bool new_loop = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
return;
}
// Intentar obtener recurso; si falla, no tocar estado
auto* resource = AudioCache::getMusic(name);
if (resource == nullptr) {
// manejo de error opcional
return;
}
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
if (music_.state == MusicState::PLAYING) {
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
}
// Llamada al motor para reproducir la nueva pista
JA_PlayMusic(resource, loop);
// Actualizar estado y metadatos después de iniciar con éxito
music_.name = name;
music_.loop = new_loop;
music_.state = MusicState::PLAYING;
}
// Pausa la música
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic();
music_.state = MusicState::PAUSED;
}
}
// Continua la música pausada
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic();
music_.state = MusicState::PLAYING;
}
}
// Detiene la música
void Audio::stopMusic() {
if (music_enabled_) {
JA_StopMusic();
music_.state = MusicState::STOPPED;
}
}
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(AudioCache::getSound(name), 0, static_cast<int>(group));
}
}
// Reproduce un sonido por puntero directo
void Audio::playSound(JA_Sound_t* sound, Group group) const {
if (sound_enabled_) {
JA_PlaySound(sound, 0, static_cast<int>(group));
}
}
// Detiene todos los sonidos
void Audio::stopAllSounds() const {
if (sound_enabled_) {
JA_StopChannel(-1);
}
}
// Realiza un fundido de salida de la música
void Audio::fadeOutMusic(int milliseconds) const {
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
JA_FadeOutMusic(milliseconds);
}
}
// Consulta directamente el estado real de la música en jailaudio
auto Audio::getRealMusicState() -> MusicState {
JA_Music_state ja_state = JA_GetMusicState();
switch (ja_state) {
case JA_MUSIC_PLAYING:
return MusicState::PLAYING;
case JA_MUSIC_PAUSED:
return MusicState::PAUSED;
case JA_MUSIC_STOPPED:
case JA_MUSIC_INVALID:
case JA_MUSIC_DISABLED:
default:
return MusicState::STOPPED;
}
}
// Establece el volumen de los sonidos
void Audio::setSoundVolume(float sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
}
// Establece el volumen de la música
void Audio::setMusicVolume(float music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
JA_SetMusicVolume(CONVERTED_VOLUME);
}
}
// Aplica la configuración
void Audio::applySettings() {
enable(Options::audio.enabled);
}
// Establecer estado general
void Audio::enable(bool value) {
enabled_ = value;
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
}
// Inicializa SDL Audio
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
std::cout << "\n** AUDIO SYSTEM **\n";
std::cout << "Audio system initialized successfully\n";
}
}

View File

@@ -0,0 +1,97 @@
#pragma once
#include <string> // Para string
#include <utility> // Para move
// --- Clase Audio: gestor de audio (singleton) ---
class Audio {
public:
// --- Enums ---
enum class Group : int {
ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz
};
enum class MusicState {
PLAYING, // Reproduciendo música
PAUSED, // Música pausada
STOPPED, // Música detenida
};
// --- Constantes ---
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
// --- Singleton ---
static void init(); // Inicializa el objeto Audio
static void destroy(); // Libera el objeto Audio
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
Audio(const Audio&) = delete; // Evitar copia
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
static void update(); // Actualización del sistema de audio
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
// --- Control de sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos
// --- Control de volumen ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(float volume) const; // Ajustar volumen de música
// --- Configuración general ---
void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
void applySettings(); // Aplica la configuración
// --- Configuración de sonidos ---
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
// --- Configuración de música ---
void enableMusic() { music_enabled_ = true; } // Habilitar música
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
// --- Consultas de estado ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
[[nodiscard]] static auto getRealMusicState() -> MusicState;
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
private:
// --- Tipos anidados ---
struct Music {
MusicState state{MusicState::STOPPED}; // Estado actual de la música
std::string name; // Última pista de música reproducida
bool loop{false}; // Indica si se reproduce en bucle
};
// --- Métodos ---
Audio(); // Constructor privado
~Audio(); // Destructor privado
void initSDLAudio(); // Inicializa SDL Audio
// --- Variables miembro ---
static Audio* instance; // Instancia única de Audio
Music music_; // Estado de la música
bool enabled_{true}; // Estado general del audio
bool sound_enabled_{true}; // Estado de los efectos de sonido
bool music_enabled_{true}; // Estado de la música
};

View File

@@ -0,0 +1,142 @@
// audio_cache.cpp - Implementació del caché de sons i música
// © 2025 Port a C++20 amb SDL3
#include "core/audio/audio_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include <iostream>
// Inicialització de variables estàtiques
std::unordered_map<std::string, JA_Sound_t*> AudioCache::sounds_;
std::unordered_map<std::string, JA_Music_t*> AudioCache::musics_;
std::string AudioCache::sounds_base_path_ = "data/sounds/";
std::string AudioCache::music_base_path_ = "data/music/";
JA_Sound_t* AudioCache::getSound(const std::string& name) {
// Cache hit
auto it = sounds_.find(name);
if (it != sounds_.end()) {
std::cout << "[AudioCache] Sound cache hit: " << name << std::endl;
return it->second;
}
// 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 decodificar " << normalized
<< std::endl;
return nullptr;
}
std::cout << "[AudioCache] Sound loaded: " << normalized << std::endl;
sounds_[name] = sound;
return sound;
}
JA_Music_t* AudioCache::getMusic(const std::string& name) {
// Cache hit
auto it = musics_.find(name);
if (it != musics_.end()) {
std::cout << "[AudioCache] Music cache hit: " << name << std::endl;
return it->second;
}
// 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 decodificar " << normalized
<< std::endl;
return nullptr;
}
std::cout << "[AudioCache] Music loaded: " << normalized << std::endl;
musics_[name] = music;
return music;
}
void AudioCache::clear() {
std::cout << "[AudioCache] Clearing cache (" << sounds_.size() << " sounds, "
<< musics_.size() << " music)" << std::endl;
// Liberar memoria de sonidos
for (auto& [name, sound] : sounds_) {
if (sound && sound->buffer) {
SDL_free(sound->buffer);
}
delete sound;
}
sounds_.clear();
// Liberar memoria de música
for (auto& [name, music] : musics_) {
if (music && music->buffer) {
SDL_free(music->buffer);
}
if (music && music->filename) {
free(music->filename);
}
delete music;
}
musics_.clear();
}
size_t AudioCache::getSoundCacheSize() { return sounds_.size(); }
size_t AudioCache::getMusicCacheSize() { return musics_.size(); }
std::string AudioCache::resolveSoundPath(const std::string& name) {
// Si es un path absoluto (comienza con '/'), usarlo directamente
if (!name.empty() && name[0] == '/') {
return name;
}
// Si ya contiene el prefix base_path, usarlo directamente
if (name.find(sounds_base_path_) == 0) {
return name;
}
// Caso contrario, añadir base_path
return sounds_base_path_ + name;
}
std::string AudioCache::resolveMusicPath(const std::string& name) {
// Si es un path absoluto (comienza con '/'), usarlo directamente
if (!name.empty() && name[0] == '/') {
return name;
}
// Si ya contiene el prefix base_path, usarlo directamente
if (name.find(music_base_path_) == 0) {
return name;
}
// Caso contrario, añadir base_path
return music_base_path_ + name;
}

View File

@@ -0,0 +1,42 @@
// audio_cache.hpp - Caché simplificado de sonidos y música
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <string>
#include <unordered_map>
#include "core/audio/jail_audio.hpp"
// Caché estático de sonidos y música
// Patrón inspirado en Graphics::ShapeLoader
class AudioCache {
public:
// No instanciable (todo estático)
AudioCache() = delete;
// Obtener sonido (carga bajo demanda)
// Retorna puntero (nullptr si error)
static JA_Sound_t* getSound(const std::string& name);
// Obtener música (carga bajo demanda)
// Retorna puntero (nullptr si error)
static JA_Music_t* getMusic(const std::string& name);
// Limpiar caché (útil para debug/recarga)
static void clear();
// Estadísticas (debug)
static size_t getSoundCacheSize();
static size_t getMusicCacheSize();
private:
static std::unordered_map<std::string, JA_Sound_t*> sounds_;
static std::unordered_map<std::string, JA_Music_t*> musics_;
static std::string sounds_base_path_; // "data/sounds/"
static std::string music_base_path_; // "data/music/"
// Helpers privados
static std::string resolveSoundPath(const std::string& name);
static std::string resolveMusicPath(const std::string& name);
};

View File

@@ -0,0 +1,482 @@
#pragma once
// --- Includes ---
#include <SDL3/SDL.h>
#include <stdint.h> // Para uint32_t, uint8_t
#include <stdio.h> // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdlib.h> // Para free, malloc
#include <string.h> // Para strcpy, strlen
#define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory
// --- Public Enums ---
enum JA_Channel_state { JA_CHANNEL_INVALID,
JA_CHANNEL_FREE,
JA_CHANNEL_PLAYING,
JA_CHANNEL_PAUSED,
JA_SOUND_DISABLED };
enum JA_Music_state { JA_MUSIC_INVALID,
JA_MUSIC_PLAYING,
JA_MUSIC_PAUSED,
JA_MUSIC_STOPPED,
JA_MUSIC_DISABLED };
// --- Struct Definitions ---
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
#define JA_MAX_GROUPS 2
struct JA_Sound_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{NULL};
};
struct JA_Channel_t {
JA_Sound_t* sound{nullptr};
int pos{0};
int times{0};
int group{0};
SDL_AudioStream* stream{nullptr};
JA_Channel_state state{JA_CHANNEL_FREE};
};
struct JA_Music_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{nullptr};
char* filename{nullptr};
int pos{0};
int times{0};
SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID};
};
// --- Internal Global State ---
// Marcado 'inline' (C++17) para asegurar una única instancia.
inline JA_Music_t* current_music{nullptr};
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
inline float JA_musicVolume{1.0f};
inline float JA_soundVolume[JA_MAX_GROUPS];
inline bool JA_musicEnabled{true};
inline bool JA_soundEnabled{true};
inline SDL_AudioDeviceID sdlAudioDevice{0};
inline bool fading{false};
inline int fade_start_time{0};
inline int fade_duration{0};
inline float fade_initial_volume{0.0f}; // Corregido de 'int' a 'float'
// --- Forward Declarations ---
inline void JA_StopMusic();
inline void JA_StopChannel(const int channel);
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
// --- Core Functions ---
inline void JA_Update() {
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
if (fading) {
int time = SDL_GetTicks();
if (time > (fade_start_time + fade_duration)) {
fading = false;
JA_StopMusic();
return;
} else {
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
}
}
if (current_music->times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) {
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
}
if (current_music->times > 0) current_music->times--;
} else {
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
}
}
if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--;
}
} else {
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
}
}
}
}
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
#ifdef _DEBUG
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
#endif
JA_audioSpec = {format, num_channels, freq};
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
}
inline void JA_Quit() {
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
sdlAudioDevice = 0;
}
// --- Music Functions ---
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
JA_Music_t* music = new JA_Music_t();
int chan, samplerate;
short* output;
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
music->spec.channels = chan;
music->spec.freq = samplerate;
music->spec.format = SDL_AUDIO_S16;
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length));
SDL_memcpy(music->buffer, output, music->length);
free(output);
music->pos = 0;
music->state = JA_MUSIC_STOPPED;
return music;
}
inline JA_Music_t* JA_LoadMusic(const char* filename) {
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
FILE* f = fopen(filename, "rb");
if (!f) return NULL; // Añadida comprobación de apertura
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
if (!buffer) { // Añadida comprobación de malloc
fclose(f);
return NULL;
}
if (fread(buffer, fsize, 1, f) != 1) {
fclose(f);
free(buffer);
return NULL;
}
fclose(f);
JA_Music_t* music = JA_LoadMusic(buffer, fsize);
if (music) { // Comprobar que JA_LoadMusic tuvo éxito
music->filename = static_cast<char*>(malloc(strlen(filename) + 1));
if (music->filename) {
strcpy(music->filename, filename);
}
}
free(buffer);
return music;
}
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
if (!JA_musicEnabled || !music) return; // Añadida comprobación de music
JA_StopMusic();
current_music = music;
current_music->pos = 0;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) { // Comprobar creación de stream
SDL_Log("Failed to create audio stream!");
current_music->state = JA_MUSIC_STOPPED;
return;
}
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
}
inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
if (!music) music = current_music;
if (!music) return nullptr; // Añadida comprobación
return music->filename;
}
inline void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
current_music->state = JA_MUSIC_PAUSED;
SDL_UnbindAudioStream(current_music->stream);
}
inline void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada
current_music->state = JA_MUSIC_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
inline void JA_StopMusic() {
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// No liberamos filename aquí, se debería liberar en JA_DeleteMusic
}
inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
}
inline JA_Music_state JA_GetMusicState() {
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
if (!current_music) return JA_MUSIC_INVALID;
return current_music->state;
}
inline void JA_DeleteMusic(JA_Music_t* music) {
if (!music) return;
if (current_music == music) {
JA_StopMusic();
current_music = nullptr;
}
SDL_free(music->buffer);
if (music->stream) SDL_DestroyAudioStream(music->stream);
free(music->filename); // filename se libera aquí
delete music;
}
inline float JA_SetMusicVolume(float volume) {
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
if (current_music && current_music->stream) {
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
}
return JA_musicVolume;
}
inline void JA_SetMusicPosition(float value) {
if (!current_music) return;
current_music->pos = value * current_music->spec.freq;
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
// el streaming. El streaming siempre parece empezar desde el principio.
}
inline float JA_GetMusicPosition() {
if (!current_music) return 0;
return float(current_music->pos) / float(current_music->spec.freq);
// Nota: Ver `JA_SetMusicPosition`
}
inline void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
// --- Sound Functions ---
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
// Nota: spec se queda con los valores por defecto.
return sound;
}
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
delete sound;
return nullptr;
}
return sound;
}
inline JA_Sound_t* JA_LoadSound(const char* filename) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
delete sound;
return nullptr;
}
return sound;
}
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
if (!JA_soundEnabled || !sound) return -1;
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
// No hay canal libre, reemplazamos el primero
channel = 0;
}
return JA_PlaySoundOnChannel(sound, channel, loop, group);
}
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
if (!JA_soundEnabled || !sound) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group; // Asignar grupo
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) {
SDL_Log("Failed to create audio stream for sound!");
channels[channel].state = JA_CHANNEL_FREE;
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
inline void JA_DeleteSound(JA_Sound_t* sound) {
if (!sound) return;
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
SDL_free(sound->buffer);
delete sound;
}
inline void JA_PauseChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PLAYING) {
channels[i].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PLAYING) {
channels[channel].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
inline void JA_ResumeChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PAUSED) {
channels[i].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PAUSED) {
channels[channel].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
}
}
}
inline void JA_StopChannel(const int channel) {
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state != JA_CHANNEL_FREE) {
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = NULL;
}
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != JA_CHANNEL_FREE) {
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = NULL;
}
}
}
inline JA_Channel_state JA_GetChannelState(const int channel) {
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
return channels[channel].state;
}
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos
{
const float v = SDL_clamp(volume, 0.0f, 1.0f);
if (group == -1) {
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
JA_soundVolume[i] = v;
}
} else if (group >= 0 && group < JA_MAX_GROUPS) {
JA_soundVolume[group] = v;
} else {
return v; // Grupo inválido
}
// Aplicar volumen a canales activos
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
if (group == -1 || channels[i].group == group) {
if (channels[i].stream) {
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
}
}
}
}
return v;
}
inline void JA_EnableSound(const bool value) {
if (!value) {
JA_StopChannel(-1); // Detener todos los canales
}
JA_soundEnabled = value;
}
inline float JA_SetVolume(float volume) {
float v = JA_SetMusicVolume(volume);
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido
return v;
}

282
source/core/defaults.hpp Normal file
View File

@@ -0,0 +1,282 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
namespace Defaults {
// Configuración de ventana
namespace Window {
constexpr int WIDTH = 640;
constexpr int HEIGHT = 480;
constexpr int MIN_WIDTH = 320; // Mínimo: mitad del original
constexpr int MIN_HEIGHT = 240;
// Zoom system
constexpr float BASE_ZOOM = 1.0f; // 640x480 baseline
constexpr float MIN_ZOOM = 0.5f; // 320x240 minimum
constexpr float ZOOM_INCREMENT = 0.1f; // 10% steps (F1/F2)
} // namespace Window
// Dimensions base del joc (coordenades lògiques)
namespace Game {
constexpr int WIDTH = 640;
constexpr int HEIGHT = 480;
} // namespace Game
// Zones del joc (SDL_FRect amb càlculs automàtics)
namespace Zones {
// --- CONFIGURACIÓ DE PORCENTATGES ---
// Basats en valors originals 640x480
// Ajusta estos valors per canviar proporcions
constexpr float PLAYAREA_MARGIN_HORIZONTAL_PERCENT = 10.0f / Game::WIDTH;
constexpr float PLAYAREA_MARGIN_VERTICAL_PERCENT = 10.0f / Game::HEIGHT;
constexpr float SCOREBOARD_HEIGHT_PERCENT = 48.0f / Game::HEIGHT;
// --- CÀLCULS AUTOMÀTICS ---
// Estos valors es recalculen si canvien Game::WIDTH o Game::HEIGHT
constexpr float PLAYAREA_MARGIN_H =
Game::WIDTH * PLAYAREA_MARGIN_HORIZONTAL_PERCENT;
constexpr float PLAYAREA_MARGIN_V =
Game::HEIGHT * PLAYAREA_MARGIN_VERTICAL_PERCENT;
constexpr float SCOREBOARD_H = Game::HEIGHT * SCOREBOARD_HEIGHT_PERCENT;
// --- ZONES FINALS (SDL_FRect) ---
// Zona de joc principal
// Ocupa: tot menys marges (dalt, esq, dret) i scoreboard (baix)
constexpr SDL_FRect PLAYAREA = {
PLAYAREA_MARGIN_H, // x = 10.0
PLAYAREA_MARGIN_V, // y = 10.0
Game::WIDTH - 2.0f * PLAYAREA_MARGIN_H, // width = 620.0
Game::HEIGHT - PLAYAREA_MARGIN_V - SCOREBOARD_H // height = 406.0
};
// Zona de marcador
// Ocupa: tot l'ample, 64px d'alçada en la part inferior
constexpr SDL_FRect SCOREBOARD = {
0.0f, // x = 0.0
Game::HEIGHT - SCOREBOARD_H, // y = 416.0
static_cast<float>(Game::WIDTH), // width = 640.0
SCOREBOARD_H // height = 64.0
};
} // namespace Zones
// Objetos del juego
namespace Entities {
constexpr int MAX_ORNIS = 15;
constexpr int MAX_BALES = 3;
constexpr int MAX_IPUNTS = 30;
constexpr float SHIP_RADIUS = 12.0f;
constexpr float ENEMY_RADIUS = 20.0f;
constexpr float BULLET_RADIUS = 3.0f;
} // namespace Entities
// Game rules (lives, respawn, game over)
namespace Game {
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)
namespace Physics {
constexpr float ROTATION_SPEED = 3.14f; // rad/s (~180°/s)
constexpr float ACCELERATION = 400.0f; // px/s²
constexpr float MAX_VELOCITY = 120.0f; // px/s
constexpr float FRICTION = 20.0f; // px/s²
constexpr float ENEMY_SPEED = 2.0f; // unidades/frame
constexpr float BULLET_SPEED = 6.0f; // unidades/frame
constexpr float VELOCITY_SCALE = 20.0f; // factor conversión frame→tiempo
// Explosions (debris physics)
namespace Debris {
constexpr float VELOCITAT_BASE = 80.0f; // Velocitat inicial (px/s)
constexpr float VARIACIO_VELOCITAT = 40.0f; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0f; // Fricció/desacceleració (px/s²)
constexpr float ROTACIO_MIN = 0.1f; // Rotació mínima (rad/s ~5.7°/s)
constexpr float ROTACIO_MAX = 0.3f; // Rotació màxima (rad/s ~17.2°/s)
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
// Matemáticas
namespace Math {
constexpr float PI = 3.14159265359f;
} // namespace Math
// Colores (oscilación para efecto CRT)
namespace Color {
// Frecuencia de oscilación
constexpr float FREQUENCY = 6.0f; // 1 Hz (1 ciclo/segundo)
// Color de líneas (efecto fósforo verde CRT)
constexpr uint8_t LINE_MIN_R = 100; // Verde oscuro
constexpr uint8_t LINE_MIN_G = 200;
constexpr uint8_t LINE_MIN_B = 100;
constexpr uint8_t LINE_MAX_R = 100; // Verde brillante
constexpr uint8_t LINE_MAX_G = 255;
constexpr uint8_t LINE_MAX_B = 100;
// Color de fondo (pulso sutil verde oscuro)
constexpr uint8_t BACKGROUND_MIN_R = 0; // Negro
constexpr uint8_t BACKGROUND_MIN_G = 5;
constexpr uint8_t BACKGROUND_MIN_B = 0;
constexpr uint8_t BACKGROUND_MAX_R = 0; // Verde muy oscuro
constexpr uint8_t BACKGROUND_MAX_G = 15;
constexpr uint8_t BACKGROUND_MAX_B = 0;
} // namespace Color
// Brillantor (control de intensitat per cada tipus d'entitat)
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 = 1.0f; // Brillo a tope (màxima visibilitat)
// Starfield: gradient segons distància al centre
// distancia_centre: 0.0 (centre) → 1.0 (vora pantalla)
// brightness = MIN + (MAX - MIN) * distancia_centre
constexpr float STARFIELD_MIN = 0.3f; // Estrelles llunyanes (prop del centre)
constexpr float STARFIELD_MAX = 0.8f; // Estrelles properes (vora pantalla)
} // namespace Brightness
// Renderització (V-Sync i altres opcions de render)
namespace Rendering {
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
} // namespace Rendering
// Audio (sistema de so i música)
namespace Audio {
constexpr float VOLUME = 1.0F; // Volumen maestro (0.0 a 1.0)
constexpr bool ENABLED = true; // Audio habilitado por defecto
} // namespace Audio
// Música (pistas de fondo)
namespace Music {
constexpr float VOLUME = 0.8F; // Volumen música
constexpr bool ENABLED = true; // Música habilitada
constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego
constexpr int FADE_DURATION_MS = 1000; // Fade out duration
} // namespace Music
// 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 const char* GOOD_JOB_COMMANDER = "good_job_commander.wav"; // Voz: "Good job, commander"
} // namespace Sound
// Enemy type configuration (tipus d'enemics)
namespace Enemies {
// Pentagon (esquivador - zigzag evasion)
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.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
// Quadrat (perseguidor - tracks player)
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.3f; // Slow rotation [+50%]
constexpr float DROTACIO_MAX = 1.5f; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Quadrat
// Molinillo (agressiu - fast straight lines, proximity spin-up)
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 = 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";
} // namespace Molinillo
// Animation parameters (shared)
namespace Animation {
// Palpitation
constexpr float PALPITACIO_TRIGGER_PROB = 0.01f; // 1% chance per second
constexpr float PALPITACIO_DURACIO_MIN = 1.0f; // Min duration (seconds)
constexpr float PALPITACIO_DURACIO_MAX = 3.0f; // Max duration (seconds)
constexpr float PALPITACIO_AMPLITUD_MIN = 0.08f; // Min scale variation
constexpr float PALPITACIO_AMPLITUD_MAX = 0.20f; // Max scale variation
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.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.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

@@ -0,0 +1,155 @@
// shape.cpp - Implementació del sistema de formes vectorials
// © 2025 Port a C++20 amb SDL3
#include "core/graphics/shape.hpp"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <sstream>
namespace Graphics {
Shape::Shape(const std::string& filepath)
: centre_({0.0f, 0.0f}),
escala_defecte_(1.0f),
nom_("unnamed") {
carregar(filepath);
}
bool Shape::carregar(const std::string& filepath) {
// Llegir fitxer
std::ifstream file(filepath);
if (!file.is_open()) {
std::cerr << "[Shape] Error: no es pot obrir " << filepath << std::endl;
return false;
}
// Llegir tot el contingut
std::stringstream buffer;
buffer << file.rdbuf();
std::string contingut = buffer.str();
file.close();
// Parsejar
return parsejar_fitxer(contingut);
}
bool Shape::parsejar_fitxer(const std::string& contingut) {
std::istringstream iss(contingut);
std::string line;
while (std::getline(iss, line)) {
// Trim whitespace
line = trim(line);
// Skip comments and blanks
if (line.empty() || line[0] == '#')
continue;
// Parse command
if (starts_with(line, "name:")) {
nom_ = trim(extract_value(line));
} else if (starts_with(line, "scale:")) {
try {
escala_defecte_ = std::stof(extract_value(line));
} catch (...) {
std::cerr << "[Shape] Warning: escala invàlida, usant 1.0" << std::endl;
escala_defecte_ = 1.0f;
}
} else if (starts_with(line, "center:")) {
parse_center(extract_value(line));
} else if (starts_with(line, "polyline:")) {
auto points = parse_points(extract_value(line));
if (points.size() >= 2) {
primitives_.push_back({PrimitiveType::POLYLINE, points});
} else {
std::cerr << "[Shape] Warning: polyline amb menys de 2 punts ignorada"
<< std::endl;
}
} else if (starts_with(line, "line:")) {
auto points = parse_points(extract_value(line));
if (points.size() == 2) {
primitives_.push_back({PrimitiveType::LINE, points});
} else {
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 punts"
<< std::endl;
}
}
// Comandes desconegudes ignorades silenciosament
}
if (primitives_.empty()) {
std::cerr << "[Shape] Error: cap primitiva carregada" << std::endl;
return false;
}
return true;
}
// Helper: trim whitespace
std::string Shape::trim(const std::string& str) const {
const char* whitespace = " \t\n\r";
size_t start = str.find_first_not_of(whitespace);
if (start == std::string::npos)
return "";
size_t end = str.find_last_not_of(whitespace);
return str.substr(start, end - start + 1);
}
// Helper: starts_with
bool Shape::starts_with(const std::string& str,
const std::string& prefix) const {
if (str.length() < prefix.length())
return false;
return str.compare(0, prefix.length(), prefix) == 0;
}
// Helper: extract value after ':'
std::string Shape::extract_value(const std::string& line) const {
size_t colon = line.find(':');
if (colon == std::string::npos)
return "";
return line.substr(colon + 1);
}
// Helper: parse center "x, y"
void Shape::parse_center(const std::string& value) {
std::string val = trim(value);
size_t comma = val.find(',');
if (comma != std::string::npos) {
try {
centre_.x = std::stof(trim(val.substr(0, comma)));
centre_.y = std::stof(trim(val.substr(comma + 1)));
} catch (...) {
std::cerr << "[Shape] Warning: centre invàlid, usant (0,0)" << std::endl;
centre_ = {0.0f, 0.0f};
}
}
}
// Helper: parse points "x1,y1 x2,y2 x3,y3"
std::vector<Punt> Shape::parse_points(const std::string& str) const {
std::vector<Punt> points;
std::istringstream iss(trim(str));
std::string pair;
while (iss >> pair) { // Whitespace-separated
size_t comma = pair.find(',');
if (comma != std::string::npos) {
try {
float x = std::stof(pair.substr(0, comma));
float y = std::stof(pair.substr(comma + 1));
points.push_back({x, y});
} catch (...) {
std::cerr << "[Shape] Warning: punt invàlid ignorat: " << pair
<< std::endl;
}
}
}
return points;
}
} // namespace Graphics

View File

@@ -0,0 +1,64 @@
// shape.hpp - Sistema de formes vectorials
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <string>
#include <vector>
#include "core/types.hpp"
namespace Graphics {
// Tipus de primitiva dins d'una forma
enum class PrimitiveType {
POLYLINE, // Seqüència de punts connectats
LINE // Línia individual (2 punts)
};
// Primitiva individual (polyline o line)
struct ShapePrimitive {
PrimitiveType type;
std::vector<Punt> points; // 2+ punts per polyline, exactament 2 per line
};
// Classe Shape - representa una forma vectorial carregada des de .shp
class Shape {
public:
// Constructors
Shape() = default;
explicit Shape(const std::string& filepath);
// 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_;
}
const Punt& get_centre() const { return centre_; }
float get_escala_defecte() const { return escala_defecte_; }
bool es_valida() const { return !primitives_.empty(); }
// Info de depuració
std::string get_nom() const { return nom_; }
size_t get_num_primitives() const { return primitives_.size(); }
private:
std::vector<ShapePrimitive> primitives_;
Punt centre_; // Centre/origen de la forma
float escala_defecte_; // Escala per defecte (normalment 1.0)
std::string nom_; // Nom de la forma (per depuració)
// Helpers privats per parsejar
std::string trim(const std::string& str) const;
bool starts_with(const std::string& str, const std::string& prefix) const;
std::string extract_value(const std::string& line) const;
void parse_center(const std::string& value);
std::vector<Punt> parse_points(const std::string& str) const;
};
} // namespace Graphics

View File

@@ -0,0 +1,86 @@
// shape_loader.cpp - Implementació del carregador amb caché
// © 2025 Port a C++20 amb SDL3
#include "core/graphics/shape_loader.hpp"
#include "core/resources/resource_helper.hpp"
#include <iostream>
namespace Graphics {
// Inicialització de variables estàtiques
std::unordered_map<std::string, std::shared_ptr<Shape>> ShapeLoader::cache_;
std::string ShapeLoader::base_path_ = "data/shapes/";
std::shared_ptr<Shape> ShapeLoader::load(const std::string& filename) {
// Check cache first
auto it = cache_.find(filename);
if (it != cache_.end()) {
std::cout << "[ShapeLoader] Cache hit: " << filename << std::endl;
return it->second; // Cache hit
}
// 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;
}
// 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->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 " << normalized << std::endl;
return nullptr;
}
// Cache and return
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->get_nom()
<< ", " << shape->get_num_primitives() << " primitives)" << std::endl;
cache_[filename] = shape;
return shape;
}
void ShapeLoader::clear_cache() {
std::cout << "[ShapeLoader] Netejant caché (" << cache_.size() << " formes)"
<< std::endl;
cache_.clear();
}
size_t ShapeLoader::get_cache_size() { return cache_.size(); }
std::string ShapeLoader::resolve_path(const std::string& filename) {
// Si és un path absolut (comença amb '/'), usar-lo directament
if (!filename.empty() && filename[0] == '/') {
return filename;
}
// Si ja conté el prefix base_path, usar-lo directament
if (filename.find(base_path_) == 0) {
return filename;
}
// Altrament, afegir base_path (ara suporta subdirectoris)
return base_path_ + filename;
}
} // namespace Graphics

View File

@@ -0,0 +1,39 @@
// shape_loader.hpp - Carregador estàtic de formes amb caché
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include "core/graphics/shape.hpp"
namespace Graphics {
// Carregador estàtic de formes amb caché
class ShapeLoader {
public:
// No instanciable (tot estàtic)
ShapeLoader() = delete;
// Carregar forma des de fitxer (amb caché)
// Retorna punter compartit (nullptr si error)
// Exemple: load("ship.shp") → busca a "data/shapes/ship.shp"
static std::shared_ptr<Shape> load(const std::string& filename);
// Netejar caché (útil per debug/recàrrega)
static void clear_cache();
// Estadístiques (debug)
static size_t get_cache_size();
private:
static std::unordered_map<std::string, std::shared_ptr<Shape>> cache_;
static std::string base_path_; // "data/shapes/"
// Helpers privats
static std::string resolve_path(const std::string& filename);
};
} // namespace Graphics

View File

@@ -0,0 +1,172 @@
// starfield.cpp - Implementació del sistema d'estrelles de fons
// © 2025 Orni Attack
#include "core/graphics/starfield.hpp"
#include <cmath>
#include <cstdlib>
#include <iostream>
#include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/rendering/shape_renderer.hpp"
namespace Graphics {
// Constructor
Starfield::Starfield(SDL_Renderer* renderer,
const Punt& punt_fuga,
const SDL_FRect& area,
int densitat)
: renderer_(renderer),
punt_fuga_(punt_fuga),
area_(area),
densitat_(densitat) {
// Carregar forma d'estrella amb ShapeLoader
shape_estrella_ = ShapeLoader::load("star.shp");
if (!shape_estrella_ || !shape_estrella_->es_valida()) {
std::cerr << "ERROR: No s'ha pogut carregar star.shp" << std::endl;
return;
}
// Configurar 3 capes amb diferents velocitats i escales
// Capa 0: Fons llunyà (lenta, petita)
capes_.push_back({20.0f, 0.3f, 0.8f, densitat / 3});
// Capa 1: Profunditat mitjana
capes_.push_back({40.0f, 0.5f, 1.2f, densitat / 3});
// Capa 2: Primer pla (ràpida, gran)
capes_.push_back({80.0f, 0.8f, 2.0f, densitat / 3});
// Calcular radi màxim (distància del centre al racó més llunyà)
float dx = std::max(punt_fuga_.x, area_.w - punt_fuga_.x);
float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y);
radi_max_ = std::sqrt(dx * dx + dy * dy);
// Inicialitzar estrelles amb posicions distribuïdes (pre-omplir pantalla)
for (int capa_idx = 0; capa_idx < 3; capa_idx++) {
int num = capes_[capa_idx].num_estrelles;
for (int i = 0; i < num; i++) {
Estrella estrella;
estrella.capa = capa_idx;
// Angle aleatori
estrella.angle = (static_cast<float>(rand()) / RAND_MAX) * 2.0f * Defaults::Math::PI;
// Distància aleatòria (0.0 a 1.0) per omplir tota la pantalla
estrella.distancia_centre = static_cast<float>(rand()) / RAND_MAX;
// Calcular posició des de la distància
float radi = estrella.distancia_centre * radi_max_;
estrella.posicio.x = punt_fuga_.x + radi * std::cos(estrella.angle);
estrella.posicio.y = punt_fuga_.y + radi * std::sin(estrella.angle);
estrelles_.push_back(estrella);
}
}
}
// Inicialitzar una estrella (nova o regenerada)
void Starfield::inicialitzar_estrella(Estrella& estrella) {
// Angle aleatori des del punt de fuga cap a fora
estrella.angle = (static_cast<float>(rand()) / RAND_MAX) * 2.0f * Defaults::Math::PI;
// Distància inicial petita (5% del radi màxim) - neix prop del centre
estrella.distancia_centre = 0.05f;
// Posició inicial: molt prop del punt de fuga
float radi = estrella.distancia_centre * radi_max_;
estrella.posicio.x = punt_fuga_.x + radi * std::cos(estrella.angle);
estrella.posicio.y = punt_fuga_.y + radi * std::sin(estrella.angle);
}
// Verificar si una estrella està fora de l'àrea
bool Starfield::fora_area(const Estrella& estrella) const {
return (estrella.posicio.x < area_.x ||
estrella.posicio.x > area_.x + area_.w ||
estrella.posicio.y < area_.y ||
estrella.posicio.y > area_.y + area_.h);
}
// Calcular escala dinàmica segons distància del centre
float Starfield::calcular_escala(const Estrella& estrella) const {
const CapaConfig& capa = capes_[estrella.capa];
// Interpolació lineal basada en distància del centre
// distancia_centre: 0.0 (centre) → 1.0 (vora)
return capa.escala_min +
(capa.escala_max - capa.escala_min) * estrella.distancia_centre;
}
// Calcular brightness dinàmica segons distància del centre
float Starfield::calcular_brightness(const Estrella& estrella) const {
// Interpolació lineal: estrelles properes (vora) més brillants
// distancia_centre: 0.0 (centre, llunyanes) → 1.0 (vora, properes)
float brightness_base = Defaults::Brightness::STARFIELD_MIN +
(Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) *
estrella.distancia_centre;
// Aplicar multiplicador i limitar a 1.0
return std::min(1.0f, brightness_base * multiplicador_brightness_);
}
// Actualitzar posicions de les estrelles
void Starfield::actualitzar(float delta_time) {
for (auto& estrella : estrelles_) {
// Obtenir configuració de la capa
const CapaConfig& capa = capes_[estrella.capa];
// Moure cap a fora des del centre
float velocitat = capa.velocitat_base;
float dx = velocitat * std::cos(estrella.angle) * delta_time;
float dy = velocitat * std::sin(estrella.angle) * delta_time;
estrella.posicio.x += dx;
estrella.posicio.y += dy;
// Actualitzar distància del centre
float dx_centre = estrella.posicio.x - punt_fuga_.x;
float dy_centre = estrella.posicio.y - punt_fuga_.y;
float dist_px = std::sqrt(dx_centre * dx_centre + dy_centre * dy_centre);
estrella.distancia_centre = dist_px / radi_max_;
// Si ha sortit de l'àrea, regenerar-la
if (fora_area(estrella)) {
inicialitzar_estrella(estrella);
}
}
}
// Establir multiplicador de brightness
void Starfield::set_brightness(float multiplier) {
multiplicador_brightness_ = std::max(0.0f, multiplier); // Evitar valors negatius
}
// Dibuixar totes les estrelles
void Starfield::dibuixar() {
if (!shape_estrella_->es_valida()) {
return;
}
for (const auto& estrella : estrelles_) {
// Calcular escala i brightness dinàmicament
float escala = calcular_escala(estrella);
float brightness = calcular_brightness(estrella);
// Renderitzar estrella sense rotació
Rendering::render_shape(
renderer_,
shape_estrella_,
estrella.posicio,
0.0f, // angle (les estrelles no giren)
escala, // escala dinàmica
true, // dibuixar
1.0f, // progress (sempre visible)
brightness // brightness dinàmica
);
}
}
} // namespace Graphics

View File

@@ -0,0 +1,82 @@
// starfield.hpp - Sistema d'estrelles de fons amb efecte de profunditat
// © 2025 Orni Attack
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include <vector>
#include "core/graphics/shape.hpp"
#include "core/types.hpp"
namespace Graphics {
// Configuració per cada capa de profunditat
struct CapaConfig {
float velocitat_base; // Velocitat base d'aquesta capa (px/s)
float escala_min; // Escala mínima prop del centre
float escala_max; // Escala màxima al límit de pantalla
int num_estrelles; // Nombre d'estrelles en aquesta capa
};
// Classe Starfield - camp d'estrelles animat amb efecte de profunditat
class Starfield {
public:
// Constructor
// - renderer: SDL renderer
// - punt_fuga: punt d'origen/fuga des d'on surten les estrelles
// - area: rectangle on actuen les estrelles (SDL_FRect)
// - densitat: nombre total d'estrelles (es divideix entre capes)
Starfield(SDL_Renderer* renderer,
const Punt& punt_fuga,
const SDL_FRect& area,
int densitat = 150);
// Actualitzar posicions de les estrelles
void actualitzar(float delta_time);
// Dibuixar totes les estrelles
void dibuixar();
// Setters per ajustar paràmetres en temps real
void set_punt_fuga(const Punt& punt) { punt_fuga_ = punt; }
void set_brightness(float multiplier);
private:
// Estructura interna per cada estrella
struct Estrella {
Punt posicio; // Posició actual
float angle; // Angle de moviment (radians)
float distancia_centre; // Distància normalitzada del centre (0.0-1.0)
int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop)
};
// Inicialitzar una estrella (nova o regenerada)
void inicialitzar_estrella(Estrella& estrella);
// Verificar si una estrella està fora de l'àrea
bool fora_area(const Estrella& estrella) const;
// Calcular escala dinàmica segons distància del centre
float calcular_escala(const Estrella& estrella) const;
// Calcular brightness dinàmica segons distància del centre
float calcular_brightness(const Estrella& estrella) const;
// Dades
std::vector<Estrella> estrelles_;
std::vector<CapaConfig> capes_; // Configuració de les 3 capes
std::shared_ptr<Shape> shape_estrella_;
SDL_Renderer* renderer_;
// Configuració
Punt punt_fuga_; // Punt d'origen de les estrelles
SDL_FRect area_; // Àrea activa
float radi_max_; // Distància màxima del centre al límit de pantalla
int densitat_; // Nombre total d'estrelles
float multiplicador_brightness_{1.0f}; // Multiplicador de brillantor (1.0 = default)
};
} // namespace Graphics

View File

@@ -0,0 +1,263 @@
// vector_text.cpp - Implementació del sistema de text vectorial
// © 2025 Port a C++20 amb SDL3
#include "core/graphics/vector_text.hpp"
#include <iostream>
#include "core/graphics/shape_loader.hpp"
#include "core/rendering/shape_renderer.hpp"
namespace Graphics {
// Constants per a mides base dels caràcters
constexpr float char_width = 20.0f; // Amplada base del caràcter
constexpr float char_height = 40.0f; // Altura base del caràcter
VectorText::VectorText(SDL_Renderer* renderer)
: renderer_(renderer) {
load_charset();
}
void VectorText::load_charset() {
// Cargar dígitos 0-9
for (char c = '0'; c <= '9'; c++) {
std::string filename = get_shape_filename(c);
auto shape = ShapeLoader::load(filename);
if (shape && shape->es_valida()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
<< std::endl;
}
}
// Cargar lletres A-Z (majúscules)
for (char c = 'A'; c <= 'Z'; c++) {
std::string filename = get_shape_filename(c);
auto shape = ShapeLoader::load(filename);
if (shape && shape->es_valida()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
<< std::endl;
}
}
// Cargar símbolos
const std::string symbols[] = {".", ",", "-", ":", "!", "?"};
for (const auto& sym : symbols) {
char c = sym[0];
std::string filename = get_shape_filename(c);
auto shape = ShapeLoader::load(filename);
if (shape && shape->es_valida()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
<< std::endl;
}
}
// Cargar símbolo de copyright (©) - UTF-8 U+00A9
// Usem el segon byte (0xA9) com a key interna
{
char c = '\xA9'; // 169 decimal
std::string filename = "font/char_copyright.shp";
auto shape = ShapeLoader::load(filename);
if (shape && shape->es_valida()) {
chars_[c] = shape;
} else {
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
<< std::endl;
}
}
std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters"
<< std::endl;
}
std::string VectorText::get_shape_filename(char c) const {
// Mapeo carácter → nombre de archivo (amb prefix "font/")
switch (c) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return std::string("font/char_") + c + ".shp";
// Lletres majúscules A-Z
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'L':
case 'M':
case 'N':
case 'O':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'X':
case 'Y':
case 'Z':
return std::string("font/char_") + c + ".shp";
// Lletres minúscules a-z (convertir a majúscules)
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'l':
case 'm':
case 'n':
case 'o':
case 'p':
case 'q':
case 'r':
case 's':
case 't':
case 'u':
case 'v':
case 'w':
case 'x':
case 'y':
case 'z':
return std::string("font/char_") + char(c - 32) + ".shp";
// Símbols
case '.':
return "font/char_dot.shp";
case ',':
return "font/char_comma.shp";
case '-':
return "font/char_minus.shp";
case ':':
return "font/char_colon.shp";
case '!':
return "font/char_exclamation.shp";
case '?':
return "font/char_question.shp";
case ' ':
return ""; // Espai es maneja sense carregar shape
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
return "font/char_copyright.shp";
default:
return ""; // Caràcter no suportat
}
}
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, float brightness) {
if (!renderer_) {
return;
}
// Ancho de un carácter base (20 px a escala 1.0)
const float char_width_scaled = char_width * escala;
// Spacing escalado
const float spacing_scaled = spacing * escala;
// Altura de un carácter escalado (necesario para ajustar Y)
const float char_height_scaled = char_height * escala;
// Posición actual del centro del carácter (ajustada desde esquina superior
// izquierda)
float current_x = posicio.x;
// Iterar sobre cada byte del string (con detecció UTF-8)
for (size_t i = 0; i < text.length(); i++) {
unsigned char c = static_cast<unsigned char>(text[i]);
// Detectar copyright UTF-8 (0xC2 0xA9)
if (c == 0xC2 && i + 1 < text.length() &&
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
c = 0xA9; // Usar segon byte com a key
i++; // Saltar el següent byte
}
// Manejar espacios (avanzar sin dibujar)
if (c == ' ') {
current_x += char_width_scaled + spacing_scaled;
continue;
}
// Verificar si el carácter está soportado
auto it = chars_.find(c);
if (it != chars_.end()) {
// Renderizar carácter
// 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, 1.0f, brightness);
// Avanzar posición
current_x += char_width_scaled + spacing_scaled;
} else {
// Carácter no soportado: saltar (o renderizar '?' en el futuro)
std::cerr << "[VectorText] Warning: caràcter no suportat '" << c << "'"
<< std::endl;
current_x += char_width_scaled + spacing_scaled;
}
}
}
float VectorText::get_text_width(const std::string& text, float escala, float spacing) const {
if (text.empty()) {
return 0.0f;
}
const float char_width_scaled = char_width * escala;
const float spacing_scaled = spacing * escala;
// Ancho total = (número de caracteres × char_width) + (espacios entre
// caracteres)
float width = text.length() * char_width_scaled;
// Añadir spacing entre caracteres (n-1 espacios para n caracteres)
if (text.length() > 1) {
width += (text.length() - 1) * spacing_scaled;
}
return width;
}
float VectorText::get_text_height(float escala) const {
return char_height * escala;
}
} // namespace Graphics

View File

@@ -0,0 +1,47 @@
// vector_text.hpp - Sistema de texto vectorial con display de 7-segmentos
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include <string>
#include <unordered_map>
#include "core/graphics/shape.hpp"
#include "core/types.hpp"
namespace Graphics {
class VectorText {
public:
VectorText(SDL_Renderer* renderer);
// Renderizar string completo
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
// '!', '?', ' ')
// - 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)
// - 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;
// Calcular altura del texto (útil para centrado vertical)
float get_text_height(float escala = 1.0f) const;
// Verificar si un carácter está soportado
bool is_supported(char c) const;
private:
SDL_Renderer* renderer_;
std::unordered_map<char, std::shared_ptr<Shape>> chars_;
void load_charset();
std::string get_shape_filename(char c) const;
};
} // namespace Graphics

View File

@@ -0,0 +1,63 @@
#include "core/input/mouse.hpp"
namespace Mouse {
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
bool cursor_visible = true; // Estado del cursor
// Modo forzado: Usado cuando SDLManager entra en pantalla completa.
// Cuando está activado, el cursor permanece oculto independientemente del movimiento del ratón.
// SDLManager controla esto mediante llamadas a setForceHidden().
bool force_hidden = false;
void setForceHidden(bool force) {
force_hidden = force;
if (force) {
// Entrando en modo oculto forzado: ocultar cursor inmediatamente
if (cursor_visible) {
SDL_HideCursor();
cursor_visible = false;
}
} else {
// Saliendo de modo oculto forzado: mostrar cursor y resetear temporizador
SDL_ShowCursor();
cursor_visible = true;
last_mouse_move_time = SDL_GetTicks(); // Resetear temporizador
}
}
bool isForceHidden() {
return force_hidden;
}
void handleEvent(const SDL_Event& event) {
// CRÍTICO: Si estamos en modo forzado, ignorar todos los eventos del ratón
if (force_hidden) {
return; // Salir temprano - no procesar ningún evento
}
// MODO NORMAL: Mostrar cursor al mover el ratón
if (event.type == SDL_EVENT_MOUSE_MOTION) {
last_mouse_move_time = SDL_GetTicks();
if (!cursor_visible) {
SDL_ShowCursor();
cursor_visible = true;
}
}
}
void updateCursorVisibility() {
// CRÍTICO: Si estamos en modo forzado, no aplicar lógica de timeout
if (force_hidden) {
return; // Salir temprano - el cursor permanece oculto
}
// MODO NORMAL: Auto-ocultar basado en timeout
Uint32 current_time = SDL_GetTicks();
if (cursor_visible && (current_time - last_mouse_move_time > cursor_hide_time)) {
SDL_HideCursor();
cursor_visible = false;
}
}
} // namespace Mouse

View File

@@ -0,0 +1,16 @@
#pragma once
#include <SDL3/SDL.h>
namespace Mouse {
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor
extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió
extern bool cursor_visible; // Estado del cursor
void handleEvent(const SDL_Event& event);
void updateCursorVisibility();
// Control de visibilidad forzada (para modo pantalla completa)
void setForceHidden(bool force); // Activar/desactivar ocultación forzada
bool isForceHidden(); // Consultar estado actual
} // namespace Mouse

View File

@@ -0,0 +1,68 @@
// color_oscillator.cpp - Implementació d'oscil·lació de color
// © 2025 Port a C++20 amb SDL3
#include "core/rendering/color_oscillator.hpp"
#include <cmath>
#include "core/defaults.hpp"
namespace Rendering {
ColorOscillator::ColorOscillator()
: accumulated_time_(0.0f) {
// Inicialitzar amb el color mínim
current_line_color_ = {Defaults::Color::LINE_MIN_R,
Defaults::Color::LINE_MIN_G,
Defaults::Color::LINE_MIN_B,
255};
current_background_color_ = {Defaults::Color::BACKGROUND_MIN_R,
Defaults::Color::BACKGROUND_MIN_G,
Defaults::Color::BACKGROUND_MIN_B,
255};
}
void ColorOscillator::update(float delta_time) {
accumulated_time_ += delta_time;
float factor =
calculateOscillationFactor(accumulated_time_, Defaults::Color::FREQUENCY);
// Interpolar colors de línies
SDL_Color line_min = {Defaults::Color::LINE_MIN_R,
Defaults::Color::LINE_MIN_G,
Defaults::Color::LINE_MIN_B,
255};
SDL_Color line_max = {Defaults::Color::LINE_MAX_R,
Defaults::Color::LINE_MAX_G,
Defaults::Color::LINE_MAX_B,
255};
current_line_color_ = interpolateColor(line_min, line_max, factor);
// Interpolar colors de fons
SDL_Color bg_min = {Defaults::Color::BACKGROUND_MIN_R,
Defaults::Color::BACKGROUND_MIN_G,
Defaults::Color::BACKGROUND_MIN_B,
255};
SDL_Color bg_max = {Defaults::Color::BACKGROUND_MAX_R,
Defaults::Color::BACKGROUND_MAX_G,
Defaults::Color::BACKGROUND_MAX_B,
255};
current_background_color_ = interpolateColor(bg_min, bg_max, factor);
}
float ColorOscillator::calculateOscillationFactor(float time, float frequency) {
// Oscil·lació senoïdal: sin(t * freq * 2π)
// Mapejar de [-1, 1] a [0, 1]
float radians = time * frequency * 2.0f * Defaults::Math::PI;
return (std::sin(radians) + 1.0f) / 2.0f;
}
SDL_Color ColorOscillator::interpolateColor(SDL_Color min, SDL_Color max, float factor) {
return {static_cast<uint8_t>(min.r + (max.r - min.r) * factor),
static_cast<uint8_t>(min.g + (max.g - min.g) * factor),
static_cast<uint8_t>(min.b + (max.b - min.b) * factor),
255};
}
} // namespace Rendering

View File

@@ -0,0 +1,29 @@
// color_oscillator.hpp - Sistema d'oscil·lació de color per efecte CRT
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <SDL3/SDL.h>
namespace Rendering {
class ColorOscillator {
public:
ColorOscillator();
void update(float delta_time);
SDL_Color getCurrentLineColor() const { return current_line_color_; }
SDL_Color getCurrentBackgroundColor() const {
return current_background_color_;
}
private:
float accumulated_time_;
SDL_Color current_line_color_;
SDL_Color current_background_color_;
static float calculateOscillationFactor(float time, float frequency);
static SDL_Color interpolateColor(SDL_Color min, SDL_Color max, float factor);
};
} // namespace Rendering

View File

@@ -0,0 +1,11 @@
// coordinate_transform.cpp - Inicialització de variables globals
// © 2025 Port a C++20 amb SDL3
#include "core/rendering/coordinate_transform.hpp"
namespace Rendering {
// Factor d'escala global (inicialitzat a 1.0 per defecte)
float g_current_scale_factor = 1.0f;
} // namespace Rendering

View File

@@ -0,0 +1,31 @@
// coordinate_transform.hpp - Transformació de coordenades lògiques a físiques
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <cmath>
namespace Rendering {
// Factor d'escala global (actualitzat cada frame per SDLManager)
extern float g_current_scale_factor;
// Transforma coordenada lògica a física amb arrodoniment
inline int transform_x(int logical_x, float scale) {
return static_cast<int>(std::round(logical_x * scale));
}
inline int transform_y(int logical_y, float scale) {
return static_cast<int>(std::round(logical_y * scale));
}
// Variant que usa el factor d'escala global
inline int transform_x(int logical_x) {
return transform_x(logical_x, g_current_scale_factor);
}
inline int transform_y(int logical_y) {
return transform_y(logical_y, g_current_scale_factor);
}
} // namespace Rendering

View File

@@ -0,0 +1,100 @@
// line_renderer.cpp - Implementació de renderitzat de línies
// © 1999 Visente i Sergi (versió Pascal)
// © 2025 Port a C++20 amb SDL3
#include "core/rendering/line_renderer.hpp"
#include <cmath>
#include "core/rendering/coordinate_transform.hpp"
namespace Rendering {
// [NUEVO] Color global compartit (actualitzat per ColorOscillator via
// SDLManager)
SDL_Color g_current_line_color = {255, 255, 255, 255}; // Blanc inicial
bool linea(SDL_Renderer* renderer, int x1, int y1, int x2, int y2, bool dibuixar, float brightness) {
// Algorisme de Bresenham per dibuixar línies
// Basat en el codi Pascal original
// Helper function: retorna el signe d'un nombre
auto sign = [](int x) -> int {
if (x < 0)
return -1;
if (x > 0)
return 1;
return 0;
};
// Variables per a l'algorisme (no utilitzades fins Fase 10 - detecció de
// col·lisions) int x = x1, y = y1; int xs = x2 - x1; int ys = y2 - y1; int
// xm = sign(xs); int ym = sign(ys); xs = std::abs(xs); ys = std::abs(ys);
// Suprimir warning de variable no usada
(void)sign;
// Detecció de col·lisió (TODO per Fase 10)
// El codi Pascal original llegia pixels del framebuffer bit-packed
// i comptava col·lisions. Per ara, usem SDL_RenderDrawLine i retornem false.
bool colisio = false;
// Dibuixar amb SDL3 (més eficient que Bresenham píxel a píxel)
if (dibuixar && renderer) {
// Transformar coordenades lògiques (640x480) a físiques (resolució real)
float scale = g_current_scale_factor;
int px1 = transform_x(x1, scale);
int py1 = transform_y(y1, scale);
int px2 = transform_x(x2, scale);
int py2 = transform_y(y2, scale);
// Aplicar brightness al color oscil·lat global
SDL_Color color_final;
color_final.r = static_cast<uint8_t>(g_current_line_color.r * brightness);
color_final.g = static_cast<uint8_t>(g_current_line_color.g * brightness);
color_final.b = static_cast<uint8_t>(g_current_line_color.b * brightness);
color_final.a = 255;
SDL_SetRenderDrawColor(renderer, color_final.r, color_final.g, color_final.b, 255);
// Renderitzar amb coordenades físiques
SDL_RenderLine(renderer, static_cast<float>(px1), static_cast<float>(py1), static_cast<float>(px2), static_cast<float>(py2));
}
// Algorisme de Bresenham original (conservat per a futura detecció de
// col·lisió)
/*
if (xs > ys) {
// Línia plana (<45 graus)
int count = -(xs / 2);
while (x != x2) {
count = count + ys;
x = x + xm;
if (count > 0) {
y = y + ym;
count = count - xs;
}
// Aquí aniria la detecció de col·lisió píxel a píxel
}
} else {
// Línia pronunciada (>=45 graus)
int count = -(ys / 2);
while (y != y2) {
count = count + xs;
y = y + ym;
if (count > 0) {
x = x + xm;
count = count - ys;
}
// Aquí aniria la detecció de col·lisió píxel a píxel
}
}
*/
return colisio;
}
// [NUEVO] Establir el color global de les línies
void setLineColor(SDL_Color color) { g_current_line_color = color; }
} // namespace Rendering

View File

@@ -0,0 +1,16 @@
// line_renderer.hpp - Renderitzat de línies
// © 1999 Visente i Sergi (versió Pascal)
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <SDL3/SDL.h>
namespace Rendering {
// Algorisme de Bresenham per dibuixar línies
// Retorna true si hi ha col·lisió (per Fase 10)
// brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor)
bool linea(SDL_Renderer* renderer, int x1, int y1, int x2, int y2, bool dibuixar, float brightness = 1.0f);
// [NUEVO] Establir el color global de les línies (oscil·lació)
void setLineColor(SDL_Color color);
} // namespace Rendering

View File

@@ -0,0 +1,86 @@
// polygon_renderer.cpp - Implementació de renderitzat de polígons
// © 1999 Visente i Sergi (versió Pascal)
// © 2025 Port a C++20 amb SDL3
//
// ==============================================================================
// DEPRECATED: Use core/rendering/shape_renderer.cpp instead
// ==============================================================================
#include "core/rendering/polygon_renderer.hpp"
#include <array>
#include <cmath>
#include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp"
namespace Rendering {
void rota_tri(SDL_Renderer* renderer, const Triangle& tri, float angul, float velocitat, bool dibuixar) {
// Rotar i dibuixar triangle (nau)
// Conversió de coordenades polars a cartesianes amb rotació
// Basat en el codi Pascal original: lines 271-284
// Convertir cada punt polar a cartesià
// x = (r + velocitat) * cos(angle_punt + angle_nau) + centre.x
// y = (r + velocitat) * sin(angle_punt + angle_nau) + centre.y
int x1 = static_cast<int>(std::round((tri.p1.r + velocitat) *
std::cos(tri.p1.angle + angul))) +
tri.centre.x;
int y1 = static_cast<int>(std::round((tri.p1.r + velocitat) *
std::sin(tri.p1.angle + angul))) +
tri.centre.y;
int x2 = static_cast<int>(std::round((tri.p2.r + velocitat) *
std::cos(tri.p2.angle + angul))) +
tri.centre.x;
int y2 = static_cast<int>(std::round((tri.p2.r + velocitat) *
std::sin(tri.p2.angle + angul))) +
tri.centre.y;
int x3 = static_cast<int>(std::round((tri.p3.r + velocitat) *
std::cos(tri.p3.angle + angul))) +
tri.centre.x;
int y3 = static_cast<int>(std::round((tri.p3.r + velocitat) *
std::sin(tri.p3.angle + angul))) +
tri.centre.y;
// Dibuixar les 3 línies que formen el triangle
linea(renderer, x1, y1, x2, y2, dibuixar);
linea(renderer, x1, y1, x3, y3, dibuixar);
linea(renderer, x3, y3, x2, y2, dibuixar);
}
void rota_pol(SDL_Renderer* renderer, const Poligon& pol, float angul, bool dibuixar) {
// Rotar i dibuixar polígon (enemics i bales)
// Conversió de coordenades polars a cartesianes amb rotació
// Basat en el codi Pascal original: lines 286-296
// Array temporal per emmagatzemar punts convertits a cartesianes
std::array<Punt, Defaults::Entities::MAX_IPUNTS> xy;
// Convertir cada punt polar a cartesià
for (uint8_t i = 0; i < pol.n; i++) {
xy[i].x = static_cast<int>(std::round(
pol.ipuntx[i].r * std::cos(pol.ipuntx[i].angle + angul))) +
pol.centre.x;
xy[i].y = static_cast<int>(std::round(
pol.ipuntx[i].r * std::sin(pol.ipuntx[i].angle + angul))) +
pol.centre.y;
}
// Dibuixar línies entre punts consecutius
for (uint8_t i = 0; i < pol.n - 1; i++) {
linea(renderer, xy[i].x, xy[i].y, xy[i + 1].x, xy[i + 1].y, dibuixar);
}
// Tancar el polígon (últim punt → primer punt)
linea(renderer, xy[pol.n - 1].x, xy[pol.n - 1].y, xy[0].x, xy[0].y, dibuixar);
}
} // namespace Rendering

View File

@@ -0,0 +1,22 @@
// polygon_renderer.hpp - Renderitzat de polígons polars
// © 1999 Visente i Sergi (versió Pascal)
// © 2025 Port a C++20 amb SDL3
//
// ==============================================================================
// DEPRECATED: Use core/rendering/shape_renderer.hpp instead
// ==============================================================================
// This file is kept temporarily for chatarra_cosmica_ (Phase 10: explosions)
// TODO Phase 10: Replace with particle system or remove completely
#pragma once
#include <SDL3/SDL.h>
#include "core/types.hpp"
namespace Rendering {
// Rotar i dibuixar triangle (nau)
void rota_tri(SDL_Renderer* renderer, const Triangle& tri, float angul, float velocitat, bool dibuixar);
// Rotar i dibuixar polígon (enemics i bales)
void rota_pol(SDL_Renderer* renderer, const Poligon& pol, float angul, bool dibuixar);
} // namespace Rendering

View File

@@ -0,0 +1,66 @@
// primitives.cpp - Implementació de funcions geomètriques
// © 1999 Visente i Sergi (versió Pascal)
// © 2025 Port a C++20 amb SDL3
//
// ==============================================================================
// DEPRECATED: Use Shape system instead (.shp files + ShapeLoader)
// ==============================================================================
#include "primitives.hpp"
#include <cmath>
#include "core/defaults.hpp"
float modul(const Punt& p) {
// Càlcul de la magnitud d'un vector: sqrt(x² + y²)
return std::sqrt(p.x * p.x + p.y * p.y);
}
void diferencia(const Punt& o, const Punt& d, Punt& p) {
// Resta de vectors (origen - destí)
p.x = o.x - d.x;
p.y = o.y - d.y;
}
int distancia(const Punt& o, const Punt& d) {
// Distància entre dos punts
Punt p;
diferencia(o, d, p);
return static_cast<int>(std::round(modul(p)));
}
float angle_punt(const Punt& p) {
// Càlcul de l'angle d'un punt (arctan)
if (p.y != 0) {
return std::atan(p.x / p.y);
}
return 0.0f;
}
void crear_poligon_regular(Poligon& pol, uint8_t n, float r) {
// Crear un polígon regular amb n costats i radi r
// Distribueix els punts uniformement al voltant d'un cercle
float interval = 2.0f * Defaults::Math::PI / n;
float act = 0.0f;
for (uint8_t i = 0; i < n; i++) {
pol.ipuntx[i].r = r;
pol.ipuntx[i].angle = act;
act += interval;
}
// Inicialitzar propietats del polígon
pol.centre.x = 320.0f;
pol.centre.y = 200.0f;
pol.angle = 0.0f;
// Convertir velocitat de px/frame a px/s: 2 px/frame × 20 FPS = 40 px/s
pol.velocitat = Defaults::Physics::ENEMY_SPEED * 20.0f;
pol.n = n;
// Convertir rotació de rad/frame a rad/s: 0.0785 rad/frame × 20 FPS = 1.57
// rad/s (~90°/s)
pol.drotacio = 0.078539816f * 20.0f;
pol.rotacio = 0.0f;
pol.esta = true;
}

View File

@@ -0,0 +1,32 @@
// primitives.hpp - Funcions geomètriques bàsiques
// © 1999 Visente i Sergi (versió Pascal)
// © 2025 Port a C++20 amb SDL3
//
// ==============================================================================
// DEPRECATED: Use Shape system instead (.shp files + ShapeLoader)
// ==============================================================================
// This file is kept temporarily for chatarra_cosmica_ (Phase 10: explosions)
// TODO Phase 10: Replace with particle system or remove completely
#pragma once
#include <cstdint>
#include "core/types.hpp"
// Funcions matemàtiques geomètriques pures (sense dependències d'estat)
// Càlcul de la magnitud d'un vector
float modul(const Punt& p);
// Diferència entre dos punts (vector origen - destí)
void diferencia(const Punt& o, const Punt& d, Punt& p);
// Distància entre dos punts
int distancia(const Punt& o, const Punt& d);
// Càlcul de l'angle d'un punt
float angle_punt(const Punt& p);
// Creació de polígons regulars
void crear_poligon_regular(Poligon& pol, uint8_t n, float r);

View File

@@ -8,6 +8,8 @@
#include <iostream>
#include "core/defaults.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/coordinate_transform.hpp"
#include "core/rendering/line_renderer.hpp"
#include "game/options.hpp"
#include "project.h"
@@ -146,6 +148,9 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
std::cout << " [FULLSCREEN]";
}
std::cout << std::endl;
// Inicialitzar mòdul Mouse amb l'estat actual de fullscreen
Mouse::setForceHidden(is_fullscreen_);
}
SDLManager::~SDLManager() {
@@ -228,6 +233,9 @@ void SDLManager::applyZoom(float new_zoom) {
// Apply to window (centers via applyWindowSize)
applyWindowSize(new_width, new_height);
// Update viewport for new zoom
updateViewport();
// Update windowed size cache
windowed_width_ = new_width;
windowed_height_ = new_height;
@@ -242,14 +250,38 @@ void SDLManager::applyZoom(float new_zoom) {
}
void SDLManager::updateLogicalPresentation() {
// AIXÒ ÉS LA MÀGIA: El joc SEMPRE dibuixa en 640x480,
// SDL escala automàticament a la mida física de la finestra
SDL_SetRenderLogicalPresentation(
renderer_,
Defaults::Game::WIDTH, // 640 (lògic)
Defaults::Game::HEIGHT, // 480 (lògic)
SDL_LOGICAL_PRESENTATION_LETTERBOX // Mantenir aspect ratio 4:3
);
// CANVIAT: Ja no usem SDL_SetRenderLogicalPresentation
// Ara renderitzem directament a resolució física per evitar pixelació irregular
// El viewport amb letterbox es configura a updateViewport()
updateViewport();
}
void SDLManager::updateViewport() {
// Calcular dimensions físiques basades en el zoom
float scale = zoom_factor_;
int scaled_width = static_cast<int>(std::round(Defaults::Game::WIDTH * scale));
int scaled_height = static_cast<int>(std::round(Defaults::Game::HEIGHT * scale));
// Càlcul de letterbox (centrar l'àrea escalada)
int offset_x = (current_width_ - scaled_width) / 2;
int offset_y = (current_height_ - scaled_height) / 2;
// Evitar offsets negatius
offset_x = std::max(offset_x, 0);
offset_y = std::max(offset_y, 0);
// Configurar viewport per al renderitzat
SDL_Rect viewport = {offset_x, offset_y, scaled_width, scaled_height};
SDL_SetRenderViewport(renderer_, &viewport);
std::cout << "Viewport: " << scaled_width << "x" << scaled_height
<< " @ (" << offset_x << "," << offset_y << ") [scale=" << scale << "]"
<< std::endl;
}
void SDLManager::updateRenderingContext() {
// Actualitzar el factor d'escala global per a totes les funcions de renderitzat
Rendering::g_current_scale_factor = zoom_factor_;
}
void SDLManager::increaseWindowSize() {
@@ -300,8 +332,8 @@ void SDLManager::applyWindowSize(int new_width, int new_height) {
SDL_SetWindowPosition(finestra_, new_x, new_y);
// NO cal actualitzar el logical presentation aquí,
// SDL ho maneja automàticament
// Actualitzar viewport després del resize
updateViewport();
}
void SDLManager::toggleFullscreen() {
@@ -328,6 +360,10 @@ void SDLManager::toggleFullscreen() {
}
Options::window.fullscreen = is_fullscreen_;
// Notificar al mòdul Mouse: Fullscreen requereix ocultació permanent del cursor.
// Quan es surt de fullscreen, restaurar el comportament normal d'auto-ocultació.
Mouse::setForceHidden(is_fullscreen_);
}
bool SDLManager::handleWindowEvent(const SDL_Event& event) {
@@ -345,6 +381,9 @@ bool SDLManager::handleWindowEvent(const SDL_Event& event) {
windowed_height_ = current_height_;
}
// Actualitzar viewport després del resize manual
updateViewport();
std::cout << "Finestra redimensionada: " << current_width_
<< "x" << current_height_ << " (zoom ≈" << zoom_factor_ << "x)"
<< std::endl;
@@ -394,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());
@@ -423,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);
@@ -42,10 +40,14 @@ class SDLManager {
// Getters
SDL_Renderer* obte_renderer() { return renderer_; }
float getScaleFactor() const { return zoom_factor_; }
// [NUEVO] Actualitzar títol de la finestra
void setWindowTitle(const std::string& title);
// [NUEVO] Actualitzar context de renderitzat (factor d'escala global)
void updateRenderingContext();
private:
SDL_Window* finestra_;
SDL_Renderer* renderer_;
@@ -74,6 +76,7 @@ class SDLManager {
void applyZoom(float new_zoom); // Apply zoom and resize window
void applyWindowSize(int width, int height); // Canviar mida + centrar
void updateLogicalPresentation(); // Actualitzar viewport
void updateViewport(); // Configurar viewport amb letterbox
// [NUEVO] Oscil·lador de colors
Rendering::ColorOscillator color_oscillator_;

View File

@@ -0,0 +1,80 @@
// shape_renderer.cpp - Implementació del renderitzat de formes
// © 2025 Port a C++20 amb SDL3
#include "core/rendering/shape_renderer.hpp"
#include <cmath>
#include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp"
namespace Rendering {
// Helper: transformar un punt amb rotació, escala i trasllació
static Punt transform_point(const Punt& point, const Punt& shape_centre, const Punt& posicio, float angle, float escala) {
// 1. Centrar el punt respecte al centre de la forma
float centered_x = point.x - shape_centre.x;
float centered_y = point.y - shape_centre.y;
// 2. Aplicar escala al punt centrat
float scaled_x = centered_x * escala;
float scaled_y = centered_y * escala;
// 3. Aplicar rotació
// IMPORTANT: En el sistema original, angle=0 apunta AMUNT (no dreta)
// Per això usem (angle - PI/2) per compensar
// Però aquí angle ja ve en el sistema correcte del joc
float cos_a = std::cos(angle);
float sin_a = std::sin(angle);
float rotated_x = scaled_x * cos_a - scaled_y * sin_a;
float rotated_y = scaled_x * sin_a + scaled_y * cos_a;
// 4. Aplicar trasllació a posició mundial
return {rotated_x + posicio.x, rotated_y + posicio.y};
}
void render_shape(SDL_Renderer* renderer,
const std::shared_ptr<Graphics::Shape>& shape,
const Punt& posicio,
float angle,
float escala,
bool dibuixar,
float progress,
float brightness) {
// Verificar que la forma és vàlida
if (!shape || !shape->es_valida()) {
return;
}
// Si progress < 1.0, no dibuixar (tot o res)
if (progress < 1.0f) {
return;
}
// Obtenir el centre de la forma per a transformacions
const Punt& shape_centre = shape->get_centre();
// Iterar sobre totes les primitives
for (const auto& primitive : shape->get_primitives()) {
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
// POLYLINE: connectar punts consecutius
for (size_t i = 0; i < primitive.points.size() - 1; i++) {
Punt p1 = transform_point(primitive.points[i], shape_centre, posicio, angle, escala);
Punt p2 = transform_point(primitive.points[i + 1], shape_centre, posicio, angle, escala);
linea(renderer, static_cast<int>(p1.x), static_cast<int>(p1.y), static_cast<int>(p2.x), static_cast<int>(p2.y), dibuixar, brightness);
}
} else { // PrimitiveType::LINE
// LINE: exactament 2 punts
if (primitive.points.size() >= 2) {
Punt p1 = transform_point(primitive.points[0], shape_centre, posicio, angle, escala);
Punt p2 = transform_point(primitive.points[1], shape_centre, posicio, angle, escala);
linea(renderer, static_cast<int>(p1.x), static_cast<int>(p1.y), static_cast<int>(p2.x), static_cast<int>(p2.y), dibuixar, brightness);
}
}
}
}
} // namespace Rendering

View File

@@ -0,0 +1,33 @@
// shape_renderer.hpp - Renderitzat de formes vectorials
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include "core/graphics/shape.hpp"
#include "core/types.hpp"
namespace Rendering {
// Renderitzar forma amb transformacions
// - renderer: SDL renderer
// - shape: forma vectorial a dibuixar
// - posicio: posició del centre en coordenades mundials
// - angle: rotació en radians (0 = amunt, sentit horari)
// - escala: factor d'escala (1.0 = mida original)
// - dibuixar: flag per dibuixar (false per col·lisions futures)
// - progress: progrés de l'animació (0.0-1.0, default 1.0 = tot visible)
// - brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor)
void render_shape(SDL_Renderer* renderer,
const std::shared_ptr<Graphics::Shape>& shape,
const Punt& posicio,
float angle,
float escala = 1.0f,
bool dibuixar = true,
float progress = 1.0f,
float brightness = 1.0f);
} // namespace Rendering

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

@@ -0,0 +1,261 @@
#include "director.hpp"
#include <SDL3/SDL.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstdlib>
#include <iostream>
#include "core/audio/audio.hpp"
#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 "context_escenes.hpp"
#include "project.h"
#ifndef _WIN32
#include <pwd.h>
#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";
// Inicialitzar opcions amb valors per defecte
Options::init();
// 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);
// Establir ruta del fitxer de configuració
Options::setConfigFile(system_folder_ + "/config.yaml");
// Carregar o crear configuració
Options::loadFromFile();
if (Options::console) {
std::cout << "Configuració carregada\n";
std::cout << " Finestra: " << Options::window.width << "×"
<< Options::window.height << '\n';
std::cout << " Física: rotation=" << Options::physics.rotation_speed
<< " rad/s\n";
}
std::cout << '\n';
}
Director::~Director() {
// Guardar opcions
Options::saveToFile();
// Cleanup audio
Audio::destroy();
// Cleanup SDL
SDL_Quit();
std::cout << "\nAdéu!\n";
}
// Comprovar arguments del programa
auto Director::checkProgramArguments(std::vector<std::string> const& args)
-> std::string {
for (std::size_t i = 1; i < args.size(); ++i) {
const std::string& argument = args[i];
if (argument == "--console") {
Options::console = true;
std::cout << "Mode consola activat\n";
} else if (argument == "--reset-config") {
Options::init();
Options::saveToFile();
std::cout << "Configuració restablida als valors per defecte\n";
}
}
return args[0]; // Retornar ruta de l'executable
}
// Crear carpeta del sistema (específic per plataforma)
void Director::createSystemFolder(const std::string& folder) {
#ifdef _WIN32
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir;
system_folder_ =
std::string(homedir) + "/Library/Application Support/" + folder;
#elif __linux__
struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir;
system_folder_ = std::string(homedir) + "/.config/" + folder;
// CRÍTIC: Crear ~/.config si no existeix
{
std::string config_base_folder = std::string(homedir) + "/.config";
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
if (ret == -1 && errno != EEXIST) {
printf("ERROR: No es pot crear la carpeta ~/.config\n");
exit(EXIT_FAILURE);
}
}
#endif
// Comprovar si la carpeta existeix
struct stat st = {.st_dev = 0};
if (stat(system_folder_.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
int ret = mkdir(system_folder_.c_str());
#else
int ret = mkdir(system_folder_.c_str(), S_IRWXU);
#endif
if (ret == -1) {
switch (errno) {
case EACCES:
printf("ERROR: Permisos denegats creant %s\n", system_folder_.c_str());
exit(EXIT_FAILURE);
case EEXIST:
// La carpeta ja existeix (race condition), continuar
break;
case ENAMETOOLONG:
printf("ERROR: Ruta massa llarga: %s\n", system_folder_.c_str());
exit(EXIT_FAILURE);
default:
perror("mkdir");
exit(EXIT_FAILURE);
}
}
}
if (Options::console) {
std::cout << "Carpeta del sistema: " << system_folder_ << '\n';
}
}
// Bucle principal del joc
auto Director::run() -> int {
// Calculate initial size from saved zoom_factor
int initial_width = static_cast<int>(std::round(
Defaults::Window::WIDTH * Options::window.zoom_factor));
int initial_height = static_cast<int>(std::round(
Defaults::Window::HEIGHT * Options::window.zoom_factor));
// Crear gestor SDL amb configuració de Options
SDLManager sdl(initial_width, initial_height, Options::window.fullscreen);
// 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");
if (Options::console) {
std::cout << "Música precachejada: "
<< 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 (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 Escena::TITOL: {
EscenaTitol titol(sdl, context);
titol.executar();
break;
}
case Escena::JOC: {
EscenaJoc joc(sdl, context);
joc.executar();
break;
}
default:
break;
}
}
// Sincronitzar final amb GestorEscenes::actual
GestorEscenes::actual = Escena::EIXIR;
return 0;
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <vector>
class Director {
public:
explicit Director(std::vector<std::string> const& args);
~Director();
auto run() -> int; // Main game loop
private:
std::string executable_path_;
std::string system_folder_;
static auto checkProgramArguments(std::vector<std::string> const& args)
-> std::string;
void createSystemFolder(const std::string& folder);
};

View File

@@ -0,0 +1,54 @@
// global_events.cpp - Implementació dels events globals
// © 2025 Port a C++20
#include "global_events.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.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, ContextEscenes& context) {
// Tecles globals de finestra (F1/F2/F3)
if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.key) {
case SDLK_F1:
sdl.decreaseWindowSize();
return true;
case SDLK_F2:
sdl.increaseWindowSize();
return true;
case SDLK_F3:
sdl.toggleFullscreen();
return true;
case SDLK_F4:
sdl.toggleVSync();
return true;
case SDLK_ESCAPE:
context.canviar_escena(Escena::EIXIR);
GestorEscenes::actual = Escena::EIXIR;
return true;
default:
break;
}
}
// Tancar finestra
if (event.type == SDL_EVENT_QUIT) {
context.canviar_escena(Escena::EIXIR);
GestorEscenes::actual = Escena::EIXIR;
return true;
}
// Gestió del ratolí (auto-ocultar)
Mouse::handleEvent(event);
return false; // Event no processat
}
} // namespace GlobalEvents

View File

@@ -0,0 +1,17 @@
// global_events.hpp - Events globals del joc
// Basat en el patró del projecte "pollo"
// © 2025 Port a C++20
#pragma once
#include <SDL3/SDL.h>
// 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, GestorEscenes::ContextEscenes& context);
} // namespace GlobalEvents

43
source/core/types.hpp Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#include <array>
#include <cstdint>
#include "core/defaults.hpp"
// Punt polar (coordenades polars)
struct IPunt {
float r; // Radi
float angle; // Angle en radians
};
// Punt cartesià
struct Punt {
float x, y;
};
// ==============================================================================
// DEPRECATED: Legacy types (replaced by Shape system)
// ==============================================================================
// These types are kept temporarily for chatarra_cosmica_ (Phase 10: explosions)
// TODO Phase 10: Replace with particle system or remove completely
// Nau (triangle) - DEPRECATED: Now using Shape system (ship.shp)
struct Triangle {
IPunt p1, p2, p3;
Punt centre;
float angle;
float velocitat;
};
// Polígon (enemics i bales) - DEPRECATED: Now using Shape system (.shp files)
struct Poligon {
std::array<IPunt, Defaults::Entities::MAX_IPUNTS> ipuntx;
Punt centre;
float angle;
float velocitat;
uint8_t n;
float drotacio;
float rotacio;
bool esta;
};

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

@@ -21,25 +21,55 @@ Enemic::Enemic(SDL_Renderer* renderer)
drotacio_(0.0f),
rotacio_(0.0f),
esta_(false),
brightness_(Defaults::Brightness::ENEMIC) {
// [NUEVO] Carregar forma compartida des de fitxer
forma_ = Graphics::ShapeLoader::load("enemy_pentagon.shp");
if (!forma_ || !forma_->es_valida()) {
std::cerr << "[Enemic] Error: no s'ha pogut carregar enemy_pentagon.shp"
<< std::endl;
}
brightness_(Defaults::Brightness::ENEMIC),
tipus_(TipusEnemic::PENTAGON),
tracking_timer_(0.0f),
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() {
// Inicialitzar enemic (pentàgon)
// Copiat de joc_asteroides.cpp línies 41-54
void Enemic::inicialitzar(TipusEnemic tipus, const Punt* ship_pos) {
// Guardar tipus
tipus_ = tipus;
// [NUEVO] Ja no cal crear_poligon_regular - la geometria es carrega del
// fitxer Només inicialitzem l'estat de la instància
// Carregar forma segons el tipus
const char* shape_file;
float drotacio_min, drotacio_max;
// Posició aleatòria dins de l'àrea de joc
// Calcular rangs segurs amb radi de l'enemic
switch (tipus_) {
case TipusEnemic::PENTAGON:
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
velocitat_ = Defaults::Enemies::Pentagon::VELOCITAT;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
break;
case TipusEnemic::QUADRAT:
shape_file = Defaults::Enemies::Quadrat::SHAPE_FILE;
velocitat_ = Defaults::Enemies::Quadrat::VELOCITAT;
drotacio_min = Defaults::Enemies::Quadrat::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Quadrat::DROTACIO_MAX;
tracking_timer_ = 0.0f;
break;
case TipusEnemic::MOLINILLO:
shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE;
velocitat_ = Defaults::Enemies::Molinillo::VELOCITAT;
drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX;
break;
}
// Carregar forma
forma_ = Graphics::ShapeLoader::load(shape_file);
if (!forma_ || !forma_->es_valida()) {
std::cerr << "[Enemic] Error: no s'ha pogut carregar " << shape_file << std::endl;
}
// [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,
@@ -47,32 +77,87 @@ void Enemic::inicialitzar() {
min_y,
max_y);
// Spawn aleatori dins dels límits segurs
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;
// Velocitat (2 px/frame original * 20 FPS = 40 px/s)
velocitat_ = 40.0f;
// Rotació visual aleatòria (rad/s)
// Original Pascal: random * 0.1 rad/frame * 20 FPS ≈ 2 rad/s
drotacio_ = (static_cast<float>(std::rand()) / RAND_MAX) * 2.0f;
// Rotació visual aleatòria (rad/s) dins del rang del tipus
float drotacio_range = drotacio_max - drotacio_min;
drotacio_ = drotacio_min + (static_cast<float>(std::rand()) / RAND_MAX) * drotacio_range;
rotacio_ = 0.0f;
// Inicialitzar estat d'animació
animacio_ = AnimacioEnemic(); // Reset to defaults
animacio_.drotacio_base = drotacio_;
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);
// Actualitzar animacions (palpitació, rotació accelerada)
actualitzar_animacio(delta_time);
// Rotació visual (time-based: drotacio_ està en rad/s)
rotacio_ += drotacio_ * delta_time;
}
@@ -80,21 +165,33 @@ void Enemic::actualitzar(float delta_time) {
void Enemic::dibuixar() const {
if (esta_ && forma_) {
// [NUEVO] Usar render_shape en lloc de rota_pol
Rendering::render_shape(renderer_, forma_, centre_, rotacio_, 1.0f, true, 1.0f, brightness_);
// 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_);
}
}
void Enemic::mou(float delta_time) {
// Moviment autònom d'ORNI (enemic pentàgon)
// Basat EXACTAMENT en el codi Pascal original: ASTEROID.PAS lines 279-293
// Copiat EXACTAMENT de joc_asteroides.cpp línies 348-394
//
// IMPORTANT: El Pascal original NO té canvi aleatori continu!
// Només ajusta l'angle quan toca una paret.
// Dispatcher: crida el comportament específic segons el tipus
switch (tipus_) {
case TipusEnemic::PENTAGON:
comportament_pentagon(delta_time);
break;
case TipusEnemic::QUADRAT:
comportament_quadrat(delta_time);
break;
case TipusEnemic::MOLINILLO:
comportament_molinillo(delta_time);
break;
}
}
void Enemic::comportament_pentagon(float delta_time) {
// Pentagon: zigzag esquivador (frequent direction changes)
// Similar a comportament original però amb probabilitat més alta
// Calcular nova posició PROPUESTA (time-based, però lògica Pascal)
// velocitat_ ja està en px/s (40 px/s), multiplicar per delta_time
float velocitat_efectiva = velocitat_ * delta_time;
// Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt)
@@ -104,7 +201,7 @@ void Enemic::mou(float delta_time) {
float new_y = centre_.y + dy;
float new_x = centre_.x + dx;
// Obtenir límits segurs compensant el radi de l'enemic
// Obtenir límits segurs
float min_x, max_x, min_y, max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
@@ -112,33 +209,306 @@ void Enemic::mou(float delta_time) {
min_y,
max_y);
// Lògica Pascal: Actualitza Y si dins, sinó ajusta angle aleatòriament
// if (dy>marge_dalt) and (dy<marge_baix) then orni.centre.y:=round(Dy)
// else orni.angle:=orni.angle+(random(256)/512)*(random(3)-1);
// CORRECCIÓ: Usar inequalitats inclusives (>= i <=) per evitar fugides
// Zigzag: canvi d'angle més freqüent en tocar límits
if (new_y >= min_y && new_y <= max_y) {
centre_.y = new_y;
} else {
// Pequeño ajuste aleatorio: (random(256)/512)*(random(3)-1)
// random(256) = 0..255, /512 = 0..0.498
// random(3) = 0,1,2, -1 = -1,0,1
// Resultado: ±0.5 rad aprox
float rand1 = (static_cast<float>(std::rand() % 256) / 512.0f);
int rand2 = (std::rand() % 3) - 1; // -1, 0, o 1
angle_ += rand1 * static_cast<float>(rand2);
// Probabilitat més alta de canvi d'angle
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
// Lògica Pascal: Actualitza X si dins, sinó ajusta angle aleatòriament
// if (dx>marge_esq) and (dx<marge_dret) then orni.centre.x:=round(Dx)
// else orni.angle:=orni.angle+(random(256)/512)*(random(3)-1);
// CORRECCIÓ: Usar inequalitats inclusives (>= i <=) per evitar fugides
if (new_x >= min_x && new_x <= max_x) {
centre_.x = new_x;
} else {
float rand1 = (static_cast<float>(std::rand() % 256) / 512.0f);
int rand2 = (std::rand() % 3) - 1;
angle_ += rand1 * static_cast<float>(rand2);
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
}
void Enemic::comportament_quadrat(float delta_time) {
// Quadrat: perseguidor (tracks player position)
// Update tracking timer
tracking_timer_ += delta_time;
// Periodically update angle toward ship
if (tracking_timer_ >= Defaults::Enemies::Quadrat::TRACKING_INTERVAL) {
tracking_timer_ = 0.0f;
if (ship_position_) {
// Calculate angle to ship
float dx = ship_position_->x - centre_.x;
float dy = ship_position_->y - centre_.y;
float target_angle = std::atan2(dy, dx) + Constants::PI / 2.0f;
// Interpolate toward target angle
float angle_diff = target_angle - angle_;
// Normalize angle difference to [-π, π]
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 (uses member variable, defaults to 0.5)
angle_ += angle_diff * tracking_strength_;
}
}
// Nota: La rotació visual (rotacio_ += drotacio_) ja es fa a actualitzar()
// Move in current direction
float velocitat_efectiva = velocitat_ * delta_time;
float dy = velocitat_efectiva * std::sin(angle_ - Constants::PI / 2.0f);
float dx = velocitat_efectiva * std::cos(angle_ - Constants::PI / 2.0f);
float new_y = centre_.y + dy;
float new_x = centre_.x + dx;
// Obtenir límits segurs
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);
// Bounce on walls (simple reflection)
if (new_y >= min_y && new_y <= max_y) {
centre_.y = new_y;
} else {
angle_ = -angle_; // Vertical reflection
}
if (new_x >= min_x && new_x <= max_x) {
centre_.x = new_x;
} else {
angle_ = Constants::PI - angle_; // Horizontal reflection
}
}
void Enemic::comportament_molinillo(float delta_time) {
// Molinillo: agressiu (fast, straight lines, proximity spin-up)
// Check proximity to ship for spin-up effect
if (ship_position_) {
float dx = ship_position_->x - centre_.x;
float dy = ship_position_->y - centre_.y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) {
// Temporarily boost rotation speed when near ship
float boost = Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER;
drotacio_ = animacio_.drotacio_base * boost;
} else {
// Normal rotation speed
drotacio_ = animacio_.drotacio_base;
}
}
// Fast straight-line movement
float velocitat_efectiva = velocitat_ * delta_time;
float dy = velocitat_efectiva * std::sin(angle_ - Constants::PI / 2.0f);
float dx = velocitat_efectiva * std::cos(angle_ - Constants::PI / 2.0f);
float new_y = centre_.y + dy;
float new_x = centre_.x + dx;
// Obtenir límits segurs
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);
// Rare angle changes on wall hits
if (new_y >= min_y && new_y <= max_y) {
centre_.y = new_y;
} else {
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
if (new_x >= min_x && new_x <= max_x) {
centre_.x = new_x;
} else {
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
}
void Enemic::actualitzar_animacio(float delta_time) {
actualitzar_palpitacio(delta_time);
actualitzar_rotacio_accelerada(delta_time);
}
void Enemic::actualitzar_palpitacio(float delta_time) {
if (animacio_.palpitacio_activa) {
// Advance phase (2π * frequency * dt)
animacio_.palpitacio_fase += 2.0f * Constants::PI * animacio_.palpitacio_frequencia * delta_time;
// Decrement timer
animacio_.palpitacio_temps_restant -= delta_time;
// Deactivate when timer expires
if (animacio_.palpitacio_temps_restant <= 0.0f) {
animacio_.palpitacio_activa = false;
}
} else {
// Random trigger (probability per second)
float rand_val = static_cast<float>(std::rand()) / RAND_MAX;
float trigger_prob = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time;
if (rand_val < trigger_prob) {
// Activate palpitation
animacio_.palpitacio_activa = true;
animacio_.palpitacio_fase = 0.0f;
// Randomize parameters
float freq_range = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX -
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN +
(static_cast<float>(std::rand()) / RAND_MAX) * freq_range;
float amp_range = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX -
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN;
animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN +
(static_cast<float>(std::rand()) / RAND_MAX) * amp_range;
float dur_range = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX -
Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN;
animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN +
(static_cast<float>(std::rand()) / RAND_MAX) * dur_range;
}
}
}
void Enemic::actualitzar_rotacio_accelerada(float delta_time) {
if (animacio_.drotacio_t < 1.0f) {
// Transitioning to new target
animacio_.drotacio_t += delta_time / animacio_.drotacio_duracio;
if (animacio_.drotacio_t >= 1.0f) {
animacio_.drotacio_t = 1.0f;
animacio_.drotacio_base = animacio_.drotacio_objetivo; // Reached target
drotacio_ = animacio_.drotacio_base;
} else {
// Smoothstep interpolation: t² * (3 - 2t)
float t = animacio_.drotacio_t;
float smooth_t = t * t * (3.0f - 2.0f * t);
// Interpolate between base and target
float initial = animacio_.drotacio_base;
float target = animacio_.drotacio_objetivo;
drotacio_ = initial + (target - initial) * smooth_t;
}
} else {
// Random trigger for new acceleration
float rand_val = static_cast<float>(std::rand()) / RAND_MAX;
float trigger_prob = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time;
if (rand_val < trigger_prob) {
// Start new transition
animacio_.drotacio_t = 0.0f;
// Randomize target speed (multiplier * base)
float mult_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN;
float multiplier = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN +
(static_cast<float>(std::rand()) / RAND_MAX) * mult_range;
animacio_.drotacio_objetivo = animacio_.drotacio_base * multiplier;
// Randomize duration
float dur_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN;
animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN +
(static_cast<float>(std::rand()) / RAND_MAX) * dur_range;
}
}
}
float Enemic::calcular_escala_actual() const {
float escala = 1.0f;
// [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,30 @@
#include "core/graphics/shape.hpp"
#include "core/types.hpp"
#include "game/constants.hpp"
// Tipus d'enemic
enum class TipusEnemic : uint8_t {
PENTAGON = 0, // Pentàgon esquivador (zigzag)
QUADRAT = 1, // Quadrat perseguidor (tracks ship)
MOLINILLO = 2 // Molinillo agressiu (fast, spinning)
};
// Estat d'animació (palpitació i rotació accelerada)
struct AnimacioEnemic {
// Palpitation (breathing effect)
bool palpitacio_activa = false;
float palpitacio_fase = 0.0f; // Phase in cycle (0.0-2π)
float palpitacio_frequencia = 2.0f; // Hz (cycles per second)
float palpitacio_amplitud = 0.15f; // Scale variation (±15%)
float palpitacio_temps_restant = 0.0f; // Time remaining (seconds)
// Rotation acceleration (long-term spin modulation)
float drotacio_base = 0.0f; // Base rotation speed (rad/s)
float drotacio_objetivo = 0.0f; // Target rotation speed (rad/s)
float drotacio_t = 0.0f; // Interpolation progress (0.0-1.0)
float drotacio_duracio = 0.0f; // Duration of transition (seconds)
};
class Enemic {
public:
@@ -16,7 +40,7 @@ class Enemic {
: renderer_(nullptr) {}
Enemic(SDL_Renderer* renderer);
void inicialitzar();
void inicialitzar(TipusEnemic tipus = TipusEnemic::PENTAGON, const Punt* ship_pos = nullptr);
void actualitzar(float delta_time);
void dibuixar() const;
@@ -25,6 +49,31 @@ 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_;
@@ -34,12 +83,37 @@ class Enemic {
// [NUEVO] Estat de la instància (separat de la geometria)
Punt centre_;
float angle_; // Angle de moviment
float angle_; // Angle de moviment
float velocitat_;
float drotacio_; // Delta rotació visual (rad/s)
float rotacio_; // Rotació visual acumulada
float drotacio_; // Delta rotació visual (rad/s)
float rotacio_; // Rotació visual acumulada
bool esta_;
float brightness_; // Factor de brillantor (0.0-1.0)
// [NEW] Enemy type and configuration
TipusEnemic tipus_;
// [NEW] Animation state
AnimacioEnemic animacio_;
// [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);
// [NEW] Private methods
void actualitzar_animacio(float delta_time);
void actualitzar_palpitacio(float delta_time);
void actualitzar_rotacio_accelerada(float delta_time);
void comportament_pentagon(float delta_time);
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,14 +22,14 @@ 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;
}
}
void Nau::inicialitzar() {
void Nau::inicialitzar(const Punt* spawn_point) {
// Inicialització de la nau (triangle)
// Basat en el codi Pascal original: lines 380-384
// Copiat de joc_asteroides.cpp línies 30-44
@@ -37,11 +37,17 @@ void Nau::inicialitzar() {
// [NUEVO] Ja no cal configurar punts polars - la geometria es carrega del
// fitxer Només inicialitzem l'estat de la instància
// Posició inicial al centre de l'àrea de joc
float centre_x, centre_y;
Constants::obtenir_centre_zona(centre_x, centre_y);
centre_.x = centre_x; // 320
centre_.y = centre_y; // 213 (not 240!)
// Use custom spawn point if provided, otherwise use center
if (spawn_point) {
centre_.x = spawn_point->x;
centre_.y = spawn_point->y;
} else {
// Default: center of play area
float centre_x, centre_y;
Constants::obtenir_centre_zona(centre_x, centre_y);
centre_.x = static_cast<int>(centre_x);
centre_.y = static_cast<int>(centre_y);
}
// Estat inicial
angle_ = 0.0f;

View File

@@ -9,6 +9,7 @@
#include "core/graphics/shape.hpp"
#include "core/types.hpp"
#include "game/constants.hpp"
class Nau {
public:
@@ -16,7 +17,7 @@ class Nau {
: renderer_(nullptr) {}
Nau(SDL_Renderer* renderer);
void inicialitzar();
void inicialitzar(const Punt* spawn_point = nullptr);
void processar_input(float delta_time);
void actualitzar(float delta_time);
void dibuixar() const;
@@ -25,6 +26,14 @@ class Nau {
const Punt& get_centre() const { return centre_; }
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; }
@@ -38,8 +47,8 @@ class Nau {
// [NUEVO] Estat de la instància (separat de la geometria)
Punt centre_;
float angle_; // Angle d'orientació
float velocitat_; // Velocitat (px/s)
float angle_; // Angle d'orientació
float velocitat_; // Velocitat (px/s)
bool esta_tocada_;
float brightness_; // Factor de brillantor (0.0-1.0)

View File

@@ -10,17 +10,31 @@
#include <iostream>
#include <vector>
#include "../../core/audio/audio.hpp"
#include "../../core/rendering/line_renderer.hpp"
#include "../../core/system/gestor_escenes.hpp"
#include "../../core/system/global_events.hpp"
#include "core/audio/audio.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/line_renderer.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());
@@ -41,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;
@@ -55,6 +69,9 @@ void EscenaJoc::executar() {
// Actualitzar comptador de FPS
sdl_.updateFPS(delta_time);
// Actualitzar visibilitat del cursor (auto-ocultar)
Mouse::updateCursorVisibility();
// Processar events SDL
while (SDL_PollEvent(&event)) {
// Manejo de finestra
@@ -63,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;
}
@@ -83,6 +100,9 @@ void EscenaJoc::executar() {
// Netejar pantalla (usa color oscil·lat)
sdl_.neteja(0, 0, 0);
// Actualitzar context de renderitzat (factor d'escala global)
sdl_.updateRenderingContext();
// Dibuixar joc
dibuixar();
@@ -98,70 +118,312 @@ 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;
// Initialize lives and game over state
num_vides_ = Defaults::Game::STARTING_LIVES;
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)
// [MODIFIED] Initialize enemies as inactive (stage system will spawn them)
for (auto& enemy : orni_) {
enemy.inicialitzar();
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
for (auto& bala : bales_) {
bala.inicialitzar();
}
// Iniciar música de joc (sense stopMusic, ja s'ha parat en destructor de TITOL)
Audio::get()->playMusic("game.ogg");
}
void EscenaJoc::actualitzar(float delta_time) {
// Actualitzar nau (input + física)
nau_.processar_input(delta_time);
nau_.actualitzar(delta_time);
// Check game over state first
if (game_over_) {
// Game over: only update timer, enemies, bullets, and debris
game_over_timer_ -= delta_time;
// Actualitzar moviment i rotació dels enemics (ORNIs)
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
if (game_over_timer_ <= 0.0f) {
// Aturar música de joc abans de tornar al títol
Audio::get()->stopMusic();
// Transició a pantalla de títol
context_.canviar_escena(Escena::TITOL);
GestorEscenes::actual = Escena::TITOL;
return;
}
// Enemies and bullets continue moving during game over
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time);
return;
}
// Actualitzar moviment de bales (Fase 9)
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
// Check death sequence state
if (itocado_ > 0.0f) {
// Death sequence active: update timer
itocado_ += delta_time;
// Check if death duration completed
if (itocado_ >= Defaults::Game::DEATH_DURATION) {
// *** PHASE 3: RESPAWN OR GAME OVER ***
// Decrement lives
num_vides_--;
if (num_vides_ > 0) {
// Respawn ship
nau_.inicialitzar(&punt_spawn_);
itocado_ = 0.0f;
} else {
// Game over
game_over_ = true;
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION;
itocado_ = 0.0f;
}
}
// Enemies and bullets continue moving during death sequence
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time);
return;
}
// Detectar col·lisions bala-enemic (Fase 10)
detectar_col·lisions_bales_enemics();
// *** STAGE SYSTEM STATE MACHINE ***
// Actualitzar fragments d'explosions
debris_manager_.actualitzar(delta_time);
StageSystem::EstatStage estat = stage_manager_->get_estat();
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;
}
}
void EscenaJoc::dibuixar() {
// Dibuixar marges de la zona de joc
// Draw borders (always visible)
dibuixar_marges();
// Dibuixar nau
nau_.dibuixar();
// Check game over state
if (game_over_) {
// Game over: draw enemies, bullets, debris, and "GAME OVER" text
// Dibuixar ORNIs (enemics)
for (const auto& enemy : orni_) {
enemy.dibuixar();
for (const auto& enemy : orni_) {
enemy.dibuixar();
}
for (const auto& bala : bales_) {
bala.dibuixar();
}
debris_manager_.dibuixar();
gestor_puntuacio_.dibuixar();
// Draw centered "GAME OVER" text
const std::string game_over_text = "GAME OVER";
constexpr float escala = 2.0f;
constexpr float spacing = 4.0f;
float text_width = text_.get_text_width(game_over_text, escala, spacing);
float text_height = text_.get_text_height(escala);
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
float x = play_area.x + (play_area.w - text_width) / 2.0f;
float y = play_area.y + (play_area.h - text_height) / 2.0f;
text_.render(game_over_text, {x, y}, escala, spacing);
dibuixar_marcador();
return;
}
// Dibuixar bales (Fase 9)
for (const auto& bala : bales_) {
bala.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;
}
// Dibuixar fragments d'explosions (després d'altres objectes)
debris_manager_.dibuixar();
// Dibuixar marcador
dibuixar_marcador();
}
void EscenaJoc::processar_input(const SDL_Event& event) {
// Ignore ship controls during game over
if (game_over_) {
return;
}
// Ignore ship controls during death sequence
if (itocado_ > 0.0f) {
return;
}
// Processament d'input per events puntuals (no continus)
// L'input continu (fletxes) es processa en actualitzar() amb
// SDL_GetKeyboardState()
@@ -210,7 +472,41 @@ void EscenaJoc::processar_input(const SDL_Event& event) {
}
void EscenaJoc::tocado() {
// TODO: Implementar seqüència de mort
// Death sequence: 3 phases
// Phase 1: First call (itocado_ == 0) - trigger explosion
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
// Phase 3: Respawn or game over (itocado_ >= 3.0s) - handled in actualitzar()
if (itocado_ == 0.0f) {
// *** PHASE 1: TRIGGER DEATH ***
// Mark ship as dead (stops rendering and input)
nau_.marcar_tocada();
// 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
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)
itocado_ = 0.001f;
}
// Phase 2 is automatic (debris updates in actualitzar())
// Phase 3 is handled in actualitzar() when itocado_ >= DEATH_DURATION
}
void EscenaJoc::dibuixar_marges() const {
@@ -231,8 +527,17 @@ void EscenaJoc::dibuixar_marges() const {
}
void EscenaJoc::dibuixar_marcador() {
// Text estàtic (hardcoded)
const std::string text = "SCORE: 01000 LIFE: 3 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;
@@ -261,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_) {
@@ -277,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)
@@ -288,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
@@ -309,3 +644,112 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() {
}
}
}
void EscenaJoc::detectar_col·lisio_nau_enemics() {
// Only check collisions if ship is alive
if (!nau_.esta_viva()) {
return;
}
// Generous collision detection (80% hitbox)
constexpr float RADI_NAU = Defaults::Entities::SHIP_RADIUS;
constexpr float RADI_ENEMIC = Defaults::Entities::ENEMY_RADIUS;
constexpr float SUMA_RADIS =
(RADI_NAU + RADI_ENEMIC) * Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS;
const Punt& pos_nau = nau_.get_centre();
// Check collision with all active enemies
for (const auto& enemic : orni_) {
if (!enemic.esta_actiu()) {
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)
float dx = static_cast<float>(pos_nau.x - pos_enemic.x);
float dy = static_cast<float>(pos_nau.y - pos_enemic.y);
float distancia_quadrada = dx * dx + dy * dy;
// Check collision
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
tocado(); // Trigger death sequence
return; // Only one collision per frame
}
}
}
// [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

@@ -10,19 +10,24 @@
#include <array>
#include <cstdint>
#include "../../core/graphics/vector_text.hpp"
#include "../../core/rendering/sdl_manager.hpp"
#include "../../core/types.hpp"
#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,25 +38,42 @@ class EscenaJoc {
private:
SDLManager& sdl_;
GestorEscenes::ContextEscenes& context_;
// Efectes visuals
Effects::DebrisManager debris_manager_;
Effects::GestorPuntuacioFlotant gestor_puntuacio_;
// Estat del joc
Nau nau_;
std::array<Enemic, Constants::MAX_ORNIS> orni_;
std::array<Bala, Constants::MAX_BALES> bales_;
Poligon chatarra_cosmica_;
uint16_t itocado_;
float itocado_; // Death timer (seconds)
// Lives and game over system
int num_vides_; // Current lives count
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

@@ -11,10 +11,16 @@
#include "core/audio/audio.hpp"
#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) {
@@ -37,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();
}
@@ -53,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;
@@ -67,6 +79,9 @@ void EscenaLogo::executar() {
// Actualitzar comptador de FPS
sdl_.updateFPS(delta_time);
// Actualitzar visibilitat del cursor (auto-ocultar)
Mouse::updateCursorVisibility();
// Processar events SDL
while (SDL_PollEvent(&event)) {
// Manejo de finestra
@@ -75,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;
}
@@ -89,6 +104,9 @@ void EscenaLogo::executar() {
// Actualitzar colors oscil·lats (efecte verd global)
sdl_.updateColors(delta_time);
// Actualitzar context de renderitzat (factor d'escala global)
sdl_.updateRenderingContext();
// Dibuixar
dibuixar();
}
@@ -193,6 +211,10 @@ void EscenaLogo::canviar_estat(EstatAnimacio nou_estat) {
std::mt19937 g(rd());
std::shuffle(ordre_explosio_.begin(), ordre_explosio_.end(), g);
}
else if (nou_estat == EstatAnimacio::POST_EXPLOSION)
{
Audio::get()->playMusic("title.ogg");
}
std::cout << "[EscenaLogo] Canvi a estat: " << static_cast<int>(nou_estat)
<< "\n";
@@ -218,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";
@@ -281,7 +305,9 @@ void EscenaLogo::actualitzar(float delta_time) {
case EstatAnimacio::POST_EXPLOSION:
if (temps_estat_actual_ >= DURACIO_POST_EXPLOSION) {
GestorEscenes::actual = GestorEscenes::Escena::TITOL;
// Transició a pantalla de títol
context_.canviar_escena(Escena::TITOL);
GestorEscenes::actual = Escena::TITOL;
}
break;
}
@@ -382,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

@@ -9,15 +9,16 @@
#include <memory>
#include <vector>
#include "../../core/graphics/shape.hpp"
#include "../../core/rendering/sdl_manager.hpp"
#include "../../core/types.hpp"
#include "../effects/debris_manager.hpp"
#include "game/effects/debris_manager.hpp"
#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

@@ -3,28 +3,53 @@
#include "escena_titol.hpp"
#include <cfloat>
#include <cmath>
#include <iostream>
#include <string>
#include "../../core/audio/audio.hpp"
#include "../../core/system/gestor_escenes.hpp"
#include "../../core/system/global_events.hpp"
#include "build/project.h"
#include "core/audio/audio.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/context_escenes.hpp"
#include "core/system/global_events.hpp"
#include "project.h"
EscenaTitol::EscenaTitol(SDLManager& sdl)
// Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes;
using Escena = ContextEscenes::Escena;
using Opcio = ContextEscenes::Opcio;
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,
Defaults::Game::HEIGHT / 2.0f};
SDL_FRect area_completa{
0, 0,
0,
0,
static_cast<float>(Defaults::Game::WIDTH),
static_cast<float>(Defaults::Game::HEIGHT)};
@@ -34,13 +59,185 @@ EscenaTitol::EscenaTitol(SDLManager& sdl)
area_completa,
150 // densitat: 150 estrelles (50 per capa)
);
// 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();
// Iniciar música de títol si no està sonant
if (Audio::get()->getMusicState() != Audio::MusicState::PLAYING) {
Audio::get()->playMusic("title.ogg");
}
}
EscenaTitol::~EscenaTitol() {
// Aturar música de títol quan es destrueix l'escena
Audio::get()->stopMusic();
}
void EscenaTitol::inicialitzar_titol() {
using namespace Graphics;
// === LÍNIA 1: "ORNI" ===
std::vector<std::string> fitxers_orni = {
"title/letra_o.shp",
"title/letra_r.shp",
"title/letra_n.shp",
"title/letra_i.shp"};
// Pas 1: Carregar formes i calcular amplades per "ORNI"
float ancho_total_orni = 0.0f;
for (const auto& fitxer : fitxers_orni) {
auto forma = ShapeLoader::load(fitxer);
if (!forma || !forma->es_valida()) {
std::cerr << "[EscenaTitol] Error carregant " << fitxer << std::endl;
continue;
}
// Calcular bounding box de la forma (trobar ancho i altura)
float min_x = FLT_MAX;
float max_x = -FLT_MAX;
float min_y = FLT_MAX;
float max_y = -FLT_MAX;
for (const auto& prim : forma->get_primitives()) {
for (const auto& punt : prim.points) {
min_x = std::min(min_x, punt.x);
max_x = std::max(max_x, punt.x);
min_y = std::min(min_y, punt.y);
max_y = std::max(max_y, punt.y);
}
}
float ancho_sin_escalar = max_x - min_x;
float altura_sin_escalar = max_y - min_y;
// Escalar ancho, altura i offset amb ESCALA_TITULO
float ancho = ancho_sin_escalar * ESCALA_TITULO;
float altura = altura_sin_escalar * ESCALA_TITULO;
float offset_centre = (forma->get_centre().x - min_x) * ESCALA_TITULO;
lletres_orni_.push_back({forma, {0.0f, 0.0f}, ancho, altura, offset_centre});
ancho_total_orni += ancho;
}
// Afegir espaiat entre lletres
ancho_total_orni += ESPAI_ENTRE_LLETRES * (lletres_orni_.size() - 1);
// Calcular posició inicial (centrat horitzontal) per "ORNI"
float x_inicial_orni = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0f;
float x_actual = x_inicial_orni;
for (auto& lletra : lletres_orni_) {
lletra.posicio.x = x_actual + lletra.offset_centre;
lletra.posicio.y = Y_ORNI;
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
}
std::cout << "[EscenaTitol] Línia 1 (ORNI): " << lletres_orni_.size()
<< " lletres, ancho total: " << ancho_total_orni << " px\n";
// === Calcular posició Y dinàmica per "ATTACK!" ===
// Totes les lletres ORNI tenen la mateixa altura, utilitzem la primera
float altura_orni = lletres_orni_.empty() ? 50.0f : lletres_orni_[0].altura;
y_attack_dinamica_ = Y_ORNI + altura_orni + SEPARACION_LINEAS;
std::cout << "[EscenaTitol] Altura ORNI: " << altura_orni
<< " px, Y_ATTACK dinàmica: " << y_attack_dinamica_ << " px\n";
// === LÍNIA 2: "ATTACK!" ===
std::vector<std::string> fitxers_attack = {
"title/letra_a.shp",
"title/letra_t.shp",
"title/letra_t.shp", // T repetida
"title/letra_a.shp", // A repetida
"title/letra_c.shp",
"title/letra_k.shp",
"title/letra_exclamacion.shp"};
// Pas 1: Carregar formes i calcular amplades per "ATTACK!"
float ancho_total_attack = 0.0f;
for (const auto& fitxer : fitxers_attack) {
auto forma = ShapeLoader::load(fitxer);
if (!forma || !forma->es_valida()) {
std::cerr << "[EscenaTitol] Error carregant " << fitxer << std::endl;
continue;
}
// Calcular bounding box de la forma (trobar ancho i altura)
float min_x = FLT_MAX;
float max_x = -FLT_MAX;
float min_y = FLT_MAX;
float max_y = -FLT_MAX;
for (const auto& prim : forma->get_primitives()) {
for (const auto& punt : prim.points) {
min_x = std::min(min_x, punt.x);
max_x = std::max(max_x, punt.x);
min_y = std::min(min_y, punt.y);
max_y = std::max(max_y, punt.y);
}
}
float ancho_sin_escalar = max_x - min_x;
float altura_sin_escalar = max_y - min_y;
// Escalar ancho, altura i offset amb ESCALA_TITULO
float ancho = ancho_sin_escalar * ESCALA_TITULO;
float altura = altura_sin_escalar * ESCALA_TITULO;
float offset_centre = (forma->get_centre().x - min_x) * ESCALA_TITULO;
lletres_attack_.push_back({forma, {0.0f, 0.0f}, ancho, altura, offset_centre});
ancho_total_attack += ancho;
}
// Afegir espaiat entre lletres
ancho_total_attack += ESPAI_ENTRE_LLETRES * (lletres_attack_.size() - 1);
// Calcular posició inicial (centrat horitzontal) per "ATTACK!"
float x_inicial_attack = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0f;
x_actual = x_inicial_attack;
for (auto& lletra : lletres_attack_) {
lletra.posicio.x = x_actual + lletra.offset_centre;
lletra.posicio.y = y_attack_dinamica_; // Usar posició dinàmica
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
}
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;
@@ -54,6 +251,9 @@ void EscenaTitol::executar() {
// Actualitzar comptador de FPS
sdl_.updateFPS(delta_time);
// Actualitzar visibilitat del cursor (auto-ocultar)
Mouse::updateCursorVisibility();
// Processar events SDL
while (SDL_PollEvent(&event)) {
// Manejo de finestra
@@ -62,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;
}
@@ -82,6 +282,9 @@ void EscenaTitol::executar() {
// Netejar pantalla
sdl_.neteja(0, 0, 0);
// Actualitzar context de renderitzat (factor d'escala global)
sdl_.updateRenderingContext();
// Dibuixar
dibuixar();
@@ -99,15 +302,101 @@ 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
case EstatTitol::MAIN: {
temps_estat_main_ += delta_time;
// 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 = 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));
}
}
}
@@ -117,27 +406,121 @@ void EscenaTitol::dibuixar() {
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: Dibuixar text de títol i copyright (sobre el starfield)
if (estat_actual_ == EstatTitol::MAIN) {
// Text principal centrat (vertical i horitzontalment)
const std::string main_text = "PRESS BUTTON TO PLAY";
const float escala_main = 1.0f;
const float spacing = 2.0f;
// 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
float text_width = text_.get_text_width(main_text, escala_main, spacing);
float text_height = text_.get_text_height(escala_main);
// 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;
float x_center = (Defaults::Game::WIDTH - text_width) / 2.0f;
float y_center = (Defaults::Game::HEIGHT - text_height) / 2.0f;
// 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;
text_.render(main_text, Punt{x_center, y_center}, escala_main, spacing);
// === RENDERITZAR OMBRA PRIMER (darrera del logo principal) ===
// Copyright a la part inferior (centrat horitzontalment)
// 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_) {
Rendering::render_shape(
sdl_.obte_renderer(),
lletra.forma,
lletra.posicio,
0.0f,
ESCALA_TITULO,
true,
1.0f // Brillantor completa
);
}
// Dibuixar "ATTACK!" (línia 2)
for (const auto& lletra : lletres_attack_) {
Rendering::render_shape(
sdl_.obte_renderer(),
lletra.forma,
lletra.posicio,
0.0f,
ESCALA_TITULO,
true,
1.0f // Brillantor completa
);
}
// === Text "PRESS BUTTON TO PLAY" ===
// En estat MAIN: sempre visible
// En estat TRANSITION: parpellejant (blink amb sinusoide)
const float spacing = 2.0f; // Espai entre caràcters (usat també per copyright)
bool mostrar_text = true;
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);
}
if (mostrar_text) {
const std::string main_text = "PRESS BUTTON TO PLAY";
const float escala_main = 1.0f;
float text_width = text_.get_text_width(main_text, escala_main, spacing);
float x_center = (Defaults::Game::WIDTH - text_width) / 2.0f;
float altura_attack = lletres_attack_.empty() ? 50.0f : lletres_attack_[0].altura;
float y_center = y_attack_dinamica_ + altura_attack + 70.0f;
text_.render(main_text, Punt{x_center, y_center}, escala_main, spacing);
}
// === Copyright a la part inferior (centrat horitzontalment) ===
// Convert to uppercase since VectorText only supports A-Z
std::string copyright = Project::COPYRIGHT;
for (char& c : copyright) {
@@ -161,15 +544,35 @@ void EscenaTitol::processar_events(const SDL_Event& event) {
// Qualsevol tecla o clic de ratolí
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:
// Anar al joc
GestorEscenes::actual = GestorEscenes::Escena::JOC;
// 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_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_TO_GAME:
// Ignorar inputs durant la transició
break;
}
}

View File

@@ -7,35 +7,95 @@
#include <SDL3/SDL.h>
#include <memory>
#include <vector>
#include "../../core/graphics/starfield.hpp"
#include "../../core/graphics/vector_text.hpp"
#include "../../core/rendering/sdl_manager.hpp"
#include "core/defaults.hpp"
#include "core/graphics/shape.hpp"
#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
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
struct LetraLogo {
std::shared_ptr<Graphics::Shape> forma; // Forma vectorial de la lletra
Punt posicio; // Posició en pantalla
float ancho; // Amplada escalada
float altura; // Altura escalada
float offset_centre; // Offset del centre per posicionament
};
SDLManager& sdl_;
Graphics::VectorText text_; // Sistema de text vectorial
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
float temps_acumulat_; // Temps acumulat per l'estat INIT
EstatTitol estat_actual_; // Estat actual de la màquina
float temps_acumulat_; // Temps acumulat per l'estat INIT
// Lletres del títol "ORNI ATTACK!"
std::vector<LetraLogo> lletres_orni_; // Lletres de "ORNI" (línia 1)
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 DURACIO_INIT = 2.0f; // Duració de l'estat INIT (2 segons)
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%)
static constexpr float ESPAI_ENTRE_LLETRES = 10.0f; // Espai entre lletres
static constexpr float Y_ORNI = 150.0f; // Posició Y de "ORNI"
static constexpr float SEPARACION_LINEAS = 10.0f; // Separació entre "ORNI" i "ATTACK!" (0.0f = pegades)
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

@@ -4,8 +4,8 @@
#include <iostream>
#include <string>
#include "../core/defaults.hpp"
#include "../external/fkyaml_node.hpp"
#include "core/defaults.hpp"
#include "external/fkyaml_node.hpp"
#include "project.h"
namespace Options {

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;
}

View File

@@ -44,8 +44,9 @@ def apply_transform(punto, matrix):
def parse_svg_path(d_attr):
"""
Convierte comandos SVG path (M y L) a lista de puntos [(x, y), ...]
Ejemplo: "M896,1693L896,1531.23L1219.53,1531.23..." → [(896, 1693), (896, 1531.23), ...]
Convierte comandos SVG path (M y L) a lista de polylines separadas.
Cada comando M inicia una nueva polyline.
Retorna: lista de listas de puntos [[(x,y), ...], [(x,y), ...], ...]
"""
# Reemplazar comas por espacios para facilitar parsing
d_attr = d_attr.replace(',', ' ')
@@ -55,7 +56,9 @@ def parse_svg_path(d_attr):
d_attr = re.sub(r'([ML])', r'|\1', d_attr)
commands = [c.strip() for c in d_attr.split('|') if c.strip()]
points = []
polylines = [] # Lista de polylines
current_polyline = [] # Polyline actual
for cmd in commands:
if not cmd:
continue
@@ -70,18 +73,29 @@ def parse_svg_path(d_attr):
# Parsear pares de coordenadas
coords = coords_str.split()
# Si es comando M (MoveTo), empezar nueva polyline
if cmd_letter == 'M':
# Guardar polyline anterior si tiene puntos
if current_polyline:
polylines.append(current_polyline)
current_polyline = []
# Procesar en pares (x, y)
i = 0
while i < len(coords) - 1:
try:
x = float(coords[i])
y = float(coords[i + 1])
points.append((x, y))
current_polyline.append((x, y))
i += 2
except (ValueError, IndexError):
i += 1
return points
# No olvidar la última polyline
if current_polyline:
polylines.append(current_polyline)
return polylines
def rect_to_points(rect_elem):
@@ -103,11 +117,22 @@ def rect_to_points(rect_elem):
]
def calc_bounding_box(puntos):
def calc_bounding_box(polylines):
"""
Calcula bounding box de una lista de puntos
Calcula bounding box de una o varias polylines
polylines puede ser:
- lista de puntos [(x,y), ...]
- lista de polylines [[(x,y), ...], [(x,y), ...]]
Retorna: (min_x, max_x, min_y, max_y, ancho, alto)
"""
# Aplanar si es lista de polylines
puntos = []
if polylines and isinstance(polylines[0], list):
for polyline in polylines:
puntos.extend(polyline)
else:
puntos = polylines
if not puntos:
return (0, 0, 0, 0, 0, 0)
@@ -125,17 +150,18 @@ def calc_bounding_box(puntos):
return (min_x, max_x, min_y, max_y, ancho, alto)
def normalizar_letra(nombre, puntos, altura_objetivo=100.0):
def normalizar_letra(nombre, polylines, altura_objetivo=100.0):
"""
Escala y traslada letra para que tenga altura_objetivo pixels
y esté centrada en origen (0, 0) en esquina superior izquierda
Retorna: dict con puntos normalizados, centro, ancho, alto
polylines: lista de polylines [[(x,y), ...], [(x,y), ...]]
Retorna: dict con polylines normalizadas, centro, ancho, alto
"""
if not puntos:
if not polylines:
return None
min_x, max_x, min_y, max_y, ancho, alto = calc_bounding_box(puntos)
min_x, max_x, min_y, max_y, ancho, alto = calc_bounding_box(polylines)
if alto == 0:
print(f" [WARN] Letra {nombre}: altura cero")
@@ -144,14 +170,26 @@ def normalizar_letra(nombre, puntos, altura_objetivo=100.0):
# Factor de escala basado en altura
escala = altura_objetivo / alto
# Normalizar puntos:
# Normalizar cada polyline:
# 1. Trasladar a origen (restar min_x, min_y)
# 2. Aplicar escala
puntos_norm = []
for x, y in puntos:
x_norm = (x - min_x) * escala
y_norm = (y - min_y) * escala
puntos_norm.append((x_norm, y_norm))
# 3. Cerrar polyline (último punto = primer punto)
polylines_norm = []
total_puntos = 0
for polyline in polylines:
polyline_norm = []
for x, y in polyline:
x_norm = (x - min_x) * escala
y_norm = (y - min_y) * escala
polyline_norm.append((x_norm, y_norm))
# Cerrar polyline si no está cerrada
if polyline_norm and polyline_norm[0] != polyline_norm[-1]:
polyline_norm.append(polyline_norm[0])
polylines_norm.append(polyline_norm)
total_puntos += len(polyline_norm)
# Calcular dimensiones finales
ancho_norm = ancho * escala
@@ -162,10 +200,11 @@ def normalizar_letra(nombre, puntos, altura_objetivo=100.0):
return {
'nombre': nombre,
'puntos': puntos_norm,
'polylines': polylines_norm,
'centro': centro,
'ancho': ancho_norm,
'alto': alto_norm
'alto': alto_norm,
'total_puntos': total_puntos
}
@@ -178,6 +217,7 @@ def generar_shp(letra_norm, output_dir):
scale: 1.0
center: cx, cy
polyline: x1,y1 x2,y2 x3,y3 ...
polyline: x1,y1 x2,y2 x3,y3 ... (si hay múltiples formas)
"""
if not letra_norm:
return
@@ -198,13 +238,13 @@ def generar_shp(letra_norm, output_dir):
f.write(f"center: {letra_norm['centro'][0]:.2f}, {letra_norm['centro'][1]:.2f}\n")
f.write(f"\n")
# Polyline con todos los puntos
f.write("polyline: ")
puntos_str = " ".join([f"{x:.2f},{y:.2f}" for x, y in letra_norm['puntos']])
f.write(puntos_str)
f.write("\n")
# Generar una línea polyline por cada forma
for polyline in letra_norm['polylines']:
puntos_str = " ".join([f"{x:.2f},{y:.2f}" for x, y in polyline])
f.write(f"polyline: {puntos_str}\n")
print(f"{nombre_archivo:20} ({len(letra_norm['puntos']):3} puntos, "
print(f"{nombre_archivo:20} ({len(letra_norm['polylines'])} formas, "
f"{letra_norm['total_puntos']:3} puntos, "
f"{letra_norm['ancho']:6.2f} x {letra_norm['alto']:6.2f} px)")
@@ -232,14 +272,18 @@ def parse_svg(filepath):
print(f"[INFO] Transform matrix: {transform_matrix}")
# Extraer paths y rects
paths = group.findall('svg:path', ns)
rects = group.findall('svg:rect', ns)
# Extraer paths y rects (buscar recursivamente en todos los descendientes)
paths = group.findall('.//svg:path', ns)
rects = group.findall('.//svg:rect', ns)
print(f"[INFO] Encontrados {len(paths)} paths y {len(rects)} rects")
# Nombres de las letras para paths (sin I que es un rect)
nombres_paths = ['J', 'A', 'L', 'G', 'A', 'M', 'E', 'S']
# Nombres de las letras para "ORNI ATTACK!"
# Grupo 1 (top): O, R, N, I (3 paths + 1 rect)
# Grupo 2 (bottom): A, T, T, A, C, K, ! (6 paths + 1 path para !)
# Total: 9 paths + 1 rect
# Asumiendo orden de aparición en SVG:
nombres_paths = ['O', 'R', 'N', 'A', 'T', 'T', 'A', 'C', 'K', 'EXCLAMACION']
letras = []
@@ -252,15 +296,18 @@ def parse_svg(filepath):
if not d_attr:
continue
# Parsear puntos del path
puntos = parse_svg_path(d_attr)
# Parsear polylines del path (ahora retorna lista de polylines)
polylines = parse_svg_path(d_attr)
# Aplicar transformación
puntos = [apply_transform(p, transform_matrix) for p in puntos]
# Aplicar transformación a cada polyline
polylines_transformed = []
for polyline in polylines:
polyline_transformed = [apply_transform(p, transform_matrix) for p in polyline]
polylines_transformed.append(polyline_transformed)
letras.append({
'nombre': nombres_paths[i],
'puntos': puntos
'polylines': polylines_transformed
})
# Procesar rects (la letra I es un rect)
@@ -268,11 +315,12 @@ def parse_svg(filepath):
puntos = rect_to_points(rect)
# Aplicar transformación
puntos = [apply_transform(p, transform_matrix) for p in puntos]
puntos_transformed = [apply_transform(p, transform_matrix) for p in puntos]
# Rect es una sola polyline
letras.append({
'nombre': 'I',
'puntos': puntos
'polylines': [puntos_transformed]
})
return letras
@@ -326,7 +374,7 @@ def main():
for nombre in sorted(letras_unicas.keys()):
letra = letras_unicas[nombre]
letra_norm = normalizar_letra(nombre, letra['puntos'], altura_objetivo=100.0)
letra_norm = normalizar_letra(nombre, letra['polylines'], altura_objetivo=100.0)
if letra_norm:
generar_shp(letra_norm, output_dir)