Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79d6e71fff | |||
| fb394d23c9 | |||
| 1951bcad11 | |||
| 9a874fc83b | |||
| 1acdd3f38d | |||
| a2b11371cf | |||
| b4b76ed6e8 | |||
| 6f4eb9c1fc | |||
| 47f7ffb169 | |||
| 70f2642e6d | |||
| 1a42f24a68 | |||
| ac0f03c725 | |||
| 1804c8a171 | |||
| d83056c614 | |||
| ba2a6fe914 | |||
| 364cf36183 | |||
| 7f6af6dd00 | |||
| fdfb84170f | |||
| 2088ccdcc6 | |||
| 7556c3fe8d | |||
| decde1b7d5 | |||
| c8545c712d | |||
| 76786203a0 | |||
| bc94eff176 | |||
| 44cd0857e0 | |||
| f8521d644c | |||
| eb2702eb19 | |||
| bfb4903998 | |||
| f3abab7a13 | |||
| 54031e3520 | |||
| 8b9d26a02c | |||
| 3d5277a395 | |||
| 2555157bd7 | |||
| 461eaedecf | |||
| 1891c9e49e | |||
| 829a895464 | |||
| 8bc259b25a | |||
| ec333efe66 | |||
| 3b432e6580 | |||
| 886ec8ab1d | |||
| bc5982b286 | |||
| 75a4a1b3b9 | |||
| f3f0bfcd9a | |||
| c959e0e3a0 | |||
| 8b896912b2 | |||
| 3d0057220d | |||
| 0c75f56cb5 | |||
| 0ceaa75862 | |||
| 087b8d346d | |||
| aca1f5200b | |||
| 3b638f4715 | |||
| 9a5adcbcc5 | |||
| d0be5ea2d1 | |||
| 07e00fff09 | |||
| b4e0ca7eca | |||
| b8173b205b | |||
| 57d623d6bc | |||
| 64ab08973c | |||
| 94a7a38cdd | |||
| 76165e4345 | |||
| 767a1f6af8 | |||
| 20ca024100 | |||
| 3c3857c1b2 | |||
| 523342fed9 | |||
| 217ca58b1a | |||
| ec6565bf71 | |||
| cd7f06f3a1 | |||
| 8886873ed5 | |||
| a41e696b69 | |||
| 4b7cbd88bb | |||
| 789cbbc593 | |||
| 1dd87c0707 | |||
| 330044e10f | |||
| f8c5207d5c | |||
| 2caaa29124 | |||
| cdc4d07394 | |||
| 1023cde1be | |||
| a3aeed4b7c | |||
| 3b0354da54 | |||
| 622ccd22bc | |||
| 1441134aea | |||
| 0500dce7aa | |||
| 9f0dfc4e24 | |||
| aa66dd41c1 | |||
| 69fb5f3cc1 | |||
| d6b2e97777 | |||
| 98c90e6075 | |||
| f795c86a38 | |||
| c1c5774406 | |||
| 0139da4764 | |||
| ec911979fb | |||
| e51749dbc6 |
104
.clang-tidy
Normal file
104
.clang-tidy
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
Checks:
|
||||||
|
# Estrategia: Habilitar checks uno por uno, aplicar fix, compilar, commit
|
||||||
|
# ✅ Check 1: readability-uppercase-literal-suffix (1.0f → 1.0F)
|
||||||
|
# ✅ Check 2: readability-math-missing-parentheses (claridad en ops matemáticas)
|
||||||
|
# ✅ Check 3: readability-identifier-naming (DESHABILITADO temporalmente - cascada de cambios)
|
||||||
|
# ✅ Check 4: readability-const-return-type (código ya cumple)
|
||||||
|
# ✅ Check 5: readability-else-after-return (código ya cumple)
|
||||||
|
# ✅ Check 6: readability-simplify-boolean-expr (código ya cumple)
|
||||||
|
# ✅ Check 7: readability-* (225 fixes aplicados)
|
||||||
|
- readability-*
|
||||||
|
- -readability-identifier-naming # Excluido (cascada de cambios)
|
||||||
|
- -readability-identifier-length # Excluido (nombres cortos son OK)
|
||||||
|
- -readability-magic-numbers # Excluido (muchos falsos positivos)
|
||||||
|
- -readability-convert-member-functions-to-static # Excluido (rompe encapsulación)
|
||||||
|
- -readability-use-anyofallof # Excluido (C++20 ranges - no todos los compiladores)
|
||||||
|
- -readability-function-cognitive-complexity # Excluido (complejidad ciclomática aceptable)
|
||||||
|
- -clang-analyzer-security.insecureAPI.rand # Excluido (rand() es suficiente para juegos)
|
||||||
|
# ✅ Check 8: modernize-* (215 fixes aplicados)
|
||||||
|
- modernize-*
|
||||||
|
- -modernize-use-trailing-return-type # Excluido (estilo controversial)
|
||||||
|
- -modernize-avoid-c-arrays # Excluido (arrays C son OK en algunos contextos)
|
||||||
|
# ✅ Check 9: performance-* (91 fixes aplicados)
|
||||||
|
- performance-*
|
||||||
|
- -performance-enum-size # Excluido (tamaño de enum no crítico)
|
||||||
|
# ✅ Check 10: bugprone-* (0 fixes - todos eran falsos positivos)
|
||||||
|
- bugprone-*
|
||||||
|
- -bugprone-easily-swappable-parameters # Excluido (muchos falsos positivos)
|
||||||
|
- -bugprone-narrowing-conversions # Excluido (conversiones intencionales)
|
||||||
|
- -bugprone-integer-division # Excluido (divisiones enteras OK en contexto)
|
||||||
|
- -bugprone-branch-clone # Excluido (fall-through en switch es intencional)
|
||||||
|
- -bugprone-switch-missing-default-case # Excluido (no todos los switches necesitan default)
|
||||||
|
- -bugprone-implicit-widening-of-multiplication-result # Excluido (valores pequeños, sin overflow)
|
||||||
|
- -bugprone-exception-escape # Excluido (excepciones en main terminan el programa - OK)
|
||||||
|
# ✅ Check 11: llvm-include-order (validar orden de includes - 0 errores)
|
||||||
|
- llvm-include-order
|
||||||
|
# ⏸️ Check 12: misc-include-cleaner (DESHABILITADO temporalmente - requiere refactorización masiva de includes)
|
||||||
|
- -misc-include-cleaner
|
||||||
|
|
||||||
|
WarningsAsErrors: '*'
|
||||||
|
# No usar HeaderFilterRegex - usamos .clang-tidy local en source/core/audio/ para excluir
|
||||||
|
FormatStyle: file
|
||||||
|
|
||||||
|
CheckOptions:
|
||||||
|
# Variables locales en snake_case
|
||||||
|
- { key: readability-identifier-naming.VariableCase, value: lower_case }
|
||||||
|
|
||||||
|
# Miembros privados en snake_case con sufijo _
|
||||||
|
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
||||||
|
|
||||||
|
# Miembros protegidos en snake_case con sufijo _
|
||||||
|
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||||
|
|
||||||
|
# Miembros públicos en snake_case (sin sufijo)
|
||||||
|
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
||||||
|
|
||||||
|
# Namespaces en CamelCase
|
||||||
|
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
||||||
|
|
||||||
|
# Variables estáticas privadas como miembros privados
|
||||||
|
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
||||||
|
|
||||||
|
# Constantes estáticas sin sufijo
|
||||||
|
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Constantes globales en UPPER_CASE
|
||||||
|
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Variables constexpr globales en UPPER_CASE
|
||||||
|
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Constantes locales en UPPER_CASE
|
||||||
|
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Constexpr miembros en UPPER_CASE (sin sufijo)
|
||||||
|
- { key: readability-identifier-naming.ConstexprMemberCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Constexpr miembros privados/protegidos con sufijo _
|
||||||
|
- { key: readability-identifier-naming.ConstexprMethodCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Clases, structs y enums en CamelCase
|
||||||
|
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
|
||||||
|
|
||||||
|
# Valores de enums en UPPER_CASE
|
||||||
|
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Métodos en camelBack (sin sufijos)
|
||||||
|
- { key: readability-identifier-naming.MethodCase, value: camelBack }
|
||||||
|
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
|
||||||
|
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
||||||
|
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
||||||
|
|
||||||
|
# Funciones en camelBack
|
||||||
|
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
||||||
|
|
||||||
|
# Parámetros en lower_case
|
||||||
|
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||||
|
|
||||||
|
# misc-include-cleaner: Ignorar SDL (genera falsos positivos)
|
||||||
|
- { key: misc-include-cleaner.IgnoreHeaders, value: 'SDL3/.*' }
|
||||||
19
.claude/settings.local.json
Normal file
19
.claude/settings.local.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(dir \"C:\\mingw\\gitea\\orni_attack\\release\\dll\")",
|
||||||
|
"Bash(make:*)",
|
||||||
|
"Bash(echo:*)",
|
||||||
|
"Bash(objdump:*)",
|
||||||
|
"Bash(unzip:*)",
|
||||||
|
"Bash(\"/Volumes/diskito/diskito.app/Contents/MacOS/diskito\")",
|
||||||
|
"Bash(pkill:*)",
|
||||||
|
"Bash(hdiutil detach:*)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(hdiutil mount:*)",
|
||||||
|
"Bash(open \"/Volumes/Orni Attack/Orni Attack.app\")"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -17,6 +17,16 @@ asteroids
|
|||||||
*.exe
|
*.exe
|
||||||
*.out
|
*.out
|
||||||
*.app
|
*.app
|
||||||
|
tools/pack_resources/pack_resources
|
||||||
|
tools/pack_resources/pack_resources.exe
|
||||||
|
|
||||||
|
# Releases
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.dmg
|
||||||
|
|
||||||
|
# Generated resources
|
||||||
|
resources.pack
|
||||||
|
|
||||||
# Compiled Object files
|
# Compiled Object files
|
||||||
*.o
|
*.o
|
||||||
@@ -58,9 +68,9 @@ _deps/
|
|||||||
*.ilk
|
*.ilk
|
||||||
|
|
||||||
# Core dumps
|
# Core dumps
|
||||||
core
|
# core
|
||||||
core.*
|
# core.*
|
||||||
*.core
|
# *.core
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
375
CLAUDE.md
375
CLAUDE.md
@@ -12,59 +12,98 @@ This is **Orni Attack**, an **Asteroids-style game** originally written in **Tur
|
|||||||
|
|
||||||
## Build System
|
## 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
|
```bash
|
||||||
# Clean + compile
|
make # Compile (delegates to CMake)
|
||||||
make clean && make
|
make debug # Debug build
|
||||||
|
make clean # Clean artifacts
|
||||||
|
./orni # Run
|
||||||
|
```
|
||||||
|
|
||||||
# Run
|
### Release Packaging
|
||||||
./orni
|
|
||||||
|
|
||||||
# Individual targets
|
```bash
|
||||||
make linux # Linux build
|
make macos_release # macOS .app bundle + .dmg (Apple Silicon)
|
||||||
make macos # macOS build
|
make linux_release # Linux .tar.gz
|
||||||
make windows # Windows build (MinGW)
|
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
|
### Build Files
|
||||||
|
|
||||||
- **CMakeLists.txt** - CMake configuration (C++20, SDL3, project metadata)
|
- **CMakeLists.txt** - CMake configuration (C++20, SDL3, auto-discovers .cpp files)
|
||||||
- **Makefile** - Cross-platform wrapper, extracts project info from CMakeLists.txt
|
- **Makefile** - Wrapper for compilation + complex packaging recipes
|
||||||
- **source/project.h.in** - Template for auto-generated project.h
|
- **source/project.h.in** - Template for auto-generated project.h
|
||||||
- **build/project.h** - Auto-generated (by CMake) with project constants
|
- **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
|
### 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
|
```cpp
|
||||||
// build/project.h (generated automatically)
|
|
||||||
namespace Project {
|
namespace Project {
|
||||||
constexpr const char* NAME = "orni"; // From project(orni ...)
|
constexpr const char* NAME = "orni";
|
||||||
constexpr const char* LONG_NAME = "Orni Attack"; // From PROJECT_LONG_NAME
|
constexpr const char* LONG_NAME = "Orni Attack";
|
||||||
constexpr const char* VERSION = "0.1.0"; // From VERSION
|
constexpr const char* VERSION = "0.3.0";
|
||||||
constexpr const char* COPYRIGHT = "© 1999..."; // From PROJECT_COPYRIGHT
|
constexpr const char* COPYRIGHT = "© 1999 Visente i Sergi, 2025 Port";
|
||||||
constexpr const char* GIT_HASH = "abc1234"; // From git rev-parse
|
constexpr const char* GIT_HASH = "abc1234"; // From git rev-parse
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Window title format** (dynamic, in sdl_manager.cpp):
|
**Window title** (dynamic): `Orni Attack v0.3.0 (© 1999 Visente i Sergi, 2025 Port)`
|
||||||
```cpp
|
|
||||||
std::format("{} v{} ({})",
|
### File Discovery
|
||||||
Project::LONG_NAME, // "Orni Attack"
|
|
||||||
Project::VERSION, // "0.1.0"
|
**Automatic** - no manual maintenance needed:
|
||||||
Project::COPYRIGHT) // "© 1999 Visente i Sergi, 2025 Port"
|
|
||||||
|
```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
|
## Architecture
|
||||||
|
|
||||||
@@ -466,6 +505,284 @@ void dibuixar() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Title Screen Ship System (BETA 3.0)
|
||||||
|
|
||||||
|
The title screen features two 3D ships floating on the starfield with perspective rendering, entry/exit animations, and subtle floating motion.
|
||||||
|
|
||||||
|
### Architecture Overview
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `source/game/title/ship_animator.hpp/cpp` - Ship animation state machine
|
||||||
|
- `source/core/rendering/shape_renderer.hpp/cpp` - 3D rotation + perspective projection
|
||||||
|
- `source/core/defaults.hpp` - Title::Ships namespace with all constants
|
||||||
|
- `source/game/escenes/escena_titol.hpp/cpp` - Integration with title scene
|
||||||
|
|
||||||
|
**Design Philosophy:**
|
||||||
|
- **Static 3D rotation**: Ships have fixed pitch/yaw/roll angles (not recalculated per frame)
|
||||||
|
- **Simple Z-axis simulation**: Scale changes simulate depth, not full perspective recalculation
|
||||||
|
- **State machine**: ENTERING → FLOATING → EXITING states
|
||||||
|
- **Easing functions**: Smooth transitions with ease_out_quad (entry) and ease_in_quad (exit)
|
||||||
|
- **Sinusoidal floating**: Organic motion using X/Y oscillation with phase offset
|
||||||
|
|
||||||
|
### 3D Rendering System
|
||||||
|
|
||||||
|
#### Rotation3D Struct (shape_renderer.hpp)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Rotation3D {
|
||||||
|
float pitch; // X-axis rotation (nose up/down)
|
||||||
|
float yaw; // Y-axis rotation (turn left/right)
|
||||||
|
float roll; // Z-axis rotation (bank left/right)
|
||||||
|
|
||||||
|
Rotation3D() : pitch(0.0f), yaw(0.0f), roll(0.0f) {}
|
||||||
|
Rotation3D(float p, float y, float r) : pitch(p), yaw(y), roll(r) {}
|
||||||
|
|
||||||
|
bool has_rotation() const {
|
||||||
|
return pitch != 0.0f || yaw != 0.0f || roll != 0.0f;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3D Transformation Pipeline (shape_renderer.cpp)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static Punt apply_3d_rotation(float x, float y, const Rotation3D& rot) {
|
||||||
|
float z = 0.0f; // All 2D points start at Z=0
|
||||||
|
|
||||||
|
// 1. Pitch (X-axis): Rotate around horizontal axis
|
||||||
|
float cos_pitch = std::cos(rot.pitch);
|
||||||
|
float sin_pitch = std::sin(rot.pitch);
|
||||||
|
float y1 = y * cos_pitch - z * sin_pitch;
|
||||||
|
float z1 = y * sin_pitch + z * cos_pitch;
|
||||||
|
|
||||||
|
// 2. Yaw (Y-axis): Rotate around vertical axis
|
||||||
|
float cos_yaw = std::cos(rot.yaw);
|
||||||
|
float sin_yaw = std::sin(rot.yaw);
|
||||||
|
float x2 = x * cos_yaw + z1 * sin_yaw;
|
||||||
|
float z2 = -x * sin_yaw + z1 * cos_yaw;
|
||||||
|
|
||||||
|
// 3. Roll (Z-axis): Rotate around depth axis
|
||||||
|
float cos_roll = std::cos(rot.roll);
|
||||||
|
float sin_roll = std::sin(rot.roll);
|
||||||
|
float x3 = x2 * cos_roll - y1 * sin_roll;
|
||||||
|
float y3 = x2 * sin_roll + y1 * cos_roll;
|
||||||
|
|
||||||
|
// 4. Perspective projection (Z-divide)
|
||||||
|
constexpr float perspective_factor = 500.0f;
|
||||||
|
float scale_factor = perspective_factor / (perspective_factor + z2);
|
||||||
|
|
||||||
|
return {x3 * scale_factor, y3 * scale_factor};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rendering order**: 3D rotation → perspective → 2D scale → 2D rotation → translation
|
||||||
|
|
||||||
|
**Backward compatibility**: Optional `rotation_3d` parameter (default nullptr) - existing code unaffected
|
||||||
|
|
||||||
|
### Ship Animation State Machine
|
||||||
|
|
||||||
|
#### States (ship_animator.hpp)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class EstatNau {
|
||||||
|
ENTERING, // Entering from off-screen
|
||||||
|
FLOATING, // Floating at target position
|
||||||
|
EXITING // Flying towards vanishing point
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NauTitol {
|
||||||
|
int jugador_id; // 1 or 2
|
||||||
|
EstatNau estat; // Current state
|
||||||
|
float temps_estat; // Time in current state
|
||||||
|
|
||||||
|
Punt posicio_inicial; // Start position
|
||||||
|
Punt posicio_objectiu; // Target position
|
||||||
|
Punt posicio_actual; // Current interpolated position
|
||||||
|
|
||||||
|
float escala_inicial; // Start scale
|
||||||
|
float escala_objectiu; // Target scale
|
||||||
|
float escala_actual; // Current interpolated scale
|
||||||
|
|
||||||
|
Rotation3D rotacio_3d; // STATIC 3D rotation (never changes)
|
||||||
|
float fase_oscilacio; // Oscillation phase accumulator
|
||||||
|
|
||||||
|
std::shared_ptr<Graphics::Shape> forma;
|
||||||
|
bool visible;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### State Transitions
|
||||||
|
|
||||||
|
**ENTERING** (2.0s):
|
||||||
|
- Ships appear from beyond screen edges (calculated radially from clock positions)
|
||||||
|
- Lerp position: off-screen → target (clock 8 / clock 4)
|
||||||
|
- Lerp scale: 1.0 → 0.6 (perspective effect)
|
||||||
|
- Easing: `ease_out_quad` (smooth deceleration)
|
||||||
|
- Transition: → FLOATING when complete
|
||||||
|
|
||||||
|
**FLOATING** (indefinite):
|
||||||
|
- Sinusoidal oscillation on X/Y axes
|
||||||
|
- Different frequencies (0.5 Hz / 0.7 Hz) with phase offset (π/2)
|
||||||
|
- Creates organic circular/elliptical motion
|
||||||
|
- Scale constant at 0.6
|
||||||
|
- Transition: → EXITING when START pressed
|
||||||
|
|
||||||
|
**EXITING** (1.0s):
|
||||||
|
- Ships fly towards vanishing point (center: 320, 240)
|
||||||
|
- Lerp position: current → vanishing point
|
||||||
|
- Lerp scale: current → 0.0 (simulates Z → infinity)
|
||||||
|
- Easing: `ease_in_quad` (acceleration)
|
||||||
|
- Edge case: If START pressed during ENTERING, ships fly from mid-animation position
|
||||||
|
- Marks invisible when complete
|
||||||
|
|
||||||
|
### Configuration (defaults.hpp)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace Title {
|
||||||
|
namespace Ships {
|
||||||
|
// Clock positions (polar coordinates from center 320, 240)
|
||||||
|
constexpr float CLOCK_8_ANGLE = 150.0f * Math::PI / 180.0f; // Bottom-left
|
||||||
|
constexpr float CLOCK_4_ANGLE = 30.0f * Math::PI / 180.0f; // Bottom-right
|
||||||
|
constexpr float CLOCK_RADIUS = 150.0f;
|
||||||
|
|
||||||
|
// Target positions (pre-calculated)
|
||||||
|
constexpr float P1_TARGET_X = 190.0f; // Clock 8
|
||||||
|
constexpr float P1_TARGET_Y = 315.0f;
|
||||||
|
constexpr float P2_TARGET_X = 450.0f; // Clock 4
|
||||||
|
constexpr float P2_TARGET_Y = 315.0f;
|
||||||
|
|
||||||
|
// 3D rotations (STATIC - tuned for subtle effect)
|
||||||
|
constexpr float P1_PITCH = 0.1f; // ~6° nose-up
|
||||||
|
constexpr float P1_YAW = -0.15f; // ~9° turn left
|
||||||
|
constexpr float P1_ROLL = -0.05f; // ~3° bank left
|
||||||
|
|
||||||
|
constexpr float P2_PITCH = 0.1f; // ~6° nose-up
|
||||||
|
constexpr float P2_YAW = 0.15f; // ~9° turn right
|
||||||
|
constexpr float P2_ROLL = 0.05f; // ~3° bank right
|
||||||
|
|
||||||
|
// Scales
|
||||||
|
constexpr float ENTRY_SCALE_START = 1.0f;
|
||||||
|
constexpr float FLOATING_SCALE = 0.6f;
|
||||||
|
|
||||||
|
// Animation durations
|
||||||
|
constexpr float ENTRY_DURATION = 2.0f;
|
||||||
|
constexpr float EXIT_DURATION = 1.0f;
|
||||||
|
constexpr float ENTRY_OFFSET = 200.0f; // Distance beyond screen edge
|
||||||
|
|
||||||
|
// Floating oscillation
|
||||||
|
constexpr float FLOAT_AMPLITUDE_X = 6.0f;
|
||||||
|
constexpr float FLOAT_AMPLITUDE_Y = 4.0f;
|
||||||
|
constexpr float FLOAT_FREQUENCY_X = 0.5f;
|
||||||
|
constexpr float FLOAT_FREQUENCY_Y = 0.7f;
|
||||||
|
constexpr float FLOAT_PHASE_OFFSET = 1.57f; // π/2 (90°)
|
||||||
|
|
||||||
|
// Vanishing point
|
||||||
|
constexpr float VANISHING_POINT_X = 320.0f;
|
||||||
|
constexpr float VANISHING_POINT_Y = 240.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration with EscenaTitol
|
||||||
|
|
||||||
|
#### Constructor
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Initialize ships after starfield
|
||||||
|
ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.obte_renderer());
|
||||||
|
ship_animator_->inicialitzar();
|
||||||
|
|
||||||
|
if (estat_actual_ == EstatTitol::MAIN) {
|
||||||
|
// Jump to MAIN: ships already in position (no entry animation)
|
||||||
|
ship_animator_->set_visible(true);
|
||||||
|
} else {
|
||||||
|
// Normal flow: ships enter during STARFIELD_FADE_IN
|
||||||
|
ship_animator_->set_visible(true);
|
||||||
|
ship_animator_->start_entry_animation();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Loop
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Update ships in visible states
|
||||||
|
if (ship_animator_ &&
|
||||||
|
(estat_actual_ == EstatTitol::STARFIELD_FADE_IN ||
|
||||||
|
estat_actual_ == EstatTitol::STARFIELD ||
|
||||||
|
estat_actual_ == EstatTitol::MAIN ||
|
||||||
|
estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) {
|
||||||
|
ship_animator_->actualitzar(delta_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger exit when START pressed
|
||||||
|
if (checkStartGameButtonPressed()) {
|
||||||
|
estat_actual_ = EstatTitol::PLAYER_JOIN_PHASE;
|
||||||
|
ship_animator_->trigger_exit_animation(); // Edge case: handles mid-ENTERING
|
||||||
|
Audio::get()->fadeOutMusic(MUSIC_FADE);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Draw Loop
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Draw order: starfield → ships → logo → text
|
||||||
|
if (starfield_) starfield_->dibuixar();
|
||||||
|
|
||||||
|
if (ship_animator_ &&
|
||||||
|
(estat_actual_ == EstatTitol::STARFIELD_FADE_IN ||
|
||||||
|
estat_actual_ == EstatTitol::STARFIELD ||
|
||||||
|
estat_actual_ == EstatTitol::MAIN ||
|
||||||
|
estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) {
|
||||||
|
ship_animator_->dibuixar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logo and text drawn after ships (foreground)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timing & Visibility
|
||||||
|
|
||||||
|
**Timeline:**
|
||||||
|
1. **STARFIELD_FADE_IN** (3.0s): Ships enter from off-screen
|
||||||
|
2. **STARFIELD** (4.0s): Ships floating
|
||||||
|
3. **MAIN** (indefinite): Ships floating + logo + text visible
|
||||||
|
4. **PLAYER_JOIN_PHASE** (2.5s): Ships exit (1.0s) + text blink
|
||||||
|
5. **BLACK_SCREEN** (2.0s): Ships already invisible (exit completed at 1.0s)
|
||||||
|
|
||||||
|
**Automatic visibility management:**
|
||||||
|
- Ships marked `visible = false` when exit animation completes (actualitzar_exiting)
|
||||||
|
- No manual hiding needed - state machine handles it
|
||||||
|
|
||||||
|
### Tuning Notes
|
||||||
|
|
||||||
|
**If ships look distorted:**
|
||||||
|
- Reduce rotation angles (P1_PITCH, P1_YAW, P1_ROLL, P2_*)
|
||||||
|
- Current values (0.1, 0.15, 0.05) are tuned for subtle 3D effect
|
||||||
|
- Angles in radians: 0.1 rad ≈ 6°, 0.15 rad ≈ 9°
|
||||||
|
|
||||||
|
**If ships are too large/small:**
|
||||||
|
- Adjust FLOATING_SCALE (currently 0.6)
|
||||||
|
- Adjust ENTRY_SCALE_START (currently 1.0)
|
||||||
|
|
||||||
|
**If floating motion is too jerky/smooth:**
|
||||||
|
- Adjust FLOAT_AMPLITUDE_X/Y (currently 6.0/4.0 pixels)
|
||||||
|
- Adjust FLOAT_FREQUENCY_X/Y (currently 0.5/0.7 Hz)
|
||||||
|
|
||||||
|
**If entry/exit animations are too fast/slow:**
|
||||||
|
- Adjust ENTRY_DURATION (currently 2.0s)
|
||||||
|
- Adjust EXIT_DURATION (currently 1.0s)
|
||||||
|
|
||||||
|
### Implementation Phases (Completed)
|
||||||
|
|
||||||
|
✅ **Phase 1**: 3D infrastructure (Rotation3D, render_shape extension)
|
||||||
|
✅ **Phase 2**: Foundation (ship_animator files, constants)
|
||||||
|
✅ **Phase 3**: Configuration & loading (shape loading, initialization)
|
||||||
|
✅ **Phase 4**: Floating animation (sinusoidal oscillation)
|
||||||
|
✅ **Phase 5**: Entry animation (off-screen → position with easing)
|
||||||
|
✅ **Phase 6**: Exit animation (position → vanishing point)
|
||||||
|
✅ **Phase 7**: EscenaTitol integration (constructor, update, draw)
|
||||||
|
✅ **Phase 8**: Polish & tuning (angles, scales, edge cases)
|
||||||
|
✅ **Phase 9**: Documentation (CLAUDE.md, code comments)
|
||||||
|
|
||||||
## Migration Progress
|
## Migration Progress
|
||||||
|
|
||||||
### ✅ Phase 0: Project Setup
|
### ✅ Phase 0: Project Setup
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# CMakeLists.txt
|
# CMakeLists.txt
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(orni VERSION 0.2.0)
|
project(orni VERSION 0.7.2)
|
||||||
|
|
||||||
# Info del proyecto
|
# Info del proyecto
|
||||||
set(PROJECT_LONG_NAME "Orni Attack")
|
set(PROJECT_LONG_NAME "Orni Attack")
|
||||||
set(PROJECT_COPYRIGHT "© 1999 Visente i Sergi, 2025 Port")
|
set(PROJECT_COPYRIGHT_ORIGINAL "© 1999 Visente i Sergi")
|
||||||
|
set(PROJECT_COPYRIGHT_PORT "© 2025 JailDesigner")
|
||||||
|
set(PROJECT_COPYRIGHT "${PROJECT_COPYRIGHT_ORIGINAL}, ${PROJECT_COPYRIGHT_PORT}")
|
||||||
|
|
||||||
# Establecer estándar de C++
|
# Establecer estándar de C++
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
@@ -31,29 +33,23 @@ endif()
|
|||||||
# Configurar archivo de versión
|
# Configurar archivo de versión
|
||||||
configure_file(${CMAKE_SOURCE_DIR}/source/project.h.in ${CMAKE_BINARY_DIR}/project.h @ONLY)
|
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
|
set(APP_SOURCES
|
||||||
|
${CORE_SOURCES}
|
||||||
|
${GAME_SOURCES}
|
||||||
source/main.cpp
|
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/game/options.cpp
|
|
||||||
source/game/escenes/escena_logo.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
|
# Configuración de SDL3
|
||||||
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
||||||
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
||||||
@@ -79,10 +75,21 @@ 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:DEBUG>:_DEBUG>)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:RELEASE_BUILD>)
|
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
|
# Configuración específica para cada plataforma
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE WINDOWS_BUILD)
|
target_compile_definitions(${PROJECT_NAME} PRIVATE WINDOWS_BUILD)
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE mingw32)
|
target_link_libraries(${PROJECT_NAME} PRIVATE mingw32)
|
||||||
|
# Static linking for libgcc and libstdc++ (avoid DLL dependencies for distribution)
|
||||||
|
target_link_options(${PROJECT_NAME} PRIVATE
|
||||||
|
-static-libgcc
|
||||||
|
-static-libstdc++
|
||||||
|
-static
|
||||||
|
)
|
||||||
# Añadir icono en Windows (se configurará desde el Makefile con windres)
|
# Añadir icono en Windows (se configurará desde el Makefile con windres)
|
||||||
elseif(APPLE)
|
elseif(APPLE)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD)
|
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD)
|
||||||
@@ -98,12 +105,16 @@ set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAK
|
|||||||
# --- STATIC ANALYSIS TARGETS ---
|
# --- STATIC ANALYSIS TARGETS ---
|
||||||
# Buscar herramientas de análisis estático
|
# Buscar herramientas de análisis estático
|
||||||
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
||||||
|
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
||||||
|
|
||||||
# Recopilar todos los archivos fuente para formateo
|
# Recopilar todos los archivos fuente para formateo
|
||||||
file(GLOB_RECURSE ALL_SOURCE_FILES
|
file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||||
"${CMAKE_SOURCE_DIR}/source/*.cpp"
|
"${CMAKE_SOURCE_DIR}/source/*.cpp"
|
||||||
"${CMAKE_SOURCE_DIR}/source/*.hpp"
|
"${CMAKE_SOURCE_DIR}/source/*.hpp"
|
||||||
)
|
)
|
||||||
|
# Excluir directorios con checks deshabilitados
|
||||||
|
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/audio/.*")
|
||||||
|
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/legacy/.*")
|
||||||
|
|
||||||
# Targets de clang-format
|
# Targets de clang-format
|
||||||
if(CLANG_FORMAT_EXE)
|
if(CLANG_FORMAT_EXE)
|
||||||
@@ -126,3 +137,43 @@ if(CLANG_FORMAT_EXE)
|
|||||||
else()
|
else()
|
||||||
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Targets de clang-tidy
|
||||||
|
if(CLANG_TIDY_EXE)
|
||||||
|
# En macOS, obtener la ruta del SDK para que clang-tidy encuentre los headers del sistema
|
||||||
|
set(CLANG_TIDY_EXTRA_ARGS "")
|
||||||
|
if(APPLE)
|
||||||
|
execute_process(
|
||||||
|
COMMAND xcrun --show-sdk-path
|
||||||
|
OUTPUT_VARIABLE MACOS_SDK_PATH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
if(MACOS_SDK_PATH)
|
||||||
|
set(CLANG_TIDY_EXTRA_ARGS "--extra-arg=-isysroot${MACOS_SDK_PATH}")
|
||||||
|
message(STATUS "clang-tidy usará SDK de macOS: ${MACOS_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_custom_target(tidy
|
||||||
|
COMMAND ${CLANG_TIDY_EXE}
|
||||||
|
-p ${CMAKE_BINARY_DIR}
|
||||||
|
${CLANG_TIDY_EXTRA_ARGS}
|
||||||
|
--fix
|
||||||
|
--fix-errors
|
||||||
|
${ALL_SOURCE_FILES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running clang-tidy with auto-fix..."
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(tidy-check
|
||||||
|
COMMAND ${CLANG_TIDY_EXE}
|
||||||
|
-p ${CMAKE_BINARY_DIR}
|
||||||
|
${CLANG_TIDY_EXTRA_ARGS}
|
||||||
|
--warnings-as-errors='*'
|
||||||
|
${ALL_SOURCE_FILES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Checking clang-tidy..."
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "clang-tidy no encontrado - targets 'tidy' y 'tidy-check' no disponibles")
|
||||||
|
endif()
|
||||||
|
|||||||
426
Makefile
426
Makefile
@@ -9,8 +9,8 @@ DIR_BIN := $(DIR_ROOT)
|
|||||||
# TARGET NAMES
|
# TARGET NAMES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
TARGET_NAME := $(shell powershell -Command "$$line = Get-Content CMakeLists.txt | Where-Object {$$_ -match '^project'}; if ($$line -match 'project\s*\x28(\w+)') { $$matches[1] }")
|
TARGET_NAME := $(shell powershell -Command "(Select-String -Path 'CMakeLists.txt' -Pattern 'project\s*\x28(\w+)').Matches.Groups[1].Value")
|
||||||
LONG_NAME := $(shell powershell -Command "$$line = Get-Content CMakeLists.txt | Where-Object {$$_ -match 'PROJECT_LONG_NAME'}; if ($$line -match '\"(.+)\"') { $$matches[1] }")
|
LONG_NAME := $(shell powershell -Command "(Select-String -Path 'CMakeLists.txt' -Pattern 'PROJECT_LONG_NAME\s+\x22(.+?)\x22').Matches.Groups[1].Value")
|
||||||
else
|
else
|
||||||
TARGET_NAME := $(shell awk '/^project/ {gsub(/[)(]/, " "); print $$2}' CMakeLists.txt)
|
TARGET_NAME := $(shell awk '/^project/ {gsub(/[)(]/, " "); print $$2}' CMakeLists.txt)
|
||||||
LONG_NAME := $(shell grep 'PROJECT_LONG_NAME' CMakeLists.txt | sed 's/.*"\(.*\)".*/\1/')
|
LONG_NAME := $(shell grep 'PROJECT_LONG_NAME' CMakeLists.txt | sed 's/.*"\(.*\)".*/\1/')
|
||||||
@@ -24,156 +24,325 @@ RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
|||||||
# VERSION
|
# VERSION
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
VERSION := v$(shell powershell -Command "$$line = Get-Content CMakeLists.txt | Where-Object {$$_ -match '^project'}; if ($$line -match 'VERSION\s+([0-9.]+)') { $$matches[1] }")
|
VERSION := v$(shell powershell -Command "(Select-String -Path 'CMakeLists.txt' -Pattern 'project.*VERSION\s+([0-9.]+)').Matches.Groups[1].Value")
|
||||||
else
|
else
|
||||||
VERSION := v$(shell grep "^project" CMakeLists.txt | tr -cd 0-9.)
|
VERSION := v$(shell grep "^project" CMakeLists.txt | tr -cd 0-9.)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# Release file names (depend on VERSION, so must come after)
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
RAW_VERSION := $(shell powershell -Command "\"$(VERSION)\" -replace '^v', ''")
|
||||||
|
else
|
||||||
|
RAW_VERSION := $(shell echo $(VERSION) | sed 's/^v//')
|
||||||
|
endif
|
||||||
|
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)
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# SOURCE FILES
|
# SOURCE FILES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
APP_SOURCES := \
|
# Note: Source files are now auto-discovered by CMake using GLOB_RECURSE
|
||||||
source/main.cpp \
|
# No need to maintain this list manually anymore!
|
||||||
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/game/options.cpp \
|
|
||||||
source/game/escenes/escena_logo.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
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# INCLUDES
|
# PLATFORM-SPECIFIC UTILITIES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
INCLUDES := -Isource -Ibuild
|
# Use Unix commands always (MinGW Make uses bash even on Windows)
|
||||||
|
RMFILE := rm -f
|
||||||
# ==============================================================================
|
RMDIR := rm -rf
|
||||||
# COMPILER FLAGS (OS-specific)
|
MKDIR := mkdir -p
|
||||||
# ==============================================================================
|
|
||||||
CPP_STANDARD := c++20
|
|
||||||
|
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
# Windows (MinGW)
|
# Windows-specific: Force cmd.exe shell for PowerShell commands
|
||||||
FixPath = $(subst /,\\,$1)
|
SHELL := cmd.exe
|
||||||
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
|
|
||||||
RMDIR := rmdir /S /Q
|
|
||||||
MKDIR := mkdir
|
|
||||||
else
|
else
|
||||||
# Unix-like systems (Linux/macOS)
|
# Unix-specific
|
||||||
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)
|
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
|
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
|
# TARGETS
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
.PHONY: all clean debug help backup
|
.PHONY: all clean debug help backup
|
||||||
|
|
||||||
# Default target
|
# ==============================================================================
|
||||||
all: $(TARGET_FILE)
|
# BUILD TARGETS (delegate to CMake)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
# Generate build/project.h from template
|
# Default target: build with CMake + resources
|
||||||
.PHONY: generate_project_h
|
all: resources.pack $(TARGET_FILE)
|
||||||
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"
|
|
||||||
|
|
||||||
# Compile executable
|
$(TARGET_FILE):
|
||||||
$(TARGET_FILE): generate_project_h $(APP_SOURCES)
|
@cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||||
ifeq ($(OS),Windows_NT)
|
@cmake --build build
|
||||||
@if not exist build $(MKDIR) build
|
@echo "Build successful: $(TARGET_FILE)"
|
||||||
@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)
|
|
||||||
|
|
||||||
# Debug build
|
# Debug build
|
||||||
debug: generate_project_h $(APP_SOURCES)
|
debug: resources.pack
|
||||||
ifeq ($(OS),Windows_NT)
|
@cmake -B build -DCMAKE_BUILD_TYPE=Debug
|
||||||
@if not exist build $(MKDIR) build
|
@cmake --build build
|
||||||
$(CXX) $(CXXFLAGS_DEBUG) $(INCLUDES) $(APP_SOURCES) $(LDFLAGS) -o $(TARGET_FILE)_debug.exe
|
@echo "Debug build successful: $(TARGET_FILE)"
|
||||||
else
|
|
||||||
@$(MKDIR) build
|
# ==============================================================================
|
||||||
$(CXX) $(CXXFLAGS_DEBUG) $(INCLUDES) $(APP_SOURCES) $(LDFLAGS) -o $(TARGET_FILE)_debug
|
# RELEASE PACKAGING TARGETS
|
||||||
endif
|
# ==============================================================================
|
||||||
@echo Debug build successful: $(TARGET_FILE)_debug
|
|
||||||
|
# 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 and names
|
||||||
|
@echo "Updating Info.plist with version $(RAW_VERSION) and names..."
|
||||||
|
@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"
|
||||||
|
@sed -i '' '/<key>CFBundleExecutable<\/key>/{n;s|<string>.*</string>|<string>$(TARGET_NAME)</string>|;}' \
|
||||||
|
"$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
||||||
|
@sed -i '' '/<key>CFBundleName<\/key>/{n;s|<string>.*</string>|<string>$(APP_NAME)</string>|;}' \
|
||||||
|
"$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
||||||
|
@sed -i '' '/<key>CFBundleDisplayName<\/key>/{n;s|<string>.*</string>|<string>$(APP_NAME)</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: pack_tool resources.pack
|
||||||
|
@echo "Creating Linux release - Version: $(VERSION)"
|
||||||
|
@echo "Note: SDL3 must be installed on the target system (libsdl3-dev)"
|
||||||
|
|
||||||
|
# Clean previous
|
||||||
|
@$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
@$(RMFILE) "$(LINUX_RELEASE)"
|
||||||
|
|
||||||
|
# Create folder
|
||||||
|
@$(MKDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# Copy resources
|
||||||
|
@cp resources.pack "$(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: pack_tool resources.pack
|
||||||
|
@echo off
|
||||||
|
@echo Creating Windows release - Version: $(VERSION)
|
||||||
|
@powershell if (Test-Path "$(RELEASE_FOLDER)") {Remove-Item "$(RELEASE_FOLDER)" -Recurse -Force}
|
||||||
|
@powershell if (Test-Path "$(WINDOWS_RELEASE)") {Remove-Item "$(WINDOWS_RELEASE)"}
|
||||||
|
@powershell if (-not (Test-Path "$(RELEASE_FOLDER)")) {New-Item "$(RELEASE_FOLDER)" -ItemType Directory}
|
||||||
|
@powershell Copy-Item -Path "resources.pack" -Destination "$(RELEASE_FOLDER)"
|
||||||
|
@powershell Copy-Item "release\dll\SDL3.dll" -Destination "$(RELEASE_FOLDER)"
|
||||||
|
@powershell Copy-Item "release\dll\libwinpthread-1.dll" -Destination "$(RELEASE_FOLDER)"
|
||||||
|
@powershell if (Test-Path "LICENSE") {Copy-Item "LICENSE" -Destination "$(RELEASE_FOLDER)"}
|
||||||
|
@powershell if (Test-Path "README.md") {Copy-Item "README.md" -Destination "$(RELEASE_FOLDER)"}
|
||||||
|
@windres release/$(TARGET_NAME).rc -O coff -o release/$(TARGET_NAME).res 2>nul || echo Warning: windres failed
|
||||||
|
@cmake -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release
|
||||||
|
@cmake --build build
|
||||||
|
@powershell if (Test-Path "$(TARGET_FILE).exe") {Copy-Item "$(TARGET_FILE).exe" -Destination "$(RELEASE_FILE).exe"} else {Copy-Item "$(TARGET_FILE)" -Destination "$(RELEASE_FILE).exe"}
|
||||||
|
@strip "$(RELEASE_FILE).exe" 2>nul || echo Warning: strip not available
|
||||||
|
@powershell Compress-Archive -Path "$(RELEASE_FOLDER)\*" -DestinationPath "$(WINDOWS_RELEASE)" -Force
|
||||||
|
@echo Release created: $(WINDOWS_RELEASE)
|
||||||
|
@powershell if (Test-Path "$(RELEASE_FOLDER)") {Remove-Item "$(RELEASE_FOLDER)" -Recurse -Force}
|
||||||
|
|
||||||
|
# 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 resources.pack "$(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 resources.pack "$(RELEASE_FOLDER)/"
|
||||||
|
@cp release/dll/SDL3.dll release/dll/libwinpthread-1.dll "$(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 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 build artifacts
|
||||||
clean:
|
clean:
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
@if exist $(call FixPath,$(TARGET_FILE).exe) $(RM) $(call FixPath,$(TARGET_FILE).exe)
|
@if exist $(call FixPath,$(TARGET_FILE).exe) $(RMFILE) $(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)_debug.exe) $(RMFILE) $(call FixPath,$(TARGET_FILE)_debug.exe)
|
||||||
@if exist build $(RMDIR) build
|
@if exist build $(RMDIR) build
|
||||||
@if exist $(RELEASE_FOLDER) $(RMDIR) $(RELEASE_FOLDER)
|
@if exist $(RELEASE_FOLDER) $(RMDIR) $(RELEASE_FOLDER)
|
||||||
else
|
else
|
||||||
@$(RMFILE) $(TARGET_FILE)
|
@$(RMFILE) $(TARGET_FILE) $(TARGET_FILE)_debug
|
||||||
@$(RMFILE) $(TARGET_FILE)_debug
|
@$(RMDIR) build $(RELEASE_FOLDER)
|
||||||
@$(RMDIR) build
|
@$(RMFILE) *.dmg *.zip *.tar.gz 2>/dev/null || true
|
||||||
@$(RMDIR) $(RELEASE_FOLDER)
|
@$(RMFILE) resources.pack 2>/dev/null || true
|
||||||
|
@make -C tools/pack_resources clean 2>/dev/null || true
|
||||||
endif
|
endif
|
||||||
@echo Clean complete
|
@echo "Clean complete"
|
||||||
|
|
||||||
# Backup to remote server
|
# Backup to remote server
|
||||||
backup:
|
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 \
|
rsync -a --delete \
|
||||||
--exclude='build/' \
|
--exclude='build/' \
|
||||||
--exclude='*.o' \
|
--exclude='*.o' \
|
||||||
@@ -181,20 +350,31 @@ backup:
|
|||||||
--exclude='orni' \
|
--exclude='orni' \
|
||||||
--exclude='orni_debug' \
|
--exclude='orni_debug' \
|
||||||
--exclude='*_release/' \
|
--exclude='*_release/' \
|
||||||
$(DIR_ROOT) maverick:/home/sergio/git-backup/asteroids/
|
$(DIR_ROOT) maverick:/home/sergio/git-backup/orni/
|
||||||
@echo "Backup completed successfully"
|
@echo "Backup completed successfully"
|
||||||
|
|
||||||
# Help target
|
# Help target
|
||||||
help:
|
help:
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " all - Build the game (default)"
|
@echo ""
|
||||||
@echo " debug - Build with debug symbols"
|
@echo "Build:"
|
||||||
@echo " clean - Remove build artifacts"
|
@echo " all - Build the game (default, delegates to CMake)"
|
||||||
@echo " backup - Backup project to remote server"
|
@echo " debug - Build with debug symbols"
|
||||||
@echo " help - Show this help message"
|
@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 ""
|
||||||
@echo "Current configuration:"
|
@echo "Current configuration:"
|
||||||
|
@echo " Project: $(LONG_NAME)"
|
||||||
@echo " Target: $(TARGET_NAME)"
|
@echo " Target: $(TARGET_NAME)"
|
||||||
@echo " Version: $(VERSION)"
|
@echo " Version: $(VERSION)"
|
||||||
@echo " Platform: $(UNAME_S)"
|
@echo " Platform: $(UNAME_S)"
|
||||||
@echo " C++ Standard: $(CPP_STANDARD)"
|
|
||||||
|
|||||||
79
README.md
79
README.md
@@ -1,20 +1,71 @@
|
|||||||
# Asteroids
|
<div align="center">
|
||||||
|
<img src="https://php.sustancia.synology.me/images/orni_attack/orni_attack_1.png" width="600" alt="Orni Attack">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Orni Attack
|
||||||
|
|
||||||
Destrueix als cosinus mesisinus que ens ataquen montats en ORNIs!
|
Destrueix als cosinus mesisinus que ens ataquen montats en ORNIs!
|
||||||
<img width="752" src="https://user-images.githubusercontent.com/110221325/184473983-a07c8594-f87c-4e6a-b723-b0a0f8d08e85.png">
|
|
||||||
|
---
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
* `cursor amunt` accelerar
|
|
||||||
* `cursor avall` frenar
|
|
||||||
* `cursor dreta` rotar en el sentit de les agulles del rellotge
|
|
||||||
* `cursor esquerra`rotar en l'altre sentit
|
|
||||||
* `espai` disparar
|
|
||||||
* `esc` eixir
|
|
||||||
|
|
||||||
Nomes tens una bala a l'hora. Crec que els teus dispars encara no fan pupa als ORNIs. Pero si ells te toquen sí que rebentes.
|
|
||||||
|
|
||||||
## Com jugar hui en dia
|
El joc permet l'ús del teclat per a controlar la nau i la finestra. Les tecles són les següents:
|
||||||
|
|
||||||
Amb DosBox. Augmenta cicles, uns 30000 en el meu macbook.
|
| Tecla | Acció |
|
||||||
|
|-------|-------|
|
||||||
|
| **↑** | Accelerar la nau |
|
||||||
|
| **↓** | Frenar |
|
||||||
|
| **←** | Rotar a l'esquerra |
|
||||||
|
| **→** | Rotar a la dreta |
|
||||||
|
| **Espai** | Disparar |
|
||||||
|
| **ESC** | Eixir del joc |
|
||||||
|
| **F1** | Disminuir la mida de la finestra |
|
||||||
|
| **F2** | Augmentar la mida de la finestra |
|
||||||
|
| **F3** | Alternar pantalla completa |
|
||||||
|
|
||||||
## Com compilar hui en dia
|
---
|
||||||
|
|
||||||
Turbo Pascal 7 desde DosBox. No m'ha fet falta activar res.
|
## Compilació i execució
|
||||||
|
|
||||||
|
### Compilar el joc
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make # Compilar
|
||||||
|
make debug # Compilació en mode debug
|
||||||
|
make clean # Netejar fitxers compilats
|
||||||
|
./orni # Executar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Crear versions release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make macos_release # macOS .app bundle + .dmg (Apple Silicon)
|
||||||
|
make linux_release # Linux .tar.gz
|
||||||
|
make windows_release # Windows .zip (requereix MinGW a Windows)
|
||||||
|
make windows_cross # Cross-compilació Windows des de Linux/macOS
|
||||||
|
make rpi_release # Raspberry Pi ARM64 cross-compilació
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requisits
|
||||||
|
|
||||||
|
- **C++20** compatible compiler
|
||||||
|
- **SDL3** library
|
||||||
|
- **CMake** 3.15 o superior
|
||||||
|
|
||||||
|
### Plataformes suportades
|
||||||
|
|
||||||
|
- macOS (Apple Silicon i Intel)
|
||||||
|
- Linux (x86_64)
|
||||||
|
- Windows (MinGW)
|
||||||
|
- Raspberry Pi (ARM64)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Història
|
||||||
|
|
||||||
|
Joc original creat en **Turbo Pascal 7 per a DOS** (1999), ara migrat a **C++20 amb SDL3**. Aquest port modern preserva la jugabilitat i l'estètica de l'original mentre aprofita les capacitats dels sistemes actuals.
|
||||||
|
|
||||||
|
**Versió actual**: BETA 3.0
|
||||||
|
|||||||
2134
data/gamecontrollerdb.txt
Normal file
2134
data/gamecontrollerdb.txt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
data/music/game.ogg
Normal file
BIN
data/music/game.ogg
Normal file
Binary file not shown.
BIN
data/music/title.ogg
Normal file
BIN
data/music/title.ogg
Normal file
Binary file not shown.
@@ -6,14 +6,18 @@ name: bullet
|
|||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
# Pentàgon petit radi=5 (1/4 del enemic)
|
# Cercle (octàgon regular radi=3)
|
||||||
# Pentàgon regular amb 72° entre punts
|
# 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):
|
# Conversió polar→cartesià (radi=3, SDL: Y creix cap avall):
|
||||||
# angle=-90°: (0.00, -5.00)
|
# angle=-90°: (0.00, -3.00)
|
||||||
# angle=-18°: (4.76, -1.55)
|
# angle=-45°: (2.12, -2.12)
|
||||||
# angle=54°: (2.94, 4.05)
|
# angle=0°: (3.00, 0.00)
|
||||||
# angle=126°: (-2.94, 4.05)
|
# angle=45°: (2.12, 2.12)
|
||||||
# angle=198°: (-4.76, -1.55)
|
# 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
|
||||||
|
|||||||
30
data/shapes/enemy_pinwheel.shp
Normal file
30
data/shapes/enemy_pinwheel.shp
Normal 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
|
||||||
19
data/shapes/enemy_square.shp
Normal file
19
data/shapes/enemy_square.shp
Normal 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
|
||||||
@@ -7,5 +7,5 @@ scale: 1.0
|
|||||||
center: 10, 20
|
center: 10, 20
|
||||||
|
|
||||||
# Trazo continuo (barra superior + lateral derecho + barra media + lateral derecho + barra inferior)
|
# Trazo continuo (barra superior + lateral derecho + barra media + lateral derecho + barra inferior)
|
||||||
polyline: 2,10 18,10 18,20 14,20
|
polyline: 2,10 18,10 18,20 8,20
|
||||||
polyline: 14,20 18,20 18,30 2,30
|
polyline: 8,20 18,20 18,30 2,30
|
||||||
|
|||||||
12
data/shapes/font/char_copyright.shp
Normal file
12
data/shapes/font/char_copyright.shp
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# char_copyright.shp - Símbolo © (copyright)
|
||||||
|
# Dimensiones: 20×40 (blocky display)
|
||||||
|
|
||||||
|
name: char_copyright
|
||||||
|
scale: 1.0
|
||||||
|
center: 10, 20
|
||||||
|
|
||||||
|
# Círculo exterior (aproximado con 12 puntos)
|
||||||
|
polyline: 10,8 13,9 15,11 17,14 18,17 18,23 17,26 15,29 13,31 10,32 7,31 5,29 3,26 2,23 2,17 3,14 5,11 7,9 10,8
|
||||||
|
|
||||||
|
# Letra C interior
|
||||||
|
polyline: 13,16 9,14 7,16 6,20 7,24 9,26 13,24
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# ship.shp - Nau del jugador (triangle)
|
# ship.shp - Nau del jugador 1 (triangle amb base còncava - punta de fletxa)
|
||||||
# © 1999 Visente i Sergi (versió Pascal)
|
# © 1999 Visente i Sergi (versió Pascal)
|
||||||
# © 2025 Port a C++20 amb SDL3
|
# © 2025 Port a C++20 amb SDL3
|
||||||
|
|
||||||
@@ -6,15 +6,19 @@ name: ship
|
|||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
# Triangle: punta amunt, base avall
|
# Triangle amb base còncava tipus "punta de fletxa"
|
||||||
# Punts originals (polar):
|
# Punts originals (polar):
|
||||||
# p1: r=12, angle=270° (3π/2) → punta amunt
|
# p1: r=12, angle=270° (3π/2) → punta amunt
|
||||||
# p2: r=12, angle=45° (π/4) → base dreta-darrere
|
# p2: r=12, angle=45° (π/4) → base dreta-darrere
|
||||||
# p3: r=12, angle=135° (3π/4) → base esquerra-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):
|
# Conversió polar→cartesià (angle-90° perquè origen visual és amunt):
|
||||||
# p1: (0, -12)
|
# p1: (0, -12) → punta
|
||||||
# p2: (8.49, 8.49)
|
# p2: (8.49, 8.49) → base dreta
|
||||||
# p3: (-8.49, 8.49)
|
# p4: (0, 4) → base centre (cap endins)
|
||||||
|
# p3: (-8.49, 8.49) → base esquerra
|
||||||
|
|
||||||
polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
|
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||||
|
|||||||
30
data/shapes/ship2.shp
Normal file
30
data/shapes/ship2.shp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# ship2.shp - Nau del jugador 2 (triangle amb circulito central)
|
||||||
|
# © 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"
|
||||||
|
# (Mateix que ship.shp)
|
||||||
|
# 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
|
||||||
|
polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
|
||||||
|
|
||||||
|
# Circulito central (octàgon r=2.5)
|
||||||
|
# Distintiu visual del jugador 2
|
||||||
|
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
|
||||||
28
data/shapes/ship2_perspective.shp
Normal file
28
data/shapes/ship2_perspective.shp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# ship2_perspective.shp - Nave P2 con perspectiva pre-calculada
|
||||||
|
# Posición optimizada: "4 del reloj" (Abajo-Derecha)
|
||||||
|
# Dirección: Volando hacia el fondo (centro pantalla)
|
||||||
|
|
||||||
|
name: ship2_perspective
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# TRANSFORMACIÓN APLICADA:
|
||||||
|
# 1. Rotación -45° (apuntando al centro desde abajo-dcha)
|
||||||
|
# 2. Proyección de perspectiva:
|
||||||
|
# - Punta (p1): Reducida al 60% (simula lejanía)
|
||||||
|
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
|
||||||
|
# 3. Flip horizontal (simétrica a ship_starfield.shp)
|
||||||
|
#
|
||||||
|
# Nuevos Punts (aprox):
|
||||||
|
# p1 (Punta): (-4, -4) -> Lejos, pequeña y apuntando arriba-izq
|
||||||
|
# p2 (Ala Izq): (-3, 11) -> Cerca, lado interior
|
||||||
|
# p4 (Base Cnt): (3, 5) -> Centro base
|
||||||
|
# p3 (Ala Dcha): (11, 2) -> Cerca, lado exterior (más grande)
|
||||||
|
|
||||||
|
#polyline: -4,-4 -3,11 3,5 11,2 -4,-4
|
||||||
|
polyline: -4,-4 -3,11 11,2 -4,-4
|
||||||
|
|
||||||
|
# Circulito central (octàgon r=2.5)
|
||||||
|
# Distintiu visual del jugador 2
|
||||||
|
# Sin perspectiva (está en el centro de la nave)
|
||||||
|
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
|
||||||
27
data/shapes/ship3.shp
Normal file
27
data/shapes/ship3.shp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
|
||||||
|
# © 2025 Orni Attack - Jugador 2
|
||||||
|
|
||||||
|
name: ship2
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Interceptor amb ales laterals
|
||||||
|
# Disseny més ample i agressiu que P1
|
||||||
|
#
|
||||||
|
# Geometria:
|
||||||
|
# - Punta més curta i ampla
|
||||||
|
# - Ales laterals pronunciades
|
||||||
|
# - Base més ampla per estabilitat visual
|
||||||
|
#
|
||||||
|
# Punts (cartesianes, Y negatiu = amunt):
|
||||||
|
# p1: (0, -10) → punta (més curta que P1)
|
||||||
|
# p2: (4, -6) → transició ala dreta
|
||||||
|
# p3: (10, 2) → punta ala dreta (més ampla)
|
||||||
|
# p4: (6, 8) → base ala dreta
|
||||||
|
# p5: (0, 6) → base centre (menys còncava)
|
||||||
|
# p6: (-6, 8) → base ala esquerra
|
||||||
|
# p7: (-10, 2) → punta ala esquerra
|
||||||
|
# p8: (-4, -6) → transició ala esquerra
|
||||||
|
# p1: (0, -10) → tanca
|
||||||
|
|
||||||
|
polyline: 0,-10 4,-6 10,2 6,8 0,6 -6,8 -10,2 -4,-6 0,-10
|
||||||
21
data/shapes/ship_perspective.shp
Normal file
21
data/shapes/ship_perspective.shp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# ship_perspective.shp - Nave con perspectiva pre-calculada
|
||||||
|
# Posición optimizada: "8 del reloj" (Abajo-Izquierda)
|
||||||
|
# Dirección: Volando hacia el fondo (centro pantalla)
|
||||||
|
|
||||||
|
name: ship_perspective
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# TRANSFORMACIÓN APLICADA:
|
||||||
|
# 1. Rotación +45° (apuntando al centro desde abajo-izq)
|
||||||
|
# 2. Proyección de perspectiva:
|
||||||
|
# - Punta (p1): Reducida al 60% (simula lejanía)
|
||||||
|
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
|
||||||
|
#
|
||||||
|
# Nuevos Puntos (aprox):
|
||||||
|
# p1 (Punta): (4, -4) -> Lejos, pequeña y apuntando arriba-dcha
|
||||||
|
# p2 (Ala Dcha): (3, 11) -> Cerca, lado interior
|
||||||
|
# p4 (Base Cnt): (-3, 5) -> Centro base
|
||||||
|
# p3 (Ala Izq): (-11, 2) -> Cerca, lado exterior (más grande)
|
||||||
|
|
||||||
|
polyline: 4,-4 3,11 -3,5 -11,2 4,-4
|
||||||
19
data/shapes/star.shp
Normal file
19
data/shapes/star.shp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# star.shp - Estrella per a starfield
|
||||||
|
# © 2025 Orni Attack
|
||||||
|
|
||||||
|
name: star
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Estrella de 4 puntes (diamant/creu)
|
||||||
|
# Petita i simple per a l'efecte starfield
|
||||||
|
#
|
||||||
|
# Punts:
|
||||||
|
# angle=0°: (0, -3) Dalt
|
||||||
|
# angle=90°: (3, 0) Dreta
|
||||||
|
# angle=180°: (0, 3) Baix
|
||||||
|
# angle=270°: (-3, 0) Esquerra
|
||||||
|
#
|
||||||
|
# Forma de diamant amb línies de centre a puntes
|
||||||
|
|
||||||
|
polyline: 0,-3 3,0 0,3 -3,0 0,-3
|
||||||
10
data/shapes/title/letra_a.shp
Normal file
10
data/shapes/title/letra_a.shp
Normal 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
|
||||||
9
data/shapes/title/letra_c.shp
Normal file
9
data/shapes/title/letra_c.shp
Normal 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
|
||||||
10
data/shapes/title/letra_exclamacion.shp
Normal file
10
data/shapes/title/letra_exclamacion.shp
Normal 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
|
||||||
9
data/shapes/title/letra_i.shp
Normal file
9
data/shapes/title/letra_i.shp
Normal 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
|
||||||
9
data/shapes/title/letra_k.shp
Normal file
9
data/shapes/title/letra_k.shp
Normal 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
|
||||||
9
data/shapes/title/letra_n.shp
Normal file
9
data/shapes/title/letra_n.shp
Normal 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
|
||||||
10
data/shapes/title/letra_o.shp
Normal file
10
data/shapes/title/letra_o.shp
Normal 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
|
||||||
10
data/shapes/title/letra_r.shp
Normal file
10
data/shapes/title/letra_r.shp
Normal 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
|
||||||
9
data/shapes/title/letra_t.shp
Normal file
9
data/shapes/title/letra_t.shp
Normal 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
|
||||||
BIN
data/sounds/effects/continue.wav
Normal file
BIN
data/sounds/effects/continue.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/explosion.wav
Normal file
BIN
data/sounds/effects/explosion.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/explosion2.wav
Normal file
BIN
data/sounds/effects/explosion2.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/init_hud.wav
Normal file
BIN
data/sounds/effects/init_hud.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/laser_shoot.wav
Normal file
BIN
data/sounds/effects/laser_shoot.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/logo.wav
Normal file
BIN
data/sounds/effects/logo.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/start.wav
Normal file
BIN
data/sounds/effects/start.wav
Normal file
Binary file not shown.
BIN
data/sounds/voices/good_job_commander.wav
Normal file
BIN
data/sounds/voices/good_job_commander.wav
Normal file
Binary file not shown.
168
data/stages/stages.yaml
Normal file
168
data/stages/stages.yaml
Normal 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
|
||||||
@@ -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.
BIN
release/icon.ico
BIN
release/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 111 KiB |
BIN
release/icon.png
BIN
release/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 402 KiB After Width: | Height: | Size: 174 KiB |
BIN
release/orni.res
Normal file
BIN
release/orni.res
Normal file
Binary file not shown.
BIN
sample.png
BIN
sample.png
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
5
source/core/audio/.clang-tidy
Normal file
5
source/core/audio/.clang-tidy
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Deshabilitar clang-tidy para este directorio (código externo: jail_audio.hpp)
|
||||||
|
# Los demás archivos de este directorio (audio.cpp, audio_cache.cpp) también se benefician
|
||||||
|
# de no ser modificados porque dependen íntimamente de la API de jail_audio.hpp
|
||||||
|
|
||||||
|
Checks: '-*'
|
||||||
183
source/core/audio/audio.cpp
Normal file
183
source/core/audio/audio.cpp
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
97
source/core/audio/audio.hpp
Normal file
97
source/core/audio/audio.hpp
Normal 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
|
||||||
|
};
|
||||||
142
source/core/audio/audio_cache.cpp
Normal file
142
source/core/audio/audio_cache.cpp
Normal 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 <iostream>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|
||||||
|
// 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 = "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 = "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;
|
||||||
|
}
|
||||||
42
source/core/audio/audio_cache.hpp
Normal file
42
source/core/audio/audio_cache.hpp
Normal 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);
|
||||||
|
};
|
||||||
482
source/core/audio/jail_audio.hpp
Normal file
482
source/core/audio/jail_audio.hpp
Normal 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(¤t_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;
|
||||||
|
}
|
||||||
532
source/core/defaults.hpp
Normal file
532
source/core/defaults.hpp
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <numbers>
|
||||||
|
|
||||||
|
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)
|
||||||
|
constexpr bool FULLSCREEN = true; // Pantalla completa activadapor defecto
|
||||||
|
} // 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 basat en percentatges)
|
||||||
|
namespace Zones {
|
||||||
|
// --- CONFIGURACIÓ DE PORCENTATGES ---
|
||||||
|
// Totes les zones definides com a percentatges de Game::WIDTH (640) i Game::HEIGHT (480)
|
||||||
|
|
||||||
|
// Percentatges d'alçada (divisió vertical)
|
||||||
|
constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior
|
||||||
|
constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central
|
||||||
|
constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior
|
||||||
|
|
||||||
|
// Padding horizontal per a PLAYAREA (dins de MAIN_PLAYAREA)
|
||||||
|
constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat
|
||||||
|
|
||||||
|
// --- CÀLCULS AUTOMÀTICS DE PÍXELS ---
|
||||||
|
// Càlculs automàtics a partir dels percentatges
|
||||||
|
|
||||||
|
// Alçades
|
||||||
|
constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT;
|
||||||
|
constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT;
|
||||||
|
constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT;
|
||||||
|
|
||||||
|
// Posicions Y
|
||||||
|
constexpr float SCOREBOARD_TOP_Y = 0.0F;
|
||||||
|
constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H;
|
||||||
|
constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H;
|
||||||
|
|
||||||
|
// Padding horizontal de PLAYAREA
|
||||||
|
constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT;
|
||||||
|
|
||||||
|
// --- ZONES FINALS (SDL_FRect) ---
|
||||||
|
|
||||||
|
// Marcador superior (reservat per a futur ús)
|
||||||
|
// Ocupa: 10% superior (0-48px)
|
||||||
|
constexpr SDL_FRect SCOREBOARD_TOP = {
|
||||||
|
0.0F, // x = 0.0
|
||||||
|
SCOREBOARD_TOP_Y, // y = 0.0
|
||||||
|
static_cast<float>(Game::WIDTH), // w = 640.0
|
||||||
|
SCOREBOARD_TOP_H // h = 48.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Àrea de joc principal (contenidor del 80% central, sense padding)
|
||||||
|
// Ocupa: 10-90% (48-432px), ample complet
|
||||||
|
constexpr SDL_FRect MAIN_PLAYAREA = {
|
||||||
|
0.0F, // x = 0.0
|
||||||
|
MAIN_PLAYAREA_Y, // y = 48.0
|
||||||
|
static_cast<float>(Game::WIDTH), // w = 640.0
|
||||||
|
MAIN_PLAYAREA_H // h = 384.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zona de joc real (amb padding horizontal del 5%)
|
||||||
|
// Ocupa: dins de MAIN_PLAYAREA, amb marges laterals
|
||||||
|
// S'utilitza per a límits del joc, col·lisions, spawn
|
||||||
|
constexpr SDL_FRect PLAYAREA = {
|
||||||
|
PLAYAREA_PADDING_H, // x = 32.0
|
||||||
|
MAIN_PLAYAREA_Y, // y = 48.0 (igual que MAIN_PLAYAREA)
|
||||||
|
Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // w = 576.0
|
||||||
|
MAIN_PLAYAREA_H // h = 384.0 (igual que MAIN_PLAYAREA)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Marcador inferior (marcador actual)
|
||||||
|
// Ocupa: 10% inferior (432-480px)
|
||||||
|
constexpr SDL_FRect SCOREBOARD = {
|
||||||
|
0.0F, // x = 0.0
|
||||||
|
SCOREBOARD_BOTTOM_Y, // y = 432.0
|
||||||
|
static_cast<float>(Game::WIDTH), // w = 640.0
|
||||||
|
SCOREBOARD_BOTTOM_H // h = 48.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Padding horizontal del marcador (per alinear zones esquerra/dreta amb PLAYAREA)
|
||||||
|
constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f;
|
||||||
|
} // 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
|
||||||
|
|
||||||
|
// Ship (nave del jugador)
|
||||||
|
namespace Ship {
|
||||||
|
// Invulnerabilidad post-respawn
|
||||||
|
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
|
||||||
|
|
||||||
|
// Parpadeo visual durante invulnerabilidad
|
||||||
|
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
|
||||||
|
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
|
||||||
|
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
|
||||||
|
} // namespace Ship
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
|
||||||
|
|
||||||
|
// Friendly fire system
|
||||||
|
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||||
|
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
||||||
|
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Transición INIT_HUD (animación inicial del HUD)
|
||||||
|
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
|
||||||
|
|
||||||
|
// Ratios de animación (inicio y fin como porcentajes del tiempo total)
|
||||||
|
// RECT (rectángulo de marges)
|
||||||
|
constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F;
|
||||||
|
constexpr float INIT_HUD_RECT_RATIO_END = 0.85F;
|
||||||
|
|
||||||
|
// SCORE (marcador de puntuación)
|
||||||
|
constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F;
|
||||||
|
constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F;
|
||||||
|
|
||||||
|
// SHIP1 (nave jugador 1)
|
||||||
|
constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F;
|
||||||
|
constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F;
|
||||||
|
|
||||||
|
// SHIP2 (nave jugador 2)
|
||||||
|
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
|
||||||
|
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F;
|
||||||
|
|
||||||
|
// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego)
|
||||||
|
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA
|
||||||
|
|
||||||
|
// Spawn positions (distribución horizontal para 2 jugadores)
|
||||||
|
constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda
|
||||||
|
constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda
|
||||||
|
constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba
|
||||||
|
|
||||||
|
// Continue system behavior
|
||||||
|
constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9
|
||||||
|
constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick
|
||||||
|
constexpr int MAX_CONTINUES = 3; // Maximum continues per game
|
||||||
|
constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues
|
||||||
|
|
||||||
|
// Continue screen visual configuration
|
||||||
|
namespace ContinueScreen {
|
||||||
|
// "CONTINUE" text
|
||||||
|
constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size
|
||||||
|
constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA
|
||||||
|
|
||||||
|
// Countdown number (9, 8, 7...)
|
||||||
|
constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large)
|
||||||
|
constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA
|
||||||
|
|
||||||
|
// "CONTINUES LEFT: X" text
|
||||||
|
constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small)
|
||||||
|
constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA
|
||||||
|
} // namespace ContinueScreen
|
||||||
|
|
||||||
|
// Game Over screen visual configuration
|
||||||
|
namespace GameOverScreen {
|
||||||
|
constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size
|
||||||
|
constexpr float TEXT_SPACING = 4.0F; // Character spacing
|
||||||
|
} // namespace GameOverScreen
|
||||||
|
|
||||||
|
// Stage message configuration (LEVEL_START, LEVEL_COMPLETED)
|
||||||
|
constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA
|
||||||
|
constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width
|
||||||
|
} // 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 = std::numbers::pi_v<float>;
|
||||||
|
} // 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 const char* TITLE_TRACK = "title.ogg"; // Pista de titulo
|
||||||
|
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* CONTINUE = "effects/continue.wav"; // Cuenta atras
|
||||||
|
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
|
||||||
|
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
|
||||||
|
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
|
||||||
|
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
|
||||||
|
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
||||||
|
constexpr const char* LOGO = "effects/logo.wav"; // Logo
|
||||||
|
constexpr const char* START = "effects/start.wav"; // El jugador pulsa START
|
||||||
|
constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
|
||||||
|
} // namespace Sound
|
||||||
|
|
||||||
|
// Controls (mapeo de teclas para los jugadores)
|
||||||
|
namespace Controls {
|
||||||
|
namespace P1 {
|
||||||
|
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT;
|
||||||
|
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT;
|
||||||
|
constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP;
|
||||||
|
constexpr SDL_Keycode SHOOT = SDLK_SPACE;
|
||||||
|
} // namespace P1
|
||||||
|
|
||||||
|
namespace P2 {
|
||||||
|
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D;
|
||||||
|
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A;
|
||||||
|
constexpr SDL_Scancode THRUST = SDL_SCANCODE_W;
|
||||||
|
constexpr SDL_Keycode SHOOT = SDLK_LSHIFT;
|
||||||
|
} // namespace P2
|
||||||
|
} // namespace Controls
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Title scene ship animations (naus 3D flotants a l'escena de títol)
|
||||||
|
namespace Title {
|
||||||
|
namespace Ships {
|
||||||
|
// ============================================================
|
||||||
|
// PARÀMETRES BASE (ajustar aquí per experimentar)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// 1. Escala global de les naus
|
||||||
|
constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp)
|
||||||
|
|
||||||
|
// 2. Altura vertical (cercanía al centro)
|
||||||
|
// Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla)
|
||||||
|
constexpr float TARGET_Y_RATIO = 0.15625F;
|
||||||
|
|
||||||
|
// 3. Radio orbital (distancia radial desde centro en coordenadas polares)
|
||||||
|
constexpr float CLOCK_RADIUS = 150.0F; // Distància des del centre
|
||||||
|
|
||||||
|
// 4. Ángulos de posición (clock positions en coordenadas polares)
|
||||||
|
// En coordenades de pantalla: 0° = dreta, 90° = baix, 180° = esquerra, 270° = dalt
|
||||||
|
constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left)
|
||||||
|
constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right)
|
||||||
|
|
||||||
|
// 5. Radio máximo de la forma de la nave (para calcular offset automáticamente)
|
||||||
|
constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp
|
||||||
|
|
||||||
|
// 6. Margen de seguridad para offset de entrada
|
||||||
|
constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// VALORS DERIVATS (calculats automàticament - NO modificar)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Centre de la pantalla (punt de referència)
|
||||||
|
constexpr float CENTER_X = Game::WIDTH / 2.0F; // 320.0f
|
||||||
|
constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // 240.0f
|
||||||
|
|
||||||
|
// Posicions target (calculades dinàmicament des dels paràmetres base)
|
||||||
|
// Nota: std::cos/sin no són constexpr en C++20, però funcionen en runtime
|
||||||
|
// Les funcions inline són optimitzades pel compilador (zero overhead)
|
||||||
|
inline float P1_TARGET_X() {
|
||||||
|
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE));
|
||||||
|
}
|
||||||
|
inline float P1_TARGET_Y() {
|
||||||
|
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
|
||||||
|
}
|
||||||
|
inline float P2_TARGET_X() {
|
||||||
|
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE));
|
||||||
|
}
|
||||||
|
inline float P2_TARGET_Y() {
|
||||||
|
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escales d'animació (relatives a SHIP_BASE_SCALE)
|
||||||
|
constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més gran
|
||||||
|
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotant: escala base
|
||||||
|
|
||||||
|
// Offset d'entrada (ajustat automàticament a l'escala)
|
||||||
|
// Fórmula: (radi màxim de la nau * escala d'entrada) + marge
|
||||||
|
constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
|
||||||
|
|
||||||
|
// Punt de fuga (centre per a l'animació de sortida)
|
||||||
|
constexpr float VANISHING_POINT_X = CENTER_X; // 320.0f
|
||||||
|
constexpr float VANISHING_POINT_Y = CENTER_Y; // 240.0f
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ANIMACIONS (durades, oscil·lacions, delays)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Durades d'animació
|
||||||
|
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
|
||||||
|
constexpr float EXIT_DURATION = 1.0F; // Sortida (segons)
|
||||||
|
|
||||||
|
// Flotació (oscil·lació reduïda i diferenciada per nau)
|
||||||
|
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
|
||||||
|
constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels)
|
||||||
|
|
||||||
|
// Freqüències base
|
||||||
|
constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz
|
||||||
|
constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz
|
||||||
|
constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°)
|
||||||
|
|
||||||
|
// Delays d'entrada (per a entrada escalonada)
|
||||||
|
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
|
||||||
|
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s després
|
||||||
|
|
||||||
|
// Delay global abans d'iniciar l'animació d'entrada al estat MAIN
|
||||||
|
constexpr float ENTRANCE_DELAY = 5.0F; // Temps d'espera abans que les naus entrin
|
||||||
|
|
||||||
|
// Multiplicadors de freqüència per a cada nau (variació sutil ±12%)
|
||||||
|
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
|
||||||
|
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
|
||||||
|
|
||||||
|
} // namespace Ships
|
||||||
|
|
||||||
|
namespace Layout {
|
||||||
|
// Posicions verticals (anclatges des del TOP de pantalla lògica, 0.0-1.0)
|
||||||
|
constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
|
||||||
|
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY"
|
||||||
|
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
|
||||||
|
|
||||||
|
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
|
||||||
|
constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px)
|
||||||
|
constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px)
|
||||||
|
|
||||||
|
// Factors d'escala
|
||||||
|
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
|
||||||
|
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
|
||||||
|
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
|
||||||
|
|
||||||
|
// Espaiat entre caràcters (usat per VectorText)
|
||||||
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
|
} // namespace Layout
|
||||||
|
} // namespace Title
|
||||||
|
|
||||||
|
// 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.45F; // Escala del text (0.6 = 60% del marcador)
|
||||||
|
constexpr float SPACING = 0.0F; // Espaiat entre caràcters
|
||||||
|
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
|
||||||
|
} // namespace FloatingScore
|
||||||
|
|
||||||
|
} // namespace Defaults
|
||||||
49
source/core/entities/entitat.hpp
Normal file
49
source/core/entities/entitat.hpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// entitat.hpp - Classe base abstracta per a totes les entitats del joc
|
||||||
|
// © 2025 Orni Attack - Arquitectura d'entitats
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "core/graphics/shape.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
namespace Entities {
|
||||||
|
|
||||||
|
class Entitat {
|
||||||
|
public:
|
||||||
|
virtual ~Entitat() = default;
|
||||||
|
|
||||||
|
// Interfície principal (virtual pur)
|
||||||
|
virtual void inicialitzar() = 0;
|
||||||
|
virtual void actualitzar(float delta_time) = 0;
|
||||||
|
virtual void dibuixar() const = 0;
|
||||||
|
[[nodiscard]] virtual bool esta_actiu() const = 0;
|
||||||
|
|
||||||
|
// Interfície de col·lisió (override opcional)
|
||||||
|
[[nodiscard]] virtual float get_collision_radius() const { return 0.0F; }
|
||||||
|
[[nodiscard]] virtual bool es_collidable() const { return false; }
|
||||||
|
|
||||||
|
// Getters comuns (inline, sense overhead)
|
||||||
|
[[nodiscard]] const Punt& get_centre() const { return centre_; }
|
||||||
|
[[nodiscard]] float get_angle() const { return angle_; }
|
||||||
|
[[nodiscard]] float get_brightness() const { return brightness_; }
|
||||||
|
[[nodiscard]] const std::shared_ptr<Graphics::Shape>& get_forma() const { return forma_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Estat comú (accés directe, sense overhead)
|
||||||
|
SDL_Renderer* renderer_;
|
||||||
|
std::shared_ptr<Graphics::Shape> forma_;
|
||||||
|
Punt centre_;
|
||||||
|
float angle_{0.0F};
|
||||||
|
float brightness_{1.0F};
|
||||||
|
|
||||||
|
// Constructor protegit (classe abstracta)
|
||||||
|
Entitat(SDL_Renderer* renderer = nullptr)
|
||||||
|
: renderer_(renderer),
|
||||||
|
centre_({.x = 0.0F, .y = 0.0F}) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Entities
|
||||||
159
source/core/graphics/shape.cpp
Normal file
159
source/core/graphics/shape.cpp
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// 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_({.x = 0.0F, .y = 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 << '\n';
|
||||||
|
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" << '\n';
|
||||||
|
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"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
} 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"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Comandes desconegudes ignorades silenciosament
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primitives_.empty()) {
|
||||||
|
std::cerr << "[Shape] Error: cap primitiva carregada" << '\n';
|
||||||
|
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.starts_with(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)" << '\n';
|
||||||
|
centre_ = {.x = 0.0F, .y = 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
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
64
source/core/graphics/shape.hpp
Normal file
64
source/core/graphics/shape.hpp
Normal 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
|
||||||
|
[[nodiscard]] const std::vector<ShapePrimitive>& get_primitives() const {
|
||||||
|
return primitives_;
|
||||||
|
}
|
||||||
|
[[nodiscard]] const Punt& get_centre() const { return centre_; }
|
||||||
|
[[nodiscard]] float get_escala_defecte() const { return escala_defecte_; }
|
||||||
|
[[nodiscard]] bool es_valida() const { return !primitives_.empty(); }
|
||||||
|
|
||||||
|
// Info de depuració
|
||||||
|
[[nodiscard]] std::string get_nom() const { return nom_; }
|
||||||
|
[[nodiscard]] 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
|
||||||
|
[[nodiscard]] std::string trim(const std::string& str) const;
|
||||||
|
[[nodiscard]] bool starts_with(const std::string& str, const std::string& prefix) const;
|
||||||
|
[[nodiscard]] std::string extract_value(const std::string& line) const;
|
||||||
|
void parse_center(const std::string& value);
|
||||||
|
[[nodiscard]] std::vector<Punt> parse_points(const std::string& str) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
86
source/core/graphics/shape_loader.cpp
Normal file
86
source/core/graphics/shape_loader.cpp
Normal 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 <iostream>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|
||||||
|
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 << '\n';
|
||||||
|
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.starts_with("shapes/")) {
|
||||||
|
// 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
|
||||||
|
<< '\n';
|
||||||
|
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
|
||||||
|
<< '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify shape is valid
|
||||||
|
if (!shape->es_valida()) {
|
||||||
|
std::cerr << "[ShapeLoader] Error: forma invàlida " << normalized << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache and return
|
||||||
|
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->get_nom()
|
||||||
|
<< ", " << shape->get_num_primitives() << " primitives)" << '\n';
|
||||||
|
|
||||||
|
cache_[filename] = shape;
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShapeLoader::clear_cache() {
|
||||||
|
std::cout << "[ShapeLoader] Netejant caché (" << cache_.size() << " formes)"
|
||||||
|
<< '\n';
|
||||||
|
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.starts_with(base_path_)) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Altrament, afegir base_path (ara suporta subdirectoris)
|
||||||
|
return base_path_ + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
39
source/core/graphics/shape_loader.hpp
Normal file
39
source/core/graphics/shape_loader.hpp
Normal 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
|
||||||
172
source/core/graphics/starfield.cpp
Normal file
172
source/core/graphics/starfield.cpp
Normal 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" << '\n';
|
||||||
|
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) const {
|
||||||
|
// 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
|
||||||
82
source/core/graphics/starfield.hpp
Normal file
82
source/core/graphics/starfield.hpp
Normal 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) const;
|
||||||
|
|
||||||
|
// Verificar si una estrella està fora de l'àrea
|
||||||
|
[[nodiscard]] bool fora_area(const Estrella& estrella) const;
|
||||||
|
|
||||||
|
// Calcular escala dinàmica segons distància del centre
|
||||||
|
[[nodiscard]] float calcular_escala(const Estrella& estrella) const;
|
||||||
|
|
||||||
|
// Calcular brightness dinàmica segons distància del centre
|
||||||
|
[[nodiscard]] 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
|
||||||
286
source/core/graphics/vector_text.cpp
Normal file
286
source/core/graphics/vector_text.cpp
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
// vector_text.cpp - Implementació del sistema de text vectorial
|
||||||
|
// © 2025 Port a C++20 amb SDL3
|
||||||
|
// Test pre-commit hook
|
||||||
|
|
||||||
|
#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
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
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_.contains(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorText::render(const std::string& text, const Punt& posicio, float escala, float spacing, float brightness) const {
|
||||||
|
if (renderer_ == nullptr) {
|
||||||
|
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 X del borde izquierdo del carácter actual
|
||||||
|
// (se ajustará +char_width/2 para obtener el centro al renderizar)
|
||||||
|
float current_x = posicio.x;
|
||||||
|
|
||||||
|
// Iterar sobre cada byte del string (con detecció UTF-8)
|
||||||
|
for (size_t i = 0; i < text.length(); i++) {
|
||||||
|
auto 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 X e Y para que posicio represente esquina superior izquierda
|
||||||
|
// (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
|
||||||
|
Punt char_pos = {.x = current_x + (char_width_scaled / 2.0F), .y = 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 << "'"
|
||||||
|
<< '\n';
|
||||||
|
current_x += char_width_scaled + spacing_scaled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VectorText::render_centered(const std::string& text, const Punt& centre_punt, float escala, float spacing, float brightness) const {
|
||||||
|
// Calcular dimensions del text
|
||||||
|
float text_width = get_text_width(text, escala, spacing);
|
||||||
|
float text_height = get_text_height(escala);
|
||||||
|
|
||||||
|
// Calcular posició de l'esquina superior esquerra
|
||||||
|
// restant la meitat de les dimensions del punt central
|
||||||
|
Punt posicio_esquerra = {
|
||||||
|
.x = centre_punt.x - (text_width / 2.0F),
|
||||||
|
.y = centre_punt.y - (text_height / 2.0F)};
|
||||||
|
|
||||||
|
// Delegar al mètode render() existent
|
||||||
|
render(text, posicio_esquerra, escala, spacing, brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Contar caracteres visuals (no bytes) - manejar UTF-8
|
||||||
|
size_t visual_chars = 0;
|
||||||
|
for (size_t i = 0; i < text.length(); i++) {
|
||||||
|
auto c = static_cast<unsigned char>(text[i]);
|
||||||
|
|
||||||
|
// Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
|
||||||
|
if (c == 0xC2 && i + 1 < text.length() &&
|
||||||
|
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
||||||
|
visual_chars++; // Un caràcter visual (©)
|
||||||
|
i++; // Saltar el següent byte
|
||||||
|
} else {
|
||||||
|
visual_chars++; // Caràcter normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ancho total = todos los caracteres VISUALES + spacing entre ellos
|
||||||
|
return (visual_chars * char_width_scaled) + ((visual_chars - 1) * spacing_scaled);
|
||||||
|
}
|
||||||
|
|
||||||
|
float VectorText::get_text_height(float escala) const {
|
||||||
|
return char_height * escala;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
55
source/core/graphics/vector_text.hpp
Normal file
55
source/core/graphics/vector_text.hpp
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// 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) const;
|
||||||
|
|
||||||
|
// Renderizar string centrado en un punto
|
||||||
|
// - text: cadena a renderizar
|
||||||
|
// - centre_punt: punto central del texto (no 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_centered(const std::string& text, const Punt& centre_punt, float escala = 1.0F, float spacing = 2.0F, float brightness = 1.0F) const;
|
||||||
|
|
||||||
|
// Calcular ancho total de un string (útil para centrado)
|
||||||
|
[[nodiscard]] 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)
|
||||||
|
[[nodiscard]] float get_text_height(float escala = 1.0F) const;
|
||||||
|
|
||||||
|
// Verificar si un carácter está soportado
|
||||||
|
[[nodiscard]] bool is_supported(char c) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_Renderer* renderer_;
|
||||||
|
std::unordered_map<char, std::shared_ptr<Shape>> chars_;
|
||||||
|
|
||||||
|
void load_charset();
|
||||||
|
[[nodiscard]] std::string get_shape_filename(char c) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
606
source/core/input/input.cpp
Normal file
606
source/core/input/input.cpp
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
#include "core/input/input.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
|
||||||
|
|
||||||
|
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
|
||||||
|
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
|
||||||
|
#include <ranges> // Para __find_if_fn, find_if
|
||||||
|
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||||
|
#include <utility> // Para pair, move
|
||||||
|
|
||||||
|
#include "game/options.hpp" // Para Options::controls
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
Input* Input::instance = nullptr;
|
||||||
|
|
||||||
|
// Inicializa la instancia única del singleton
|
||||||
|
void Input::init(const std::string& game_controller_db_path) {
|
||||||
|
Input::instance = new Input(game_controller_db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Libera la instancia
|
||||||
|
void Input::destroy() { delete Input::instance; }
|
||||||
|
|
||||||
|
// Obtiene la instancia
|
||||||
|
auto Input::get() -> Input* { return Input::instance; }
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Input::Input(std::string game_controller_db_path)
|
||||||
|
: gamepad_mappings_file_(std::move(game_controller_db_path)) {
|
||||||
|
// Inicializar bindings del teclado (valores por defecto)
|
||||||
|
// Estos serán sobrescritos por applyPlayer1BindingsFromOptions()
|
||||||
|
keyboard_.bindings = {
|
||||||
|
// Movimiento del jugador
|
||||||
|
{Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}},
|
||||||
|
{Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}},
|
||||||
|
{Action::THRUST, KeyState{.scancode = SDL_SCANCODE_UP}},
|
||||||
|
{Action::SHOOT, KeyState{.scancode = SDL_SCANCODE_SPACE}},
|
||||||
|
|
||||||
|
// Inputs de sistema (globales)
|
||||||
|
{Action::WINDOW_DEC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F1}},
|
||||||
|
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
|
||||||
|
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
|
||||||
|
{Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F4}},
|
||||||
|
{Action::EXIT, KeyState{.scancode = SDL_SCANCODE_ESCAPE}}};
|
||||||
|
|
||||||
|
initSDLGamePad(); // Inicializa el subsistema SDL_INIT_GAMEPAD
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asigna inputs a teclas
|
||||||
|
void Input::bindKey(Action action, SDL_Scancode code) {
|
||||||
|
keyboard_.bindings[action].scancode = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica las teclas configuradas desde Options
|
||||||
|
void Input::applyKeyboardBindingsFromOptions() {
|
||||||
|
bindKey(Action::LEFT, Options::keyboard_controls.key_left);
|
||||||
|
bindKey(Action::RIGHT, Options::keyboard_controls.key_right);
|
||||||
|
bindKey(Action::THRUST, Options::keyboard_controls.key_thrust);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado
|
||||||
|
void Input::applyGamepadBindingsFromOptions() {
|
||||||
|
// Si no hay gamepads conectados, no hay nada que hacer
|
||||||
|
if (gamepads_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener el primer gamepad conectado
|
||||||
|
const auto& gamepad = gamepads_[0];
|
||||||
|
|
||||||
|
// Aplicar bindings desde Options
|
||||||
|
// Los valores pueden ser:
|
||||||
|
// - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders)
|
||||||
|
// - 100: L2 trigger
|
||||||
|
// - 101: R2 trigger
|
||||||
|
// - 200+: Ejes del stick analógico
|
||||||
|
gamepad->bindings[Action::LEFT].button = Options::gamepad_controls.button_left;
|
||||||
|
gamepad->bindings[Action::RIGHT].button = Options::gamepad_controls.button_right;
|
||||||
|
gamepad->bindings[Action::THRUST].button = Options::gamepad_controls.button_thrust;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asigna inputs a botones del mando
|
||||||
|
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) {
|
||||||
|
if (gamepad != nullptr) {
|
||||||
|
gamepad->bindings[action].button = button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asigna inputs a botones del mando
|
||||||
|
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source) {
|
||||||
|
if (gamepad != nullptr) {
|
||||||
|
gamepad->bindings[action_target].button = gamepad->bindings[action_source].button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si alguna acción está activa
|
||||||
|
auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
|
||||||
|
bool success_keyboard = false;
|
||||||
|
bool success_controller = false;
|
||||||
|
|
||||||
|
if (check_keyboard) {
|
||||||
|
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
|
||||||
|
success_keyboard = keyboard_.bindings[action].is_held;
|
||||||
|
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
|
||||||
|
success_keyboard = keyboard_.bindings[action].just_pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
|
||||||
|
std::shared_ptr<Gamepad> active_gamepad = gamepad;
|
||||||
|
if (active_gamepad == nullptr && !gamepads_.empty()) {
|
||||||
|
active_gamepad = gamepads_[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active_gamepad != nullptr) {
|
||||||
|
success_controller = checkAxisInput(action, active_gamepad, repeat);
|
||||||
|
|
||||||
|
if (!success_controller) {
|
||||||
|
success_controller = checkTriggerInput(action, active_gamepad, repeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success_controller) {
|
||||||
|
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
|
||||||
|
success_controller = active_gamepad->bindings[action].is_held;
|
||||||
|
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
|
||||||
|
success_controller = active_gamepad->bindings[action].just_pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (success_keyboard || success_controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si hay almenos una acción activa
|
||||||
|
auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
|
||||||
|
// Obtenemos el número total de acciones posibles para iterar sobre ellas.
|
||||||
|
|
||||||
|
// --- Comprobación del Teclado ---
|
||||||
|
if (check_keyboard) {
|
||||||
|
for (const auto& pair : keyboard_.bindings) {
|
||||||
|
// Simplemente leemos el estado pre-calculado por Input::update().
|
||||||
|
// Ya no se llama a SDL_GetKeyboardState ni se modifica el estado '.active'.
|
||||||
|
if (pair.second.just_pressed) {
|
||||||
|
return true; // Se encontró una acción recién pulsada.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
|
||||||
|
std::shared_ptr<Gamepad> active_gamepad = gamepad;
|
||||||
|
if (active_gamepad == nullptr && !gamepads_.empty()) {
|
||||||
|
active_gamepad = gamepads_[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comprobación del Mando ---
|
||||||
|
// Comprobamos si hay mandos y si el índice solicitado es válido.
|
||||||
|
if (active_gamepad != nullptr) {
|
||||||
|
// Iteramos sobre todas las acciones, no sobre el número de mandos.
|
||||||
|
for (const auto& pair : active_gamepad->bindings) {
|
||||||
|
// Leemos el estado pre-calculado para el mando y la acción específicos.
|
||||||
|
if (pair.second.just_pressed) {
|
||||||
|
return true; // Se encontró una acción recién pulsada en el mando.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si llegamos hasta aquí, no se detectó ninguna nueva pulsación.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si hay algún botón pulsado
|
||||||
|
auto Input::checkAnyButton(bool repeat) -> bool {
|
||||||
|
// Solo comprueba los botones definidos previamente
|
||||||
|
for (auto bi : BUTTON_INPUTS) {
|
||||||
|
// Comprueba el teclado
|
||||||
|
if (checkAction(bi, repeat, CHECK_KEYBOARD)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba los mandos
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (checkAction(bi, repeat, DO_NOT_CHECK_KEYBOARD, gamepad)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si algún jugador (P1 o P2) presionó alguna acción de una lista
|
||||||
|
auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
|
||||||
|
for (const auto& action : actions) {
|
||||||
|
if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si hay algun mando conectado
|
||||||
|
auto Input::gameControllerFound() const -> bool { return !gamepads_.empty(); }
|
||||||
|
|
||||||
|
// Obten el nombre de un mando de juego
|
||||||
|
auto Input::getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string {
|
||||||
|
return gamepad == nullptr ? std::string() : gamepad->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la lista de nombres de mandos
|
||||||
|
auto Input::getControllerNames() const -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
names.push_back(gamepad->name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el número de mandos conectados
|
||||||
|
auto Input::getNumGamepads() const -> int { return gamepads_.size(); }
|
||||||
|
|
||||||
|
// Obtiene el gamepad a partir de un event.id
|
||||||
|
auto Input::getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Input::Gamepad> {
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (gamepad->instance_id == id) {
|
||||||
|
return gamepad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad> {
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (gamepad && gamepad->name == name) {
|
||||||
|
return gamepad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el SDL_GamepadButton asignado a un action
|
||||||
|
auto Input::getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton {
|
||||||
|
return static_cast<SDL_GamepadButton>(gamepad->bindings[action].button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba el eje del mando
|
||||||
|
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
|
||||||
|
// Obtener el binding configurado para esta acción
|
||||||
|
auto& binding = gamepad->bindings[action];
|
||||||
|
|
||||||
|
// Solo revisar ejes si el binding está configurado como eje (valores 200+)
|
||||||
|
// 200 = Left stick izquierda, 201 = Left stick derecha
|
||||||
|
if (binding.button < 200) {
|
||||||
|
// El binding no es un eje, no revisar axis
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determinar qué eje y dirección revisar según el binding
|
||||||
|
bool axis_active_now = false;
|
||||||
|
|
||||||
|
if (binding.button == 200) {
|
||||||
|
// Left stick izquierda
|
||||||
|
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) < -AXIS_THRESHOLD;
|
||||||
|
} else if (binding.button == 201) {
|
||||||
|
// Left stick derecha
|
||||||
|
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) > AXIS_THRESHOLD;
|
||||||
|
} else {
|
||||||
|
// Binding de eje no soportado
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repeat) {
|
||||||
|
// Si se permite repetir, simplemente devolvemos el estado actual
|
||||||
|
return axis_active_now;
|
||||||
|
} // Si no se permite repetir, aplicamos la lógica de transición
|
||||||
|
if (axis_active_now && !binding.axis_active) {
|
||||||
|
// Transición de inactivo a activo
|
||||||
|
binding.axis_active = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!axis_active_now && binding.axis_active) {
|
||||||
|
// Transición de activo a inactivo
|
||||||
|
binding.axis_active = false;
|
||||||
|
}
|
||||||
|
// Mantener el estado actual
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba los triggers del mando como botones digitales
|
||||||
|
auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
|
||||||
|
// Solo manejamos botones específicos que pueden ser triggers
|
||||||
|
if (gamepad->bindings[action].button != static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)) {
|
||||||
|
// Solo procesamos L2 y R2 como triggers
|
||||||
|
int button = gamepad->bindings[action].button;
|
||||||
|
|
||||||
|
// Verificar si el botón mapeado corresponde a un trigger virtual
|
||||||
|
// (Para esto necesitamos valores especiales que representen L2/R2 como botones)
|
||||||
|
bool trigger_active_now = false;
|
||||||
|
|
||||||
|
// Usamos constantes especiales para L2 y R2 como botones
|
||||||
|
if (button == TRIGGER_L2_AS_BUTTON) { // L2 como botón
|
||||||
|
Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER);
|
||||||
|
trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
|
||||||
|
} else if (button == TRIGGER_R2_AS_BUTTON) { // R2 como botón
|
||||||
|
Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
|
||||||
|
trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
|
||||||
|
} else {
|
||||||
|
return false; // No es un trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Referencia al binding correspondiente
|
||||||
|
auto& binding = gamepad->bindings[action];
|
||||||
|
|
||||||
|
if (repeat) {
|
||||||
|
// Si se permite repetir, simplemente devolvemos el estado actual
|
||||||
|
return trigger_active_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no se permite repetir, aplicamos la lógica de transición
|
||||||
|
if (trigger_active_now && !binding.trigger_active) {
|
||||||
|
// Transición de inactivo a activo
|
||||||
|
binding.trigger_active = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!trigger_active_now && binding.trigger_active) {
|
||||||
|
// Transición de activo a inactivo
|
||||||
|
binding.trigger_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mantener el estado actual
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::addGamepadMappingsFromFile() {
|
||||||
|
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
|
||||||
|
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::discoverGamepads() {
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
handleEvent(event); // Comprueba mandos conectados
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::initSDLGamePad() {
|
||||||
|
if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
|
||||||
|
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GAMEPAD could not initialize! SDL Error: %s", SDL_GetError());
|
||||||
|
} else {
|
||||||
|
addGamepadMappingsFromFile();
|
||||||
|
discoverGamepads();
|
||||||
|
std::cout << "\n** INPUT SYSTEM **\n";
|
||||||
|
std::cout << "Input System initialized successfully\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::resetInputStates() {
|
||||||
|
// Resetear todos los KeyBindings.active a false
|
||||||
|
for (auto& key : keyboard_.bindings) {
|
||||||
|
key.second.is_held = false;
|
||||||
|
key.second.just_pressed = false;
|
||||||
|
}
|
||||||
|
// Resetear todos los ControllerBindings.active a false
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
for (auto& binding : gamepad->bindings) {
|
||||||
|
binding.second.is_held = false;
|
||||||
|
binding.second.just_pressed = false;
|
||||||
|
binding.second.trigger_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::update() {
|
||||||
|
// --- TECLADO ---
|
||||||
|
const bool* key_states = SDL_GetKeyboardState(nullptr);
|
||||||
|
|
||||||
|
// Actualizar bindings globales (F1-F4, ESC)
|
||||||
|
for (auto& binding : keyboard_.bindings) {
|
||||||
|
bool key_is_down_now = key_states[binding.second.scancode];
|
||||||
|
|
||||||
|
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
||||||
|
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
|
||||||
|
binding.second.is_held = key_is_down_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar bindings de jugador 1
|
||||||
|
for (auto& binding : player1_keyboard_bindings_) {
|
||||||
|
bool key_is_down_now = key_states[binding.second.scancode];
|
||||||
|
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
|
||||||
|
binding.second.is_held = key_is_down_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar bindings de jugador 2
|
||||||
|
for (auto& binding : player2_keyboard_bindings_) {
|
||||||
|
bool key_is_down_now = key_states[binding.second.scancode];
|
||||||
|
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
|
||||||
|
binding.second.is_held = key_is_down_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MANDOS ---
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
for (auto& binding : gamepad->bindings) {
|
||||||
|
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
|
||||||
|
|
||||||
|
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
||||||
|
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
|
||||||
|
binding.second.is_held = button_is_down_now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::handleEvent(const SDL_Event& event) -> std::string {
|
||||||
|
switch (event.type) {
|
||||||
|
case SDL_EVENT_GAMEPAD_ADDED:
|
||||||
|
return addGamepad(event.gdevice.which);
|
||||||
|
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||||
|
return removeGamepad(event.gdevice.which);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::addGamepad(int device_index) -> std::string {
|
||||||
|
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
|
||||||
|
if (pad == nullptr) {
|
||||||
|
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto gamepad = std::make_shared<Gamepad>(pad);
|
||||||
|
auto name = gamepad->name;
|
||||||
|
std::cout << "Gamepad connected (" << name << ")" << '\n';
|
||||||
|
gamepads_.push_back(std::move(gamepad));
|
||||||
|
return name + " CONNECTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
|
||||||
|
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) {
|
||||||
|
return gamepad->instance_id == id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (it != gamepads_.end()) {
|
||||||
|
std::string name = (*it)->name;
|
||||||
|
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
|
||||||
|
gamepads_.erase(it);
|
||||||
|
return name + " DISCONNECTED";
|
||||||
|
}
|
||||||
|
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::printConnectedGamepads() const {
|
||||||
|
if (gamepads_.empty()) {
|
||||||
|
std::cout << "No hay gamepads conectados." << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Gamepads conectados:\n";
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
std::string name = gamepad->name.empty() ? "Desconocido" : gamepad->name;
|
||||||
|
std::cout << " - ID: " << gamepad->instance_id
|
||||||
|
<< ", Nombre: " << name << ")" << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> {
|
||||||
|
// Si no hay gamepads disponibles, devolver gamepad por defecto
|
||||||
|
if (gamepads_.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar por nombre
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (gamepad && gamepad->name == gamepad_name) {
|
||||||
|
return gamepad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no se encuentra por nombre, devolver el primer gamepad válido
|
||||||
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
if (gamepad) {
|
||||||
|
return gamepad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si llegamos aquí, no hay gamepads válidos
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
|
||||||
|
|
||||||
|
// Aplica configuración de controles del jugador 1
|
||||||
|
void Input::applyPlayer1BindingsFromOptions() {
|
||||||
|
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
|
||||||
|
player1_keyboard_bindings_[Action::LEFT].scancode = Options::player1.keyboard.key_left;
|
||||||
|
player1_keyboard_bindings_[Action::RIGHT].scancode = Options::player1.keyboard.key_right;
|
||||||
|
player1_keyboard_bindings_[Action::THRUST].scancode = Options::player1.keyboard.key_thrust;
|
||||||
|
player1_keyboard_bindings_[Action::SHOOT].scancode = Options::player1.keyboard.key_shoot;
|
||||||
|
player1_keyboard_bindings_[Action::START].scancode = Options::player1.keyboard.key_start;
|
||||||
|
|
||||||
|
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
|
||||||
|
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||||
|
if (Options::player1.gamepad_name.empty()) {
|
||||||
|
// Fallback: usar primer gamepad disponible
|
||||||
|
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
|
||||||
|
} else {
|
||||||
|
// Buscar por nombre
|
||||||
|
gamepad = findAvailableGamepadByName(Options::player1.gamepad_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gamepad) {
|
||||||
|
player1_gamepad_ = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Aplicar bindings de gamepad
|
||||||
|
gamepad->bindings[Action::LEFT].button = Options::player1.gamepad.button_left;
|
||||||
|
gamepad->bindings[Action::RIGHT].button = Options::player1.gamepad.button_right;
|
||||||
|
gamepad->bindings[Action::THRUST].button = Options::player1.gamepad.button_thrust;
|
||||||
|
gamepad->bindings[Action::SHOOT].button = Options::player1.gamepad.button_shoot;
|
||||||
|
|
||||||
|
// 4. Cachear referencia
|
||||||
|
player1_gamepad_ = gamepad;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica configuración de controles del jugador 2
|
||||||
|
void Input::applyPlayer2BindingsFromOptions() {
|
||||||
|
// 1. Aplicar bindings de teclado (mapa específico de P2, no sobrescribe P1)
|
||||||
|
player2_keyboard_bindings_[Action::LEFT].scancode = Options::player2.keyboard.key_left;
|
||||||
|
player2_keyboard_bindings_[Action::RIGHT].scancode = Options::player2.keyboard.key_right;
|
||||||
|
player2_keyboard_bindings_[Action::THRUST].scancode = Options::player2.keyboard.key_thrust;
|
||||||
|
player2_keyboard_bindings_[Action::SHOOT].scancode = Options::player2.keyboard.key_shoot;
|
||||||
|
player2_keyboard_bindings_[Action::START].scancode = Options::player2.keyboard.key_start;
|
||||||
|
|
||||||
|
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
|
||||||
|
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||||
|
if (Options::player2.gamepad_name.empty()) {
|
||||||
|
// Fallback: usar segundo gamepad disponible
|
||||||
|
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
|
||||||
|
} else {
|
||||||
|
// Buscar por nombre
|
||||||
|
gamepad = findAvailableGamepadByName(Options::player2.gamepad_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gamepad) {
|
||||||
|
player2_gamepad_ = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Aplicar bindings de gamepad
|
||||||
|
gamepad->bindings[Action::LEFT].button = Options::player2.gamepad.button_left;
|
||||||
|
gamepad->bindings[Action::RIGHT].button = Options::player2.gamepad.button_right;
|
||||||
|
gamepad->bindings[Action::THRUST].button = Options::player2.gamepad.button_thrust;
|
||||||
|
gamepad->bindings[Action::SHOOT].button = Options::player2.gamepad.button_shoot;
|
||||||
|
|
||||||
|
// 4. Cachear referencia
|
||||||
|
player2_gamepad_ = gamepad;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consulta de input para jugador 1
|
||||||
|
auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
|
||||||
|
// Comprobar teclado con el mapa específico de P1
|
||||||
|
bool keyboard_active = false;
|
||||||
|
|
||||||
|
if (player1_keyboard_bindings_.contains(action)) {
|
||||||
|
if (repeat) {
|
||||||
|
keyboard_active = player1_keyboard_bindings_[action].is_held;
|
||||||
|
} else {
|
||||||
|
keyboard_active = player1_keyboard_bindings_[action].just_pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprobar gamepad de P1
|
||||||
|
bool gamepad_active = false;
|
||||||
|
if (player1_gamepad_) {
|
||||||
|
gamepad_active = checkAction(action, repeat, DO_NOT_CHECK_KEYBOARD, player1_gamepad_);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyboard_active || gamepad_active;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consulta de input para jugador 2
|
||||||
|
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
|
||||||
|
// Comprobar teclado con el mapa específico de P2
|
||||||
|
bool keyboard_active = false;
|
||||||
|
|
||||||
|
if (player2_keyboard_bindings_.contains(action)) {
|
||||||
|
if (repeat) {
|
||||||
|
keyboard_active = player2_keyboard_bindings_[action].is_held;
|
||||||
|
} else {
|
||||||
|
keyboard_active = player2_keyboard_bindings_[action].just_pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprobar gamepad de P2
|
||||||
|
bool gamepad_active = false;
|
||||||
|
if (player2_gamepad_) {
|
||||||
|
gamepad_active = checkAction(action, repeat, DO_NOT_CHECK_KEYBOARD, player2_gamepad_);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyboard_active || gamepad_active;
|
||||||
|
}
|
||||||
162
source/core/input/input.hpp
Normal file
162
source/core/input/input.hpp
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h> // Para SDL_Scancode, SDL_GamepadButton, SDL_JoystickID, SDL_CloseGamepad, SDL_Gamepad, SDL_GetGamepadJoystick, SDL_GetGamepadName, SDL_GetGamepadPath, SDL_GetJoystickID, Sint16, Uint8, SDL_Event
|
||||||
|
|
||||||
|
#include <array> // Para array
|
||||||
|
#include <memory> // Para shared_ptr
|
||||||
|
#include <span> // Para span
|
||||||
|
#include <string> // Para string, basic_string
|
||||||
|
#include <unordered_map> // Para unordered_map
|
||||||
|
#include <utility> // Para pair
|
||||||
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
#include "core/input/input_types.hpp" // for InputAction
|
||||||
|
|
||||||
|
// --- Clase Input: gestiona la entrada de teclado y mandos (singleton) ---
|
||||||
|
class Input {
|
||||||
|
public:
|
||||||
|
// --- Constantes ---
|
||||||
|
static constexpr bool ALLOW_REPEAT = true; // Permite repetición
|
||||||
|
static constexpr bool DO_NOT_ALLOW_REPEAT = false; // No permite repetición
|
||||||
|
static constexpr bool CHECK_KEYBOARD = true; // Comprueba teclado
|
||||||
|
static constexpr bool DO_NOT_CHECK_KEYBOARD = false; // No comprueba teclado
|
||||||
|
static constexpr int TRIGGER_L2_AS_BUTTON = 100; // L2 como botón
|
||||||
|
static constexpr int TRIGGER_R2_AS_BUTTON = 101; // R2 como botón
|
||||||
|
|
||||||
|
// --- Tipos ---
|
||||||
|
using Action = InputAction; // Alias para mantener compatibilidad
|
||||||
|
|
||||||
|
// --- Estructuras ---
|
||||||
|
struct KeyState {
|
||||||
|
Uint8 scancode{0}; // Scancode asociado
|
||||||
|
bool is_held{false}; // Está pulsada ahora mismo
|
||||||
|
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ButtonState {
|
||||||
|
int button{static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)}; // GameControllerButton asociado
|
||||||
|
bool is_held{false}; // Está pulsada ahora mismo
|
||||||
|
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
||||||
|
bool axis_active{false}; // Estado del eje
|
||||||
|
bool trigger_active{false}; // Estado del trigger como botón digital
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Keyboard {
|
||||||
|
std::unordered_map<Action, KeyState> bindings; // Mapa de acciones a estados de tecla
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Gamepad {
|
||||||
|
SDL_Gamepad* pad{nullptr}; // Puntero al gamepad SDL
|
||||||
|
SDL_JoystickID instance_id{0}; // ID de instancia del joystick
|
||||||
|
std::string name; // Nombre del gamepad
|
||||||
|
std::string path; // Ruta del dispositivo
|
||||||
|
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
|
||||||
|
|
||||||
|
explicit Gamepad(SDL_Gamepad* gamepad)
|
||||||
|
: pad(gamepad),
|
||||||
|
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
|
||||||
|
name(std::string(SDL_GetGamepadName(gamepad))),
|
||||||
|
path(std::string(SDL_GetGamepadPath(pad))),
|
||||||
|
bindings{
|
||||||
|
// Movimiento y acciones del jugador
|
||||||
|
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||||
|
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||||
|
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
|
||||||
|
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {}
|
||||||
|
|
||||||
|
~Gamepad() {
|
||||||
|
if (pad != nullptr) {
|
||||||
|
SDL_CloseGamepad(pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reasigna un botón a una acción
|
||||||
|
void rebindAction(Action action, SDL_GamepadButton new_button) {
|
||||||
|
bindings[action].button = static_cast<int>(new_button);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Tipos ---
|
||||||
|
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // Vector de gamepads
|
||||||
|
|
||||||
|
// --- Singleton ---
|
||||||
|
static void init(const std::string& game_controller_db_path);
|
||||||
|
static void destroy();
|
||||||
|
static auto get() -> Input*;
|
||||||
|
|
||||||
|
// --- Actualización del sistema ---
|
||||||
|
void update(); // Actualiza estados de entrada
|
||||||
|
|
||||||
|
// --- Configuración de controles ---
|
||||||
|
void bindKey(Action action, SDL_Scancode code);
|
||||||
|
void applyKeyboardBindingsFromOptions();
|
||||||
|
void applyGamepadBindingsFromOptions();
|
||||||
|
|
||||||
|
// Configuración por jugador (Orni - dos jugadores)
|
||||||
|
void applyPlayer1BindingsFromOptions();
|
||||||
|
void applyPlayer2BindingsFromOptions();
|
||||||
|
|
||||||
|
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button);
|
||||||
|
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source);
|
||||||
|
|
||||||
|
// --- Consulta de entrada ---
|
||||||
|
auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
||||||
|
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
||||||
|
auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||||
|
void resetInputStates();
|
||||||
|
|
||||||
|
// Consulta por jugador (Orni - dos jugadores)
|
||||||
|
auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
|
||||||
|
auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
|
||||||
|
|
||||||
|
// Check if any player pressed any action from a list
|
||||||
|
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||||
|
|
||||||
|
// --- Gestión de gamepads ---
|
||||||
|
[[nodiscard]] auto gameControllerFound() const -> bool;
|
||||||
|
[[nodiscard]] auto getNumGamepads() const -> int;
|
||||||
|
[[nodiscard]] auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Gamepad>;
|
||||||
|
[[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
|
||||||
|
[[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; }
|
||||||
|
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
|
||||||
|
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
|
||||||
|
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
|
||||||
|
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton;
|
||||||
|
void printConnectedGamepads() const;
|
||||||
|
|
||||||
|
// --- Eventos ---
|
||||||
|
auto handleEvent(const SDL_Event& event) -> std::string;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// --- Constantes ---
|
||||||
|
static constexpr Sint16 AXIS_THRESHOLD = 30000; // Umbral para ejes analógicos
|
||||||
|
static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (50% del rango)
|
||||||
|
static constexpr std::array<Action, 1> BUTTON_INPUTS = {Action::SHOOT}; // Inputs que usan botones
|
||||||
|
|
||||||
|
// --- Métodos ---
|
||||||
|
explicit Input(std::string game_controller_db_path);
|
||||||
|
~Input() = default;
|
||||||
|
|
||||||
|
void initSDLGamePad();
|
||||||
|
static auto checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
||||||
|
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
||||||
|
auto addGamepad(int device_index) -> std::string;
|
||||||
|
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||||
|
void addGamepadMappingsFromFile();
|
||||||
|
void discoverGamepads();
|
||||||
|
|
||||||
|
// --- Variables miembro ---
|
||||||
|
static Input* instance; // Instancia única del singleton
|
||||||
|
|
||||||
|
Gamepads gamepads_; // Lista de gamepads conectados
|
||||||
|
Keyboard keyboard_{}; // Estado del teclado (solo acciones globales)
|
||||||
|
std::string gamepad_mappings_file_; // Ruta al archivo de mappings
|
||||||
|
|
||||||
|
// Referencias cacheadas a gamepads por jugador (Orni)
|
||||||
|
std::shared_ptr<Gamepad> player1_gamepad_;
|
||||||
|
std::shared_ptr<Gamepad> player2_gamepad_;
|
||||||
|
|
||||||
|
// Mapas de bindings separados por jugador (Orni - dos jugadores)
|
||||||
|
std::unordered_map<Action, KeyState> player1_keyboard_bindings_;
|
||||||
|
std::unordered_map<Action, KeyState> player2_keyboard_bindings_;
|
||||||
|
};
|
||||||
60
source/core/input/input_types.cpp
Normal file
60
source/core/input/input_types.cpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#include "input_types.hpp"
|
||||||
|
|
||||||
|
#include <utility> // Para pair
|
||||||
|
|
||||||
|
// Definición de los mapas
|
||||||
|
const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||||
|
{InputAction::LEFT, "LEFT"},
|
||||||
|
{InputAction::RIGHT, "RIGHT"},
|
||||||
|
{InputAction::THRUST, "THRUST"},
|
||||||
|
{InputAction::SHOOT, "SHOOT"},
|
||||||
|
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
||||||
|
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
||||||
|
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
||||||
|
{InputAction::TOGGLE_VSYNC, "TOGGLE_VSYNC"},
|
||||||
|
{InputAction::EXIT, "EXIT"},
|
||||||
|
{InputAction::NONE, "NONE"}};
|
||||||
|
|
||||||
|
const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||||
|
{"LEFT", InputAction::LEFT},
|
||||||
|
{"RIGHT", InputAction::RIGHT},
|
||||||
|
{"THRUST", InputAction::THRUST},
|
||||||
|
{"SHOOT", InputAction::SHOOT},
|
||||||
|
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
||||||
|
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
||||||
|
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
||||||
|
{"TOGGLE_VSYNC", InputAction::TOGGLE_VSYNC},
|
||||||
|
{"EXIT", InputAction::EXIT},
|
||||||
|
{"NONE", InputAction::NONE}};
|
||||||
|
|
||||||
|
const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING = {
|
||||||
|
{SDL_GAMEPAD_BUTTON_WEST, "WEST"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_NORTH, "NORTH"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_EAST, "EAST"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_SOUTH, "SOUTH"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_START, "START"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_BACK, "BACK"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LEFT_SHOULDER"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RIGHT_SHOULDER"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT"},
|
||||||
|
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT"},
|
||||||
|
{static_cast<SDL_GamepadButton>(100), "L2_AS_BUTTON"},
|
||||||
|
{static_cast<SDL_GamepadButton>(101), "R2_AS_BUTTON"}};
|
||||||
|
|
||||||
|
const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON = {
|
||||||
|
{"WEST", SDL_GAMEPAD_BUTTON_WEST},
|
||||||
|
{"NORTH", SDL_GAMEPAD_BUTTON_NORTH},
|
||||||
|
{"EAST", SDL_GAMEPAD_BUTTON_EAST},
|
||||||
|
{"SOUTH", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||||
|
{"START", SDL_GAMEPAD_BUTTON_START},
|
||||||
|
{"BACK", SDL_GAMEPAD_BUTTON_BACK},
|
||||||
|
{"LEFT_SHOULDER", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||||
|
{"RIGHT_SHOULDER", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||||
|
{"DPAD_UP", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||||
|
{"DPAD_DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||||
|
{"DPAD_LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||||
|
{"DPAD_RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||||
|
{"L2_AS_BUTTON", static_cast<SDL_GamepadButton>(100)},
|
||||||
|
{"R2_AS_BUTTON", static_cast<SDL_GamepadButton>(101)}};
|
||||||
41
source/core/input/input_types.hpp
Normal file
41
source/core/input/input_types.hpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
// --- Enums ---
|
||||||
|
enum class InputAction : int { // Acciones de entrada posibles en el juego
|
||||||
|
// Inputs de juego (movimiento y acción)
|
||||||
|
LEFT, // Rotar izquierda
|
||||||
|
RIGHT, // Rotar derecha
|
||||||
|
THRUST, // Acelerar
|
||||||
|
SHOOT, // Disparar
|
||||||
|
START, // Empezar partida
|
||||||
|
|
||||||
|
// Inputs de sistema (globales)
|
||||||
|
WINDOW_INC_ZOOM, // F2
|
||||||
|
WINDOW_DEC_ZOOM, // F1
|
||||||
|
TOGGLE_FULLSCREEN, // F3
|
||||||
|
TOGGLE_VSYNC, // F4
|
||||||
|
EXIT, // ESC
|
||||||
|
|
||||||
|
// Input obligatorio
|
||||||
|
NONE,
|
||||||
|
SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Variables ---
|
||||||
|
extern const std::unordered_map<InputAction, std::string> ACTION_TO_STRING; // Mapeo de acción a string
|
||||||
|
extern const std::unordered_map<std::string, InputAction> STRING_TO_ACTION; // Mapeo de string a acción
|
||||||
|
extern const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING; // Mapeo de botón a string
|
||||||
|
extern const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON; // Mapeo de string a botón
|
||||||
|
|
||||||
|
// --- Constantes ---
|
||||||
|
// Physical arcade buttons (excludes directional controls LEFT/RIGHT)
|
||||||
|
static constexpr std::array<InputAction, 3> ARCADE_BUTTONS = {
|
||||||
|
InputAction::SHOOT,
|
||||||
|
InputAction::THRUST,
|
||||||
|
InputAction::START};
|
||||||
90
source/core/input/mouse.cpp
Normal file
90
source/core/input/mouse.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#include "core/input/mouse.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
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 = false; // Estado del cursor (inicia ocult)
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Temps d'inicialització per ignorar esdeveniments fantasma de SDL
|
||||||
|
Uint32 initialization_time = 0;
|
||||||
|
constexpr Uint32 IGNORE_MOTION_DURATION = 1000; // Ignorar primers 1000ms
|
||||||
|
|
||||||
|
void forceHide() {
|
||||||
|
// Forçar ocultació sincronitzant estat SDL i estat intern
|
||||||
|
std::cout << "[Mouse::forceHide] Ocultant cursor i sincronitzant estat. cursor_visible=" << cursor_visible
|
||||||
|
<< " -> false" << '\n';
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
last_mouse_move_time = 0;
|
||||||
|
initialization_time = SDL_GetTicks(); // Marcar temps per ignorar esdeveniments inicials
|
||||||
|
std::cout << "[Mouse::forceHide] Ignorant moviments durant " << IGNORE_MOTION_DURATION << "ms" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void setForceHidden(bool force) {
|
||||||
|
force_hidden = force;
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
// Entrando en modo oculto forzado: ocultar cursor inmediatamente
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
} else {
|
||||||
|
// Saliendo de modo oculto forzado: NO mostrar cursor automáticamente
|
||||||
|
// El cursor permanece oculto hasta que haya movimiento de ratón (handleEvent)
|
||||||
|
last_mouse_move_time = SDL_GetTicks(); // Resetear temporizador
|
||||||
|
// cursor_visible permanece false - handleEvent lo cambiará al detectar movimiento
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
Uint32 current_time = SDL_GetTicks();
|
||||||
|
|
||||||
|
// Ignorar esdeveniments fantasma de SDL durant el període inicial
|
||||||
|
if (initialization_time > 0 && (current_time - initialization_time < IGNORE_MOTION_DURATION)) {
|
||||||
|
std::cout << "[Mouse::handleEvent] Ignorant moviment fantasma de SDL. time=" << current_time
|
||||||
|
<< " (inicialització fa " << (current_time - initialization_time) << "ms)" << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_mouse_move_time = current_time;
|
||||||
|
if (!cursor_visible) {
|
||||||
|
std::cout << "[Mouse::handleEvent] Mostrant cursor per moviment REAL. time=" << last_mouse_move_time << '\n';
|
||||||
|
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)) {
|
||||||
|
std::cout << "[Mouse::updateCursorVisibility] Ocultant cursor per timeout. current=" << current_time
|
||||||
|
<< " last=" << last_mouse_move_time << " diff=" << (current_time - last_mouse_move_time) << '\n';
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace Mouse
|
||||||
17
source/core/input/mouse.hpp
Normal file
17
source/core/input/mouse.hpp
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#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 forceHide(); // Forçar ocultació del cursor (sincronitza estat intern)
|
||||||
|
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
|
||||||
44
source/core/math/easing.hpp
Normal file
44
source/core/math/easing.hpp
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// easing.hpp - Funcions d'interpolació i easing
|
||||||
|
// © 2025 Orni Attack
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Easing {
|
||||||
|
|
||||||
|
// Ease-out quadratic: empieza rápido, desacelera suavemente
|
||||||
|
// t = progreso normalizado [0.0 - 1.0]
|
||||||
|
// retorna valor interpolado [0.0 - 1.0]
|
||||||
|
inline float ease_out_quad(float t) {
|
||||||
|
return 1.0F - ((1.0F - t) * (1.0F - t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ease-in quadratic: empieza lento, acelera
|
||||||
|
// t = progreso normalizado [0.0 - 1.0]
|
||||||
|
// retorna valor interpolado [0.0 - 1.0]
|
||||||
|
inline float ease_in_quad(float t) {
|
||||||
|
return t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ease-in-out quadratic: acelera al inicio, desacelera al final
|
||||||
|
// t = progreso normalizado [0.0 - 1.0]
|
||||||
|
// retorna valor interpolado [0.0 - 1.0]
|
||||||
|
inline float ease_in_out_quad(float t) {
|
||||||
|
return (t < 0.5F)
|
||||||
|
? 2.0F * t * t
|
||||||
|
: 1.0F - ((-2.0F * t + 2.0F) * (-2.0F * t + 2.0F) / 2.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ease-out cubic: desaceleración más suave que quadratic
|
||||||
|
// t = progreso normalizado [0.0 - 1.0]
|
||||||
|
// retorna valor interpolado [0.0 - 1.0]
|
||||||
|
inline float ease_out_cubic(float t) {
|
||||||
|
float t1 = 1.0F - t;
|
||||||
|
return 1.0F - (t1 * t1 * t1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolación lineal básica (para referencia)
|
||||||
|
inline float lerp(float start, float end, float t) {
|
||||||
|
return start + ((end - start) * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Easing
|
||||||
32
source/core/physics/collision.hpp
Normal file
32
source/core/physics/collision.hpp
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// collision.hpp - Utilitats de detecció de col·lisions
|
||||||
|
// © 2025 Orni Attack - Sistema de física
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/entities/entitat.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
namespace Physics {
|
||||||
|
|
||||||
|
// Comprovació genèrica de col·lisió entre dues entitats
|
||||||
|
inline bool check_collision(const Entities::Entitat& a, const Entities::Entitat& b, float amplifier = 1.0F) {
|
||||||
|
// Comprovar si ambdós són col·lisionables
|
||||||
|
if (!a.es_collidable() || !b.es_collidable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular radi combinat (amb amplificador per hitbox generós)
|
||||||
|
float suma_radis = (a.get_collision_radius() + b.get_collision_radius()) * amplifier;
|
||||||
|
float suma_radis_sq = suma_radis * suma_radis;
|
||||||
|
|
||||||
|
// Comprovació distància al quadrat (sense sqrt)
|
||||||
|
const Punt& pos_a = a.get_centre();
|
||||||
|
const Punt& pos_b = b.get_centre();
|
||||||
|
float dx = pos_a.x - pos_b.x;
|
||||||
|
float dy = pos_a.y - pos_b.y;
|
||||||
|
float dist_sq = (dx * dx) + (dy * dy);
|
||||||
|
|
||||||
|
return dist_sq <= suma_radis_sq;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Physics
|
||||||
68
source/core/rendering/color_oscillator.cpp
Normal file
68
source/core/rendering/color_oscillator.cpp
Normal 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_ = {.r = Defaults::Color::LINE_MIN_R,
|
||||||
|
.g = Defaults::Color::LINE_MIN_G,
|
||||||
|
.b = Defaults::Color::LINE_MIN_B,
|
||||||
|
.a = 255};
|
||||||
|
current_background_color_ = {.r = Defaults::Color::BACKGROUND_MIN_R,
|
||||||
|
.g = Defaults::Color::BACKGROUND_MIN_G,
|
||||||
|
.b = Defaults::Color::BACKGROUND_MIN_B,
|
||||||
|
.a = 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
|
||||||
29
source/core/rendering/color_oscillator.hpp
Normal file
29
source/core/rendering/color_oscillator.hpp
Normal 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);
|
||||||
|
|
||||||
|
[[nodiscard]] SDL_Color getCurrentLineColor() const { return current_line_color_; }
|
||||||
|
[[nodiscard]] 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
|
||||||
11
source/core/rendering/coordinate_transform.cpp
Normal file
11
source/core/rendering/coordinate_transform.cpp
Normal 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
|
||||||
31
source/core/rendering/coordinate_transform.hpp
Normal file
31
source/core/rendering/coordinate_transform.hpp
Normal 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
|
||||||
102
source/core/rendering/line_renderer.cpp
Normal file
102
source/core/rendering/line_renderer.cpp
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// 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 != nullptr)) {
|
||||||
|
// 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
|
||||||
16
source/core/rendering/line_renderer.hpp
Normal file
16
source/core/rendering/line_renderer.hpp
Normal 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
|
||||||
86
source/core/rendering/polygon_renderer.cpp
Normal file
86
source/core/rendering/polygon_renderer.cpp
Normal 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
|
||||||
22
source/core/rendering/polygon_renderer.hpp
Normal file
22
source/core/rendering/polygon_renderer.hpp
Normal 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
|
||||||
66
source/core/rendering/primitives.cpp
Normal file
66
source/core/rendering/primitives.cpp
Normal 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;
|
||||||
|
}
|
||||||
32
source/core/rendering/primitives.hpp
Normal file
32
source/core/rendering/primitives.hpp
Normal 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);
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include "core/defaults.hpp"
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/input/mouse.hpp"
|
||||||
|
#include "core/rendering/coordinate_transform.hpp"
|
||||||
#include "core/rendering/line_renderer.hpp"
|
#include "core/rendering/line_renderer.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "project.h"
|
#include "project.h"
|
||||||
@@ -15,17 +17,21 @@
|
|||||||
SDLManager::SDLManager()
|
SDLManager::SDLManager()
|
||||||
: finestra_(nullptr),
|
: finestra_(nullptr),
|
||||||
renderer_(nullptr),
|
renderer_(nullptr),
|
||||||
fps_accumulator_(0.0f),
|
fps_accumulator_(0.0F),
|
||||||
fps_frame_count_(0),
|
fps_frame_count_(0),
|
||||||
fps_display_(0),
|
fps_display_(0),
|
||||||
current_width_(Defaults::Window::WIDTH),
|
current_width_(Defaults::Window::WIDTH),
|
||||||
current_height_(Defaults::Window::HEIGHT),
|
current_height_(Defaults::Window::HEIGHT),
|
||||||
is_fullscreen_(false),
|
is_fullscreen_(false),
|
||||||
max_width_(1920),
|
max_width_(1920),
|
||||||
max_height_(1080) {
|
max_height_(1080),
|
||||||
|
zoom_factor_(Defaults::Window::BASE_ZOOM),
|
||||||
|
windowed_width_(Defaults::Window::WIDTH),
|
||||||
|
windowed_height_(Defaults::Window::HEIGHT),
|
||||||
|
max_zoom_(1.0F) {
|
||||||
// Inicialitzar SDL3
|
// Inicialitzar SDL3
|
||||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||||
std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << std::endl;
|
std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +47,8 @@ SDLManager::SDLManager()
|
|||||||
SDL_WINDOW_RESIZABLE // Permetre resize manual també
|
SDL_WINDOW_RESIZABLE // Permetre resize manual també
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!finestra_) {
|
if (finestra_ == nullptr) {
|
||||||
std::cerr << "Error creant finestra: " << SDL_GetError() << std::endl;
|
std::cerr << "Error creant finestra: " << SDL_GetError() << '\n';
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,8 +59,8 @@ SDLManager::SDLManager()
|
|||||||
// Crear renderer amb acceleració
|
// Crear renderer amb acceleració
|
||||||
renderer_ = SDL_CreateRenderer(finestra_, nullptr);
|
renderer_ = SDL_CreateRenderer(finestra_, nullptr);
|
||||||
|
|
||||||
if (!renderer_) {
|
if (renderer_ == nullptr) {
|
||||||
std::cerr << "Error creant renderer: " << SDL_GetError() << std::endl;
|
std::cerr << "Error creant renderer: " << SDL_GetError() << '\n';
|
||||||
SDL_DestroyWindow(finestra_);
|
SDL_DestroyWindow(finestra_);
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
return;
|
return;
|
||||||
@@ -68,24 +74,28 @@ SDLManager::SDLManager()
|
|||||||
|
|
||||||
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
|
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
|
||||||
<< " (logic: " << Defaults::Game::WIDTH << "x"
|
<< " (logic: " << Defaults::Game::WIDTH << "x"
|
||||||
<< Defaults::Game::HEIGHT << ")" << std::endl;
|
<< Defaults::Game::HEIGHT << ")" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor amb configuració
|
// Constructor amb configuració
|
||||||
SDLManager::SDLManager(int width, int height, bool fullscreen)
|
SDLManager::SDLManager(int width, int height, bool fullscreen)
|
||||||
: finestra_(nullptr),
|
: finestra_(nullptr),
|
||||||
renderer_(nullptr),
|
renderer_(nullptr),
|
||||||
fps_accumulator_(0.0f),
|
fps_accumulator_(0.0F),
|
||||||
fps_frame_count_(0),
|
fps_frame_count_(0),
|
||||||
fps_display_(0),
|
fps_display_(0),
|
||||||
current_width_(width),
|
current_width_(width),
|
||||||
current_height_(height),
|
current_height_(height),
|
||||||
is_fullscreen_(fullscreen),
|
is_fullscreen_(fullscreen),
|
||||||
max_width_(1920),
|
max_width_(1920),
|
||||||
max_height_(1080) {
|
max_height_(1080),
|
||||||
|
zoom_factor_(static_cast<float>(width) / Defaults::Window::WIDTH),
|
||||||
|
windowed_width_(width),
|
||||||
|
windowed_height_(height),
|
||||||
|
max_zoom_(1.0F) {
|
||||||
// Inicialitzar SDL3
|
// Inicialitzar SDL3
|
||||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||||
std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << std::endl;
|
std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +114,8 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
|
|||||||
// Crear finestra
|
// Crear finestra
|
||||||
finestra_ = SDL_CreateWindow(window_title.c_str(), current_width_, current_height_, flags);
|
finestra_ = SDL_CreateWindow(window_title.c_str(), current_width_, current_height_, flags);
|
||||||
|
|
||||||
if (!finestra_) {
|
if (finestra_ == nullptr) {
|
||||||
std::cerr << "Error creant finestra: " << SDL_GetError() << std::endl;
|
std::cerr << "Error creant finestra: " << SDL_GetError() << '\n';
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,8 +128,8 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
|
|||||||
// Crear renderer amb acceleració
|
// Crear renderer amb acceleració
|
||||||
renderer_ = SDL_CreateRenderer(finestra_, nullptr);
|
renderer_ = SDL_CreateRenderer(finestra_, nullptr);
|
||||||
|
|
||||||
if (!renderer_) {
|
if (renderer_ == nullptr) {
|
||||||
std::cerr << "Error creant renderer: " << SDL_GetError() << std::endl;
|
std::cerr << "Error creant renderer: " << SDL_GetError() << '\n';
|
||||||
SDL_DestroyWindow(finestra_);
|
SDL_DestroyWindow(finestra_);
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
return;
|
return;
|
||||||
@@ -131,110 +141,178 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
|
|||||||
// Configurar viewport scaling
|
// Configurar viewport scaling
|
||||||
updateLogicalPresentation();
|
updateLogicalPresentation();
|
||||||
|
|
||||||
|
// Inicialitzar sistema de cursor
|
||||||
|
// En fullscreen: forzar ocultació permanent
|
||||||
|
if (is_fullscreen_) {
|
||||||
|
Mouse::setForceHidden(true);
|
||||||
|
}
|
||||||
|
|
||||||
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
|
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
|
||||||
<< " (logic: " << Defaults::Game::WIDTH << "x"
|
<< " (logic: " << Defaults::Game::WIDTH << "x"
|
||||||
<< Defaults::Game::HEIGHT << ")";
|
<< Defaults::Game::HEIGHT << ")";
|
||||||
if (is_fullscreen_) {
|
if (is_fullscreen_) {
|
||||||
std::cout << " [FULLSCREEN]";
|
std::cout << " [FULLSCREEN]";
|
||||||
}
|
}
|
||||||
std::cout << std::endl;
|
std::cout << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
SDLManager::~SDLManager() {
|
SDLManager::~SDLManager() {
|
||||||
if (renderer_) {
|
if (renderer_ != nullptr) {
|
||||||
SDL_DestroyRenderer(renderer_);
|
SDL_DestroyRenderer(renderer_);
|
||||||
renderer_ = nullptr;
|
renderer_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finestra_) {
|
if (finestra_ != nullptr) {
|
||||||
SDL_DestroyWindow(finestra_);
|
SDL_DestroyWindow(finestra_);
|
||||||
finestra_ = nullptr;
|
finestra_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
std::cout << "SDL3 netejat correctament" << std::endl;
|
std::cout << "SDL3 netejat correctament" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::calculateMaxWindowSize() {
|
void SDLManager::calculateMaxWindowSize() {
|
||||||
SDL_DisplayID display = SDL_GetPrimaryDisplay();
|
SDL_DisplayID display = SDL_GetPrimaryDisplay();
|
||||||
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
|
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
|
||||||
|
|
||||||
if (mode) {
|
if (mode != nullptr) {
|
||||||
// Deixar marge de 100px per a decoracions de l'OS
|
// Deixar marge de 100px per a decoracions de l'OS
|
||||||
max_width_ = mode->w - 100;
|
max_width_ = mode->w - 100;
|
||||||
max_height_ = mode->h - 100;
|
max_height_ = mode->h - 100;
|
||||||
std::cout << "Display detectat: " << mode->w << "x" << mode->h
|
std::cout << "Display detectat: " << mode->w << "x" << mode->h
|
||||||
<< " (max finestra: " << max_width_ << "x" << max_height_ << ")"
|
<< " (max finestra: " << max_width_ << "x" << max_height_ << ")"
|
||||||
<< std::endl;
|
<< '\n';
|
||||||
} else {
|
} else {
|
||||||
// Fallback conservador
|
// Fallback conservador
|
||||||
max_width_ = 1920;
|
max_width_ = 1920;
|
||||||
max_height_ = 1080;
|
max_height_ = 1080;
|
||||||
std::cerr << "No s'ha pogut detectar el display, usant fallback: "
|
std::cerr << "No s'ha pogut detectar el display, usant fallback: "
|
||||||
<< max_width_ << "x" << max_height_ << std::endl;
|
<< max_width_ << "x" << max_height_ << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate max zoom immediately after determining max size
|
||||||
|
calculateMaxZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLManager::calculateMaxZoom() {
|
||||||
|
// Maximum zoom limited by BOTH width and height (preserves 4:3)
|
||||||
|
float max_zoom_width = static_cast<float>(max_width_) / Defaults::Window::WIDTH;
|
||||||
|
float max_zoom_height = static_cast<float>(max_height_) / Defaults::Window::HEIGHT;
|
||||||
|
|
||||||
|
// Take smaller constraint
|
||||||
|
float max_zoom_unrounded = std::min(max_zoom_width, max_zoom_height);
|
||||||
|
|
||||||
|
// Round DOWN to nearest 0.1 increment (user preference)
|
||||||
|
max_zoom_ = std::floor(max_zoom_unrounded / Defaults::Window::ZOOM_INCREMENT) * Defaults::Window::ZOOM_INCREMENT;
|
||||||
|
|
||||||
|
// Safety clamp
|
||||||
|
max_zoom_ = std::max(max_zoom_, Defaults::Window::MIN_ZOOM);
|
||||||
|
|
||||||
|
std::cout << "Max zoom: " << max_zoom_ << "x (display: "
|
||||||
|
<< max_width_ << "x" << max_height_ << ")" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLManager::applyZoom(float new_zoom) {
|
||||||
|
// Clamp to valid range
|
||||||
|
new_zoom = std::max(Defaults::Window::MIN_ZOOM,
|
||||||
|
std::min(new_zoom, max_zoom_));
|
||||||
|
|
||||||
|
// Round to nearest 0.1 increment
|
||||||
|
new_zoom = std::round(new_zoom / Defaults::Window::ZOOM_INCREMENT) * Defaults::Window::ZOOM_INCREMENT;
|
||||||
|
|
||||||
|
// No change?
|
||||||
|
if (std::abs(new_zoom - zoom_factor_) < 0.01F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom_factor_ = new_zoom;
|
||||||
|
|
||||||
|
// Calculate physical dimensions (4:3 maintained automatically)
|
||||||
|
int new_width = static_cast<int>(std::round(
|
||||||
|
Defaults::Window::WIDTH * zoom_factor_));
|
||||||
|
int new_height = static_cast<int>(std::round(
|
||||||
|
Defaults::Window::HEIGHT * zoom_factor_));
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Persist
|
||||||
|
Options::window.width = new_width;
|
||||||
|
Options::window.height = new_height;
|
||||||
|
Options::window.zoom_factor = zoom_factor_;
|
||||||
|
|
||||||
|
std::cout << "Zoom: " << zoom_factor_ << "x ("
|
||||||
|
<< new_width << "x" << new_height << ")" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::updateLogicalPresentation() {
|
void SDLManager::updateLogicalPresentation() {
|
||||||
// AIXÒ ÉS LA MÀGIA: El joc SEMPRE dibuixa en 640x480,
|
// CANVIAT: Ja no usem SDL_SetRenderLogicalPresentation
|
||||||
// SDL escala automàticament a la mida física de la finestra
|
// Ara renderitzem directament a resolució física per evitar pixelació irregular
|
||||||
SDL_SetRenderLogicalPresentation(
|
// El viewport amb letterbox es configura a updateViewport()
|
||||||
renderer_,
|
updateViewport();
|
||||||
Defaults::Game::WIDTH, // 640 (lògic)
|
}
|
||||||
Defaults::Game::HEIGHT, // 480 (lògic)
|
|
||||||
SDL_LOGICAL_PRESENTATION_LETTERBOX // Mantenir aspect ratio 4:3
|
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 << "]"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLManager::updateRenderingContext() const {
|
||||||
|
// Actualitzar el factor d'escala global per a totes les funcions de renderitzat
|
||||||
|
Rendering::g_current_scale_factor = zoom_factor_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::increaseWindowSize() {
|
void SDLManager::increaseWindowSize() {
|
||||||
if (is_fullscreen_)
|
if (is_fullscreen_) {
|
||||||
return; // No operar en fullscreen
|
return;
|
||||||
|
|
||||||
int new_width = current_width_ + Defaults::Window::SIZE_INCREMENT;
|
|
||||||
int new_height = current_height_ + Defaults::Window::SIZE_INCREMENT;
|
|
||||||
|
|
||||||
// Clamp a màxim
|
|
||||||
new_width = std::min(new_width, max_width_);
|
|
||||||
new_height = std::min(new_height, max_height_);
|
|
||||||
|
|
||||||
if (new_width != current_width_ || new_height != current_height_) {
|
|
||||||
applyWindowSize(new_width, new_height);
|
|
||||||
|
|
||||||
// Persistir canvis a Options (es guardarà a config.yaml al tancar)
|
|
||||||
Options::window.width = current_width_;
|
|
||||||
Options::window.height = current_height_;
|
|
||||||
|
|
||||||
std::cout << "F2: Finestra augmentada a " << new_width << "x" << new_height
|
|
||||||
<< std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
|
||||||
|
applyZoom(new_zoom);
|
||||||
|
|
||||||
|
std::cout << "F2: Zoom aumentat a " << zoom_factor_ << "x" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::decreaseWindowSize() {
|
void SDLManager::decreaseWindowSize() {
|
||||||
if (is_fullscreen_)
|
if (is_fullscreen_) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int new_width = current_width_ - Defaults::Window::SIZE_INCREMENT;
|
|
||||||
int new_height = current_height_ - Defaults::Window::SIZE_INCREMENT;
|
|
||||||
|
|
||||||
// Clamp a mínim
|
|
||||||
new_width = std::max(new_width, Defaults::Window::MIN_WIDTH);
|
|
||||||
new_height = std::max(new_height, Defaults::Window::MIN_HEIGHT);
|
|
||||||
|
|
||||||
if (new_width != current_width_ || new_height != current_height_) {
|
|
||||||
applyWindowSize(new_width, new_height);
|
|
||||||
|
|
||||||
// Persistir canvis a Options (es guardarà a config.yaml al tancar)
|
|
||||||
Options::window.width = current_width_;
|
|
||||||
Options::window.height = current_height_;
|
|
||||||
|
|
||||||
std::cout << "F1: Finestra reduïda a " << new_width << "x" << new_height
|
|
||||||
<< std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
|
||||||
|
applyZoom(new_zoom);
|
||||||
|
|
||||||
|
std::cout << "F1: Zoom reduït a " << zoom_factor_ << "x" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::applyWindowSize(int new_width, int new_height) {
|
void SDLManager::applyWindowSize(int new_width, int new_height) {
|
||||||
// Obtenir posició actual ABANS del resize
|
// Obtenir posició actual ABANS del resize
|
||||||
int old_x, old_y;
|
int old_x;
|
||||||
|
int old_y;
|
||||||
SDL_GetWindowPosition(finestra_, &old_x, &old_y);
|
SDL_GetWindowPosition(finestra_, &old_x, &old_y);
|
||||||
|
|
||||||
int old_width = current_width_;
|
int old_width = current_width_;
|
||||||
@@ -260,39 +338,70 @@ void SDLManager::applyWindowSize(int new_width, int new_height) {
|
|||||||
|
|
||||||
SDL_SetWindowPosition(finestra_, new_x, new_y);
|
SDL_SetWindowPosition(finestra_, new_x, new_y);
|
||||||
|
|
||||||
// NO cal actualitzar el logical presentation aquí,
|
// Actualitzar viewport després del resize
|
||||||
// SDL ho maneja automàticament
|
updateViewport();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::toggleFullscreen() {
|
void SDLManager::toggleFullscreen() {
|
||||||
is_fullscreen_ = !is_fullscreen_;
|
if (!is_fullscreen_) {
|
||||||
SDL_SetWindowFullscreen(finestra_, is_fullscreen_);
|
// ENTERING FULLSCREEN
|
||||||
|
windowed_width_ = current_width_;
|
||||||
|
windowed_height_ = current_height_;
|
||||||
|
|
||||||
|
is_fullscreen_ = true;
|
||||||
|
SDL_SetWindowFullscreen(finestra_, true);
|
||||||
|
|
||||||
|
std::cout << "F3: Fullscreen activat (guardada: "
|
||||||
|
<< windowed_width_ << "x" << windowed_height_ << ")" << '\n';
|
||||||
|
} else {
|
||||||
|
// EXITING FULLSCREEN
|
||||||
|
is_fullscreen_ = false;
|
||||||
|
SDL_SetWindowFullscreen(finestra_, false);
|
||||||
|
|
||||||
|
// CRITICAL: Explicitly restore windowed size
|
||||||
|
applyWindowSize(windowed_width_, windowed_height_);
|
||||||
|
|
||||||
|
std::cout << "F3: Fullscreen desactivat (restaurada: "
|
||||||
|
<< windowed_width_ << "x" << windowed_height_ << ")" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
// Persistir canvis a Options (es guardarà a config.yaml al tancar)
|
|
||||||
Options::window.fullscreen = is_fullscreen_;
|
Options::window.fullscreen = is_fullscreen_;
|
||||||
|
|
||||||
std::cout << "F3: Fullscreen " << (is_fullscreen_ ? "activat" : "desactivat")
|
// Notificar al mòdul Mouse: Fullscreen requereix ocultació permanent del cursor.
|
||||||
<< std::endl;
|
// Quan es surt de fullscreen, restaurar el comportament normal d'auto-ocultació.
|
||||||
|
Mouse::setForceHidden(is_fullscreen_);
|
||||||
// En fullscreen, SDL gestiona tot automàticament
|
|
||||||
// En sortir, restaura la mida anterior
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SDLManager::handleWindowEvent(const SDL_Event& event) {
|
bool SDLManager::handleWindowEvent(const SDL_Event& event) {
|
||||||
if (event.type == SDL_EVENT_WINDOW_RESIZED) {
|
if (event.type == SDL_EVENT_WINDOW_RESIZED) {
|
||||||
// Usuari ha redimensionat manualment (arrossegar vora)
|
|
||||||
// Actualitzar el nostre tracking
|
|
||||||
SDL_GetWindowSize(finestra_, ¤t_width_, ¤t_height_);
|
SDL_GetWindowSize(finestra_, ¤t_width_, ¤t_height_);
|
||||||
std::cout << "Finestra redimensionada manualment a " << current_width_
|
|
||||||
<< "x" << current_height_ << std::endl;
|
// Calculate zoom from actual size (may not align to 0.1 increments)
|
||||||
|
float new_zoom = static_cast<float>(current_width_) / Defaults::Window::WIDTH;
|
||||||
|
zoom_factor_ = std::max(Defaults::Window::MIN_ZOOM,
|
||||||
|
std::min(new_zoom, max_zoom_));
|
||||||
|
|
||||||
|
// Update windowed cache (if not in fullscreen)
|
||||||
|
if (!is_fullscreen_) {
|
||||||
|
windowed_width_ = current_width_;
|
||||||
|
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)"
|
||||||
|
<< '\n';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::neteja(uint8_t r, uint8_t g, uint8_t b) {
|
void SDLManager::neteja(uint8_t r, uint8_t g, uint8_t b) {
|
||||||
if (!renderer_)
|
if (renderer_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// [MODIFICAT] Usar color oscil·lat del fons en lloc dels paràmetres
|
// [MODIFICAT] Usar color oscil·lat del fons en lloc dels paràmetres
|
||||||
(void)r;
|
(void)r;
|
||||||
@@ -304,8 +413,9 @@ void SDLManager::neteja(uint8_t r, uint8_t g, uint8_t b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::presenta() {
|
void SDLManager::presenta() {
|
||||||
if (!renderer_)
|
if (renderer_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
SDL_RenderPresent(renderer_);
|
SDL_RenderPresent(renderer_);
|
||||||
}
|
}
|
||||||
@@ -325,19 +435,21 @@ void SDLManager::updateFPS(float delta_time) {
|
|||||||
fps_frame_count_++;
|
fps_frame_count_++;
|
||||||
|
|
||||||
// Actualitzar display cada 0.5 segons
|
// Actualitzar display cada 0.5 segons
|
||||||
if (fps_accumulator_ >= 0.5f) {
|
if (fps_accumulator_ >= 0.5F) {
|
||||||
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_);
|
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_);
|
||||||
fps_frame_count_ = 0;
|
fps_frame_count_ = 0;
|
||||||
fps_accumulator_ = 0.0f;
|
fps_accumulator_ = 0.0F;
|
||||||
|
|
||||||
// Actualitzar títol de la finestra
|
// 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::LONG_NAME,
|
||||||
Project::VERSION,
|
Project::VERSION,
|
||||||
Project::COPYRIGHT,
|
Project::COPYRIGHT,
|
||||||
fps_display_);
|
fps_display_,
|
||||||
|
vsync_state);
|
||||||
|
|
||||||
if (finestra_) {
|
if (finestra_ != nullptr) {
|
||||||
SDL_SetWindowTitle(finestra_, title.c_str());
|
SDL_SetWindowTitle(finestra_, title.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,7 +457,7 @@ void SDLManager::updateFPS(float delta_time) {
|
|||||||
|
|
||||||
// [NUEVO] Actualitzar títol de la finestra
|
// [NUEVO] Actualitzar títol de la finestra
|
||||||
void SDLManager::setWindowTitle(const std::string& title) {
|
void SDLManager::setWindowTitle(const std::string& title) {
|
||||||
if (finestra_) {
|
if (finestra_ != nullptr) {
|
||||||
SDL_SetWindowTitle(finestra_, title.c_str());
|
SDL_SetWindowTitle(finestra_, title.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,10 +468,14 @@ void SDLManager::toggleVSync() {
|
|||||||
Options::rendering.vsync = (Options::rendering.vsync == 1) ? 0 : 1;
|
Options::rendering.vsync = (Options::rendering.vsync == 1) ? 0 : 1;
|
||||||
|
|
||||||
// Aplicar a SDL
|
// Aplicar a SDL
|
||||||
if (renderer_) {
|
if (renderer_ != nullptr) {
|
||||||
SDL_SetRenderVSync(renderer_, Options::rendering.vsync);
|
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ó
|
// Guardar configuració
|
||||||
Options::saveToFile();
|
Options::saveToFile();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,8 @@
|
|||||||
|
|
||||||
class SDLManager {
|
class SDLManager {
|
||||||
public:
|
public:
|
||||||
SDLManager(); // Constructor per defecte (usa Defaults::)
|
SDLManager(); // Constructor per defecte (usa Defaults::)
|
||||||
SDLManager(int width, int height,
|
SDLManager(int width, int height, bool fullscreen); // Constructor amb configuració
|
||||||
bool fullscreen); // Constructor amb configuració
|
|
||||||
~SDLManager();
|
~SDLManager();
|
||||||
|
|
||||||
// No permetre còpia ni assignació
|
// No permetre còpia ni assignació
|
||||||
@@ -23,12 +22,11 @@ class SDLManager {
|
|||||||
SDLManager& operator=(const SDLManager&) = delete;
|
SDLManager& operator=(const SDLManager&) = delete;
|
||||||
|
|
||||||
// [NUEVO] Gestió de finestra dinàmica
|
// [NUEVO] Gestió de finestra dinàmica
|
||||||
void increaseWindowSize(); // F2: +100px
|
void increaseWindowSize(); // F2: +100px
|
||||||
void decreaseWindowSize(); // F1: -100px
|
void decreaseWindowSize(); // F1: -100px
|
||||||
void toggleFullscreen(); // F3
|
void toggleFullscreen(); // F3
|
||||||
void toggleVSync(); // F4
|
void toggleVSync(); // F4
|
||||||
bool
|
bool handleWindowEvent(const SDL_Event& event); // Per a SDL_EVENT_WINDOW_RESIZED
|
||||||
handleWindowEvent(const SDL_Event& event); // Per a SDL_EVENT_WINDOW_RESIZED
|
|
||||||
|
|
||||||
// Funcions principals (renderitzat)
|
// Funcions principals (renderitzat)
|
||||||
void neteja(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);
|
void neteja(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);
|
||||||
@@ -42,10 +40,14 @@ class SDLManager {
|
|||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
SDL_Renderer* obte_renderer() { return renderer_; }
|
SDL_Renderer* obte_renderer() { return renderer_; }
|
||||||
|
[[nodiscard]] float getScaleFactor() const { return zoom_factor_; }
|
||||||
|
|
||||||
// [NUEVO] Actualitzar títol de la finestra
|
// [NUEVO] Actualitzar títol de la finestra
|
||||||
void setWindowTitle(const std::string& title);
|
void setWindowTitle(const std::string& title);
|
||||||
|
|
||||||
|
// [NUEVO] Actualitzar context de renderitzat (factor d'escala global)
|
||||||
|
void updateRenderingContext() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_Window* finestra_;
|
SDL_Window* finestra_;
|
||||||
SDL_Renderer* renderer_;
|
SDL_Renderer* renderer_;
|
||||||
@@ -62,10 +64,19 @@ class SDLManager {
|
|||||||
int max_width_; // Calculat des del display
|
int max_width_; // Calculat des del display
|
||||||
int max_height_;
|
int max_height_;
|
||||||
|
|
||||||
|
// [ZOOM SYSTEM]
|
||||||
|
float zoom_factor_; // Current zoom (0.5x to max_zoom_)
|
||||||
|
int windowed_width_; // Saved size before fullscreen
|
||||||
|
int windowed_height_; // Saved size before fullscreen
|
||||||
|
float max_zoom_; // Maximum zoom (calculated from display)
|
||||||
|
|
||||||
// [NUEVO] Funcions internes
|
// [NUEVO] Funcions internes
|
||||||
void calculateMaxWindowSize(); // Llegir resolució del display
|
void calculateMaxWindowSize(); // Llegir resolució del display
|
||||||
|
void calculateMaxZoom(); // Calculate max zoom from display
|
||||||
|
void applyZoom(float new_zoom); // Apply zoom and resize window
|
||||||
void applyWindowSize(int width, int height); // Canviar mida + centrar
|
void applyWindowSize(int width, int height); // Canviar mida + centrar
|
||||||
void updateLogicalPresentation(); // Actualitzar viewport
|
void updateLogicalPresentation(); // Actualitzar viewport
|
||||||
|
void updateViewport(); // Configurar viewport amb letterbox
|
||||||
|
|
||||||
// [NUEVO] Oscil·lador de colors
|
// [NUEVO] Oscil·lador de colors
|
||||||
Rendering::ColorOscillator color_oscillator_;
|
Rendering::ColorOscillator color_oscillator_;
|
||||||
|
|||||||
119
source/core/rendering/shape_renderer.cpp
Normal file
119
source/core/rendering/shape_renderer.cpp
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// 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: aplicar rotació 3D a un punt 2D (assumeix Z=0)
|
||||||
|
static Punt apply_3d_rotation(float x, float y, const Rotation3D& rot) {
|
||||||
|
float z = 0.0F; // Tots els punts 2D comencen a Z=0
|
||||||
|
|
||||||
|
// Pitch (rotació eix X): cabeceo arriba/baix
|
||||||
|
float cos_pitch = std::cos(rot.pitch);
|
||||||
|
float sin_pitch = std::sin(rot.pitch);
|
||||||
|
float y1 = (y * cos_pitch) - (z * sin_pitch);
|
||||||
|
float z1 = (y * sin_pitch) + (z * cos_pitch);
|
||||||
|
|
||||||
|
// Yaw (rotació eix Y): guiñada esquerra/dreta
|
||||||
|
float cos_yaw = std::cos(rot.yaw);
|
||||||
|
float sin_yaw = std::sin(rot.yaw);
|
||||||
|
float x2 = (x * cos_yaw) + (z1 * sin_yaw);
|
||||||
|
float z2 = (-x * sin_yaw) + (z1 * cos_yaw);
|
||||||
|
|
||||||
|
// Roll (rotació eix Z): alabeo lateral
|
||||||
|
float cos_roll = std::cos(rot.roll);
|
||||||
|
float sin_roll = std::sin(rot.roll);
|
||||||
|
float x3 = (x2 * cos_roll) - (y1 * sin_roll);
|
||||||
|
float y3 = (x2 * sin_roll) + (y1 * cos_roll);
|
||||||
|
|
||||||
|
// Proyecció perspectiva (Z-divide simple)
|
||||||
|
// Naus volen cap al punt de fuga (320, 240) a "infinit" (Z → +∞)
|
||||||
|
// Z més gran = més lluny = més petit a pantalla
|
||||||
|
constexpr float perspective_factor = 500.0F;
|
||||||
|
float scale_factor = perspective_factor / (perspective_factor + z2);
|
||||||
|
|
||||||
|
return {.x = x3 * scale_factor, .y = y3 * scale_factor};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, const Rotation3D* rotation_3d) {
|
||||||
|
// 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 rotació 3D (si es proporciona)
|
||||||
|
if ((rotation_3d != nullptr) && rotation_3d->has_rotation()) {
|
||||||
|
Punt rotated_3d = apply_3d_rotation(centered_x, centered_y, *rotation_3d);
|
||||||
|
centered_x = rotated_3d.x;
|
||||||
|
centered_y = rotated_3d.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Aplicar escala al punt (després de rotació 3D)
|
||||||
|
float scaled_x = centered_x * escala;
|
||||||
|
float scaled_y = centered_y * escala;
|
||||||
|
|
||||||
|
// 4. Aplicar rotació 2D (Z-axis, tradicional)
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 5. Aplicar trasllació a posició mundial
|
||||||
|
return {.x = rotated_x + posicio.x, .y = 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,
|
||||||
|
const Rotation3D* rotation_3d) {
|
||||||
|
// 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, rotation_3d);
|
||||||
|
Punt p2 = transform_point(primitive.points[i + 1], shape_centre, posicio, angle, escala, rotation_3d);
|
||||||
|
|
||||||
|
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, rotation_3d);
|
||||||
|
Punt p2 = transform_point(primitive.points[1], shape_centre, posicio, angle, escala, rotation_3d);
|
||||||
|
|
||||||
|
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
|
||||||
54
source/core/rendering/shape_renderer.hpp
Normal file
54
source/core/rendering/shape_renderer.hpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// 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 {
|
||||||
|
|
||||||
|
// Estructura per rotacions 3D (pitch, yaw, roll)
|
||||||
|
struct Rotation3D {
|
||||||
|
float pitch; // Rotació eix X (cabeceo arriba/baix)
|
||||||
|
float yaw; // Rotació eix Y (guiñada esquerra/dreta)
|
||||||
|
float roll; // Rotació eix Z (alabeo lateral)
|
||||||
|
|
||||||
|
Rotation3D()
|
||||||
|
: pitch(0.0F),
|
||||||
|
yaw(0.0F),
|
||||||
|
roll(0.0F) {}
|
||||||
|
Rotation3D(float p, float y, float r)
|
||||||
|
: pitch(p),
|
||||||
|
yaw(y),
|
||||||
|
roll(r) {}
|
||||||
|
|
||||||
|
[[nodiscard]] bool has_rotation() const {
|
||||||
|
return pitch != 0.0F || yaw != 0.0F || roll != 0.0F;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
const Rotation3D* rotation_3d = nullptr);
|
||||||
|
|
||||||
|
} // namespace Rendering
|
||||||
81
source/core/resources/resource_helper.cpp
Normal file
81
source/core/resources/resource_helper.cpp
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// resource_helper.cpp - Implementació de funcions d'ajuda
|
||||||
|
// © 2025 Port a C++20 amb SDL3
|
||||||
|
|
||||||
|
#include "resource_helper.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "resource_loader.hpp"
|
||||||
|
|
||||||
|
namespace Resource::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 Resource::Helper
|
||||||
27
source/core/resources/resource_helper.hpp
Normal file
27
source/core/resources/resource_helper.hpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// 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 <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Resource::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 Resource::Helper
|
||||||
143
source/core/resources/resource_loader.cpp
Normal file
143
source/core/resources/resource_loader.cpp
Normal 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
|
||||||
53
source/core/resources/resource_loader.hpp
Normal file
53
source/core/resources/resource_loader.hpp
Normal 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 <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "resource_pack.hpp"
|
||||||
|
|
||||||
|
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();
|
||||||
|
[[nodiscard]] bool isPackLoaded() const;
|
||||||
|
|
||||||
|
// Estat
|
||||||
|
void setBasePath(const std::string& path);
|
||||||
|
[[nodiscard]] std::string getBasePath() const;
|
||||||
|
|
||||||
|
// No es pot copiar ni moure
|
||||||
|
Loader(const Loader&) = delete;
|
||||||
|
Loader& operator=(const Loader&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Loader() = default;
|
||||||
|
~Loader() = default;
|
||||||
|
|
||||||
|
// 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
|
||||||
309
source/core/resources/resource_pack.cpp
Normal file
309
source/core/resources/resource_pack.cpp
Normal 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.data(), 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_.contains(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
67
source/core/resources/resource_pack.hpp
Normal file
67
source/core/resources/resource_pack.hpp
Normal 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);
|
||||||
|
[[nodiscard]] bool hasResource(const std::string& filename) const;
|
||||||
|
[[nodiscard]] std::vector<std::string> getResourceList() const;
|
||||||
|
|
||||||
|
// Validació
|
||||||
|
[[nodiscard]] 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);
|
||||||
|
[[nodiscard]] 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
|
||||||
81
source/core/system/context_escenes.hpp
Normal file
81
source/core/system/context_escenes.hpp
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// context_escenes.hpp - Sistema de gestió d'escenes i context de transicions
|
||||||
|
// © 2025 Port a C++20
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/system/game_config.hpp"
|
||||||
|
|
||||||
|
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() = default;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar partida abans de transicionar a JOC
|
||||||
|
void set_config_partida(const GameConfig::ConfigPartida& config) {
|
||||||
|
config_partida_ = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir configuració de partida (consumit per EscenaJoc)
|
||||||
|
[[nodiscard]] const GameConfig::ConfigPartida& get_config_partida() const {
|
||||||
|
return config_partida_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Escena escena_desti_{Escena::LOGO}; // Escena a la qual transicionar
|
||||||
|
Opcio opcio_{Opcio::NONE}; // Opció específica per l'escena
|
||||||
|
GameConfig::ConfigPartida config_partida_; // Configuració de partida (jugadors actius, mode)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
282
source/core/system/director.cpp
Normal file
282
source/core/system/director.cpp
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
#include "director.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "context_escenes.hpp"
|
||||||
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/audio/audio_cache.hpp"
|
||||||
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/input/input.hpp"
|
||||||
|
#include "core/input/mouse.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 "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();
|
||||||
|
|
||||||
|
// Inicialitzar sistema d'input
|
||||||
|
Input::init("data/gamecontrollerdb.txt");
|
||||||
|
|
||||||
|
// Aplicar configuració de controls dels jugadors
|
||||||
|
Input::get()->applyPlayer1BindingsFromOptions();
|
||||||
|
Input::get()->applyPlayer2BindingsFromOptions();
|
||||||
|
|
||||||
|
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 << " Input: " << Input::get()->getNumGamepads()
|
||||||
|
<< " gamepad(s) detectat(s)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
Director::~Director() {
|
||||||
|
// Guardar opcions
|
||||||
|
Options::saveToFile();
|
||||||
|
|
||||||
|
// Cleanup input
|
||||||
|
Input::destroy();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// CRÍTIC: Forçar ocultació del cursor DESPRÉS de tota la inicialització SDL
|
||||||
|
// Això evita que SDL mostre el cursor automàticament durant la creació de la finestra
|
||||||
|
if (!Options::window.fullscreen) {
|
||||||
|
Mouse::forceHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
AudioCache::getMusic("game.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::TITOL);
|
||||||
|
#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;
|
||||||
|
}
|
||||||
20
source/core/system/director.hpp
Normal file
20
source/core/system/director.hpp
Normal 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);
|
||||||
|
};
|
||||||
55
source/core/system/game_config.hpp
Normal file
55
source/core/system/game_config.hpp
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace GameConfig {
|
||||||
|
|
||||||
|
// Mode de joc
|
||||||
|
enum class Mode {
|
||||||
|
NORMAL, // Partida normal
|
||||||
|
DEMO // Mode demostració (futur)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configuració d'una partida
|
||||||
|
struct ConfigPartida {
|
||||||
|
bool jugador1_actiu{false}; // És actiu el jugador 1?
|
||||||
|
bool jugador2_actiu{false}; // És actiu el jugador 2?
|
||||||
|
Mode mode{Mode::NORMAL}; // Mode de joc
|
||||||
|
|
||||||
|
// Mètodes auxiliars
|
||||||
|
|
||||||
|
// Retorna true si només hi ha un jugador actiu
|
||||||
|
[[nodiscard]] bool es_un_jugador() const {
|
||||||
|
return (jugador1_actiu && !jugador2_actiu) ||
|
||||||
|
(!jugador1_actiu && jugador2_actiu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retorna true si hi ha dos jugadors actius
|
||||||
|
[[nodiscard]] bool son_dos_jugadors() const {
|
||||||
|
return jugador1_actiu && jugador2_actiu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retorna true si no hi ha cap jugador actiu
|
||||||
|
[[nodiscard]] bool cap_jugador() const {
|
||||||
|
return !jugador1_actiu && !jugador2_actiu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compte de jugadors actius (0, 1 o 2)
|
||||||
|
[[nodiscard]] uint8_t compte_jugadors() const {
|
||||||
|
return (jugador1_actiu ? 1 : 0) + (jugador2_actiu ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retorna l'ID de l'únic jugador actiu (0 o 1)
|
||||||
|
// Només vàlid si es_un_jugador() retorna true
|
||||||
|
[[nodiscard]] uint8_t id_unic_jugador() const {
|
||||||
|
if (jugador1_actiu && !jugador2_actiu) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!jugador1_actiu && jugador2_actiu) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0; // Fallback (cal comprovar es_un_jugador() primer)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace GameConfig
|
||||||
70
source/core/system/global_events.cpp
Normal file
70
source/core/system/global_events.cpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// global_events.cpp - Implementació dels events globals
|
||||||
|
// © 2025 Port a C++20
|
||||||
|
|
||||||
|
#include "global_events.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "context_escenes.hpp"
|
||||||
|
#include "core/input/input.hpp"
|
||||||
|
#include "core/input/mouse.hpp"
|
||||||
|
#include "core/rendering/sdl_manager.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) {
|
||||||
|
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
|
||||||
|
auto event_msg = Input::get()->handleEvent(event);
|
||||||
|
if (!event_msg.empty()) {
|
||||||
|
std::cout << "[Input] " << event_msg << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
|
||||||
|
if (event.type == SDL_EVENT_QUIT) {
|
||||||
|
context.canviar_escena(Escena::EIXIR);
|
||||||
|
GestorEscenes::actual = Escena::EIXIR;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Gestió del ratolí (auto-ocultar)
|
||||||
|
Mouse::handleEvent(event);
|
||||||
|
|
||||||
|
// 4. Procesar acciones globales directamente desde eventos SDL
|
||||||
|
// (NO usar Input::checkAction() para evitar desfase de timing)
|
||||||
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||||
|
switch (event.key.scancode) {
|
||||||
|
case SDL_SCANCODE_F1:
|
||||||
|
sdl.decreaseWindowSize();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SDL_SCANCODE_F2:
|
||||||
|
sdl.increaseWindowSize();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SDL_SCANCODE_F3:
|
||||||
|
sdl.toggleFullscreen();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SDL_SCANCODE_F4:
|
||||||
|
sdl.toggleVSync();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SDL_SCANCODE_ESCAPE:
|
||||||
|
context.canviar_escena(Escena::EIXIR);
|
||||||
|
GestorEscenes::actual = Escena::EIXIR;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Tecla no global
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Event no processat
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace GlobalEvents
|
||||||
19
source/core/system/global_events.hpp
Normal file
19
source/core/system/global_events.hpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// 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
43
source/core/types.hpp
Normal 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;
|
||||||
|
};
|
||||||
90
source/core/utils/path_utils.cpp
Normal file
90
source/core/utils/path_utils.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// 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 == nullptr) {
|
||||||
|
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";
|
||||||
|
} // 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
|
||||||
24
source/core/utils/path_utils.hpp
Normal file
24
source/core/utils/path_utils.hpp
Normal 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
|
||||||
4
source/external/.clang-tidy
vendored
Normal file
4
source/external/.clang-tidy
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# source/external/.clang-tidy
|
||||||
|
Checks: '-*'
|
||||||
|
WarningsAsErrors: ''
|
||||||
|
HeaderFilterRegex: ''
|
||||||
5565
source/external/stb_vorbis.h
vendored
Normal file
5565
source/external/stb_vorbis.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user