Merge branch 'rewrite/physics-gpu'
Reescritura completa de la capa de rendering y la arquitectura del juego. Aportaciones principales por fases: - Fase 1: rename masivo a CamelCase/camelBack (clang-tidy) - Fase 2: resolución lógica 1280×720 (16:9) - Fase 3: subsistema de audio importado de AEEA - Fases 5/6: sistema de física vectorial; Ship/Enemy/Bullet sobre RigidBody - Fase 7: migración atómica de SDL_Renderer a SDL3 GPU (Vulkan/Metal) - Fase 8: paleta semántica por entidad y postpro completo (bloom + flicker + background pulse) configurable vía data/config/postfx.yaml - Fase 9: extracción de sistemas de GameScene (collision, continue, init_hud_animator) y descomposición de update en sub-pasos - Plan A: frame loop al Director con interfaz Scene común - Debug overlay (FPS + VSync) toggleable con F11; título de ventana estático
This commit is contained in:
@@ -135,6 +135,46 @@ add_custom_command(
|
||||
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
|
||||
add_dependencies(${PROJECT_NAME} resource_pack)
|
||||
|
||||
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V ---
|
||||
# Compila tots els shaders .glsl a SPIR-V (Vulkan/Linux/Windows).
|
||||
# macOS necessitarà MSL en el futur (Metal) — es generen amb spirv-cross
|
||||
# o glslang amb target distint en una etapa posterior.
|
||||
# Sortida: build/shaders/*.spv
|
||||
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
|
||||
if(GLSLC_EXE)
|
||||
file(GLOB SHADER_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/shaders/*.glsl")
|
||||
set(COMPILED_SHADERS "")
|
||||
foreach(SHADER ${SHADER_SOURCES})
|
||||
get_filename_component(SHADER_NAME ${SHADER} NAME)
|
||||
# Detectar stage del nom: line.vert.glsl → vert, line.frag.glsl → frag
|
||||
if(SHADER_NAME MATCHES "\\.vert\\.glsl$")
|
||||
set(SHADER_STAGE "vert")
|
||||
string(REPLACE ".glsl" ".spv" SPV_NAME ${SHADER_NAME})
|
||||
elseif(SHADER_NAME MATCHES "\\.frag\\.glsl$")
|
||||
set(SHADER_STAGE "frag")
|
||||
string(REPLACE ".glsl" ".spv" SPV_NAME ${SHADER_NAME})
|
||||
else()
|
||||
message(WARNING "Shader sense stage detectat: ${SHADER_NAME} (esperat .vert.glsl o .frag.glsl)")
|
||||
continue()
|
||||
endif()
|
||||
set(SPV_OUTPUT "${CMAKE_BINARY_DIR}/shaders/${SPV_NAME}")
|
||||
add_custom_command(
|
||||
OUTPUT ${SPV_OUTPUT}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/shaders"
|
||||
COMMAND ${GLSLC_EXE} -fshader-stage=${SHADER_STAGE} -O ${SHADER} -o ${SPV_OUTPUT}
|
||||
DEPENDS ${SHADER}
|
||||
COMMENT "Compilant shader ${SHADER_NAME} → ${SPV_NAME}"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND COMPILED_SHADERS ${SPV_OUTPUT})
|
||||
endforeach()
|
||||
add_custom_target(shaders ALL DEPENDS ${COMPILED_SHADERS})
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
message(STATUS "Shaders trobats: ${SHADER_SOURCES}")
|
||||
else()
|
||||
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V")
|
||||
endif()
|
||||
|
||||
# --- STATIC ANALYSIS / FORMAT TARGETS ---
|
||||
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
||||
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Plan de migración Orni Attack → SDL3 GPU + física vectorial
|
||||
|
||||
Documento maestro de la rama `rewrite/physics-gpu`. Resumen del progreso y
|
||||
qué queda. Si pierdes el contexto de la sesión, **léeme primero**.
|
||||
|
||||
## Visión
|
||||
|
||||
Modernizar la base técnica del juego antes de tunear jugabilidad:
|
||||
1. Limpiar legacy (polares, mezcla catalán/inglés, header guards).
|
||||
2. Adoptar formato 16:9 (1280×720).
|
||||
3. Importar subsistemas robustos de AEEA (audio, *input deferido*).
|
||||
4. Implementar física vectorial 2D con rigid bodies + colisiones elásticas.
|
||||
5. **Migrar renderizado a SDL3 GPU sin fallback** ([memoria: project-no-sdl-fallback](./.claude/projects/-home-sergio-gitea-orni-attack/memory/project_no_sdl_fallback.md)).
|
||||
6. Postprocesado y color.
|
||||
|
||||
El tuning de feeling se hace al final, post-GPU.
|
||||
|
||||
## Estado actual
|
||||
|
||||
Rama de trabajo: **`rewrite/physics-gpu`** (pusheada a Gitea).
|
||||
Tag de seguridad: **`beta-3.0`** (snapshot de `main` antes de empezar).
|
||||
|
||||
| Fase | Estado | Commits |
|
||||
|---|---|---|
|
||||
| 0 — Limpieza de código muerto (primitives, polígonos, Bresenham, polares) | ✅ | `6cf990b` |
|
||||
| 1a — `Punt → Vec2` con operadores modernos | ✅ | `cd38101` |
|
||||
| 1b — Renames de entidades + métodos virtuales a camelBack | ✅ | `ae5cc1c` |
|
||||
| 1c — Renames de escenas | ✅ | `5871d29` |
|
||||
| 1d — Renames effects/stage_system/locales | ✅ | `7ee359b` |
|
||||
| 1e — Pragma once + locales restantes + comentarios castellano | ✅ | `bf83f16` |
|
||||
| (fix) — clave YAML `quadrat → cuadrado` post-sweep | ✅ | `56533ca` |
|
||||
| 2 — Cambio a 1280×720 (16:9) | ✅ | `a4f6a55` |
|
||||
| 3 — Import del subsistema de Audio desde AEEA | ✅ | `ed98ef6` |
|
||||
| **4** — Input parcial AEEA | ⏭️ **saltado** (decisión usuario) | — |
|
||||
| 5 — Infraestructura física (`RigidBody`, `PhysicsWorld`) | ✅ | `0fd9360` |
|
||||
| 6a+b — `body_` en Entity + `world_` en GameScene | ✅ | `0574077` |
|
||||
| 6c — Migrar Ship | ✅ | `2fe22ff` |
|
||||
| 6d — Migrar Enemy | ✅ | `27242f5` |
|
||||
| 6e — Migrar Bullet | ✅ | `9993b2d` |
|
||||
| 7a — Infra GPU (shaders + wrappers, runtime dormido) | ✅ | `ba6fd00` |
|
||||
| 7b+c — Swap atómico a SDL3 GPU + line_renderer al pipeline | ✅ | `fa7da4c` |
|
||||
| 7d — Validación visual del usuario en hardware Vulkan | ✅ | — |
|
||||
| 9a — Extraer `Systems::Collision` | ✅ | `896a899` |
|
||||
| 9b — Extraer `Systems::ContinueScreen` | ✅ | `816bc02` |
|
||||
| 9c — Extraer `Systems::InitHud` | ✅ | `a4942fc` |
|
||||
| 9d — Descomponer `GameScene::update` (339→18 LOC) | ✅ | `808abb2` |
|
||||
| **8 — Postprocesado + paleta de colores por entidad** | 🔲 **siguiente** | — |
|
||||
| 10 — Tuning final de masa/restitución/damping | 🔲 | — |
|
||||
| Mac/Metal — shaders MSL en build | 🔲 | — |
|
||||
| 8 — Postprocesado, color, paleta por tipo | 🔲 | — |
|
||||
| 9 — Refactor de GameScene (2.877 LOC → módulos) | 🔲 | — |
|
||||
| 10 — Tuning final de masa/restitución/damping | 🔲 | — |
|
||||
|
||||
## Lo que queda inmediato (Fase 8 — postprocesado + paleta de colores)
|
||||
|
||||
Fase 7 y Fase 9 cerradas. Validación visual SDL3 GPU OK (`Backend GPU: vulkan`).
|
||||
`GameScene.cpp` 1429 → 1015 LOC; `update()` 339 → 18 LOC, complexity baja a
|
||||
< 10 por sub-paso. Tres sistemas extraídos a `source/game/systems/`:
|
||||
- `Systems::Collision` (~210 LOC propios)
|
||||
- `Systems::ContinueScreen` (~160 LOC propios)
|
||||
- `Systems::InitHud` (~155 LOC propios)
|
||||
|
||||
**Fase 8 — siguientes pasos**:
|
||||
1. Color por entidad (paleta semántica): naves blancas, balas verdes,
|
||||
pentagons azules, quadrats rojos, molinillos magenta, debris hereda
|
||||
color del padre. Hoy todo se pinta con el color global del oscilador
|
||||
(`g_current_line_color`). Hay que pasar el color al `pushLine` desde
|
||||
las entidades (cada entity expone un `getColor()`).
|
||||
2. Postprocesado básico:
|
||||
- Bloom/glow: render-to-texture al swapchain real con fragment shader
|
||||
que aplique kernel separable de blur sobre los píxeles iluminados.
|
||||
Requiere un pipeline extra y un par de texturas off-screen.
|
||||
- Opcional: scanlines o leve aberración cromática para feel CRT.
|
||||
|
||||
**Fase 10** (tras 8): tuning de mass/restitution/damping con feedback
|
||||
visual de los colores.
|
||||
|
||||
**Mac/Metal**: cuando vayamos a release de macOS, añadir compilación
|
||||
de los shaders también a MSL (con `spirv-cross` o `glslangValidator`).
|
||||
|
||||
## Memoria del proyecto
|
||||
|
||||
Las preferencias y decisiones persistentes están en:
|
||||
`~/.claude/projects/-home-sergio-gitea-orni-attack/memory/MEMORY.md` y los
|
||||
archivos enlazados desde ahí. Léelos al empezar sesión nueva.
|
||||
|
||||
Resumen:
|
||||
- **No fallback a SDL_Renderer** en Fase 7 — solo GPU o falla.
|
||||
- **Naming**: `.clang-tidy` exige `CamelCase` tipos, `camelBack` métodos,
|
||||
`lower_case_` miembros privados, `UPPER_CASE` constantes.
|
||||
- **Costuras del título tras 16:9** son tuning artístico, no bug, post-Fase 10.
|
||||
- **Push automático** tras cada commit en `rewrite/physics-gpu`.
|
||||
- **Smoke test xvfb** (`timeout 5 xvfb-run -a ./build/orni 2>&1 | head -40`)
|
||||
tras builds importantes para validar arranque.
|
||||
|
||||
## Convenciones técnicas
|
||||
|
||||
- **Lenguaje código**: inglés. **Lenguaje comentarios**: castellano. **Strings
|
||||
de UI**: valenciano (preservados).
|
||||
- **Coordenadas**: cartesianas (`Vec2`). Polares prohibidas (legacy).
|
||||
- **Resolución lógica**: 1280×720. Constantes en `core/defaults.hpp`.
|
||||
- **Audio**: import de AEEA. API: `Audio::get()->playSound/playMusic` con
|
||||
crossfade y efectos.
|
||||
- **Física**: `Physics::RigidBody` en `core/physics/rigid_body.hpp`,
|
||||
`Physics::PhysicsWorld` en `core/physics/physics_world.hpp/.cpp`.
|
||||
Entity tiene `body_` como member. Flujo tri-fase por frame en
|
||||
`GameScene::update`:
|
||||
1. `physics_world_.update(dt)` — integrar y resolver
|
||||
2. `entity.postUpdate(dt)` — sincronizar mirror (center_, angle_)
|
||||
3. Lógica del juego (input, AI, etc.) — aplica fuerzas/cambia velocity
|
||||
|
||||
## Cómo reanudar tras compactación
|
||||
|
||||
1. Lee este archivo.
|
||||
2. Lee `MEMORY.md` (memorias persistentes).
|
||||
3. `git log --oneline -20` para ver últimos commits.
|
||||
4. `git status` para ver si hay trabajo en curso sin commitear.
|
||||
5. Si la fase actual estaba a medias: continúa donde se quedó (este archivo
|
||||
indica en qué fase estamos).
|
||||
6. Tras cada commit, push automático: `git push origin rewrite/physics-gpu`.
|
||||
|
||||
## Configuración física actual (Fase 6e)
|
||||
|
||||
| Entidad | mass | radius (world) | restitution | linear_damping | angular_damping |
|
||||
|---|---|---|---|---|---|
|
||||
| Ship | 10.0 | 12 | 0.6 | 1.5 | 0.0 |
|
||||
| Enemy Pentagon | 5.0 | 20 | 1.0 | 0.0 | 0.0 |
|
||||
| Enemy Quadrat | 8.0 | 20 | 1.0 | 0.0 | 0.0 |
|
||||
| Enemy Molinillo | 4.0 | 20 | 1.0 | 0.0 | 0.0 |
|
||||
| Bullet | 0.5 | **0** (cinemática) | 0 | 0 | 0 |
|
||||
| Wall (PLAYAREA bounds) | ∞ (estático) | — | — | — | — |
|
||||
|
||||
Nota: Bullet usa `radius=0` en el body físico para que no participe en las
|
||||
colisiones del world (ni body-body ni bounds). El radio de gameplay
|
||||
(`Defaults::Entities::BULLET_RADIUS = 3`) lo expone vía `getCollisionRadius()`
|
||||
y lo consumen `check_collision` y la detección de salida del PLAYAREA en
|
||||
`Bullet::update`.
|
||||
|
||||
Las paredes son implícitas: `physics_world_.setBounds(PLAYAREA)` en
|
||||
`GameScene::init`. No son `RigidBody` separados, sino un AABB que rebota.
|
||||
@@ -0,0 +1,34 @@
|
||||
# postfx.yaml - Parámetros del shader de postprocesado
|
||||
#
|
||||
# Este archivo configura el pase final del renderer (bloom + flicker +
|
||||
# background pulse). Se carga al iniciar el juego desde resources.pack.
|
||||
# Si falta o tiene errores, se usan los valores por defecto de
|
||||
# Defaults::PostFx (defaults.hpp).
|
||||
#
|
||||
# Tip de tuning:
|
||||
# - Para más "neón vector", sube bloom.intensity y bloom.radius_px.
|
||||
# - Para más "CRT viejo", sube flicker.amplitude (riesgo de mareo si >0.3).
|
||||
# - Background es muy sutil; pasa los componentes G a 0.15-0.20 para
|
||||
# un fondo verde-tenue más marcado.
|
||||
|
||||
# Bloom / glow: desenfoque gaussiano de las regiones brillantes.
|
||||
bloom:
|
||||
enabled: true
|
||||
intensity: 0.6 # 0..2 — cuanto del bloom se suma a la imagen
|
||||
threshold: 0.30 # 0..1 — luminancia mínima que aporta al bloom
|
||||
radius_px: 2.0 # radio del kernel en píxeles lógicos (1..8 razonable)
|
||||
|
||||
# Flicker: modulación global de brillo (efecto fósforo CRT).
|
||||
# Sustituye a la antigua oscilación CPU del ColorOscillator.
|
||||
flicker:
|
||||
enabled: true
|
||||
amplitude: 0.10 # 0..1 — profundidad del flicker
|
||||
frequency_hz: 6.0 # Hz — velocidad de la pulsación
|
||||
|
||||
# Background pulse: color de fondo oscilante (suma aditiva).
|
||||
# RGB en [0..255]; el shader normaliza a [0..1].
|
||||
background:
|
||||
enabled: true
|
||||
color_min: [0, 5, 0] # negro casi puro
|
||||
color_max: [0, 15, 0] # verde muy tenue
|
||||
pulse_frequency_hz: 6.0 # Hz — sincronizado con flicker por defecto
|
||||
+1
-17
@@ -1,23 +1,7 @@
|
||||
# bullet.shp - Projectil (petit pentàgon)
|
||||
# © 1999 Visente i Sergi (versió Pascal)
|
||||
# © 2025 Port a C++20 amb SDL3
|
||||
# bullet.shp - Projectil (octàgon, radi=3)
|
||||
|
||||
name: bullet
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# Cercle (octàgon regular radi=3)
|
||||
# 8 punts equidistants (45° entre ells) per aproximar un cercle
|
||||
# Començant a angle=-90° (amunt), rotant sentit horari
|
||||
#
|
||||
# Conversió polar→cartesià (radi=3, SDL: Y creix cap avall):
|
||||
# angle=-90°: (0.00, -3.00)
|
||||
# angle=-45°: (2.12, -2.12)
|
||||
# angle=0°: (3.00, 0.00)
|
||||
# angle=45°: (2.12, 2.12)
|
||||
# angle=90°: (0.00, 3.00)
|
||||
# angle=135°: (-2.12, 2.12)
|
||||
# angle=180°: (-3.00, 0.00)
|
||||
# angle=225°: (-2.12, -2.12)
|
||||
|
||||
polyline: 0,-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
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
# enemy_pentagon.shp - ORNI enemic (pentàgon regular)
|
||||
# © 1999 Visente i Sergi (versió Pascal)
|
||||
# © 2025 Port a C++20 amb SDL3
|
||||
# enemy_pentagon.shp - ORNI enemic (pentàgon regular, radi=20)
|
||||
|
||||
name: enemy_pentagon
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# Pentàgon regular radi=20
|
||||
# 5 punts equidistants al voltant d'un cercle (72° entre ells)
|
||||
# Començant a angle=-90° (amunt), rotant sentit antihorari
|
||||
#
|
||||
# Angles: -90°, -18°, 54°, 126°, 198°
|
||||
# Conversió polar→cartesià (SDL: Y creix cap avall):
|
||||
# angle=-90°: (0.00, -20.00)
|
||||
# angle=-18°: (19.02, -6.18)
|
||||
# angle=54°: (11.76, 16.18)
|
||||
# angle=126°: (-11.76, 16.18)
|
||||
# angle=198°: (-19.02, -6.18)
|
||||
|
||||
polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
# enemy_square.shp - ORNI enemic (quadrat regular)
|
||||
# © 2025 Port a C++20 amb SDL3
|
||||
# enemy_square.shp - ORNI enemic (quadrat regular, radi=20)
|
||||
|
||||
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
|
||||
|
||||
+2
-18
@@ -1,24 +1,8 @@
|
||||
# ship.shp - Nau del jugador 1 (triangle amb base còncava - punta de fletxa)
|
||||
# © 1999 Visente i Sergi (versió Pascal)
|
||||
# © 2025 Port a C++20 amb SDL3
|
||||
# ship.shp - Nau del jugador 1
|
||||
# Triangle amb base còncava (punta de fletxa)
|
||||
|
||||
name: ship
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# Triangle amb base còncava tipus "punta de fletxa"
|
||||
# Punts originals (polar):
|
||||
# p1: r=12, angle=270° (3π/2) → punta amunt
|
||||
# p2: r=12, angle=45° (π/4) → base dreta-darrere
|
||||
# p3: r=12, angle=135° (3π/4) → base esquerra-darrere
|
||||
#
|
||||
# MODIFICACIÓ: afegit p4 al mig de la base, desplaçat cap al centre
|
||||
# p4: (0, 4) → punt central de la base, cap endins
|
||||
#
|
||||
# Conversió polar→cartesià (angle-90° perquè origen visual és amunt):
|
||||
# p1: (0, -12) → punta
|
||||
# p2: (8.49, 8.49) → base dreta
|
||||
# p4: (0, 4) → base centre (cap endins)
|
||||
# p3: (-8.49, 8.49) → base esquerra
|
||||
|
||||
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||
|
||||
+3
-22
@@ -1,30 +1,11 @@
|
||||
# ship2.shp - Nau del jugador 2 (triangle amb circulito central)
|
||||
# © 1999 Visente i Sergi (versió Pascal)
|
||||
# © 2025 Port a C++20 amb SDL3
|
||||
# ship2.shp - Nau del jugador 2
|
||||
# Triangle amb cercle central (distintiu visual)
|
||||
|
||||
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
|
||||
# Octàgon central (radi=2.5)
|
||||
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
|
||||
|
||||
+10
-10
@@ -16,7 +16,7 @@ stages:
|
||||
spawn_interval: 3.0
|
||||
enemy_distribution:
|
||||
pentagon: 100
|
||||
quadrat: 0
|
||||
cuadrado: 0
|
||||
molinillo: 0
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 0.7
|
||||
@@ -32,7 +32,7 @@ stages:
|
||||
spawn_interval: 2.5
|
||||
enemy_distribution:
|
||||
pentagon: 70
|
||||
quadrat: 30
|
||||
cuadrado: 30
|
||||
molinillo: 0
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 0.85
|
||||
@@ -48,7 +48,7 @@ stages:
|
||||
spawn_interval: 2.0
|
||||
enemy_distribution:
|
||||
pentagon: 50
|
||||
quadrat: 30
|
||||
cuadrado: 30
|
||||
molinillo: 20
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.0
|
||||
@@ -64,7 +64,7 @@ stages:
|
||||
spawn_interval: 1.8
|
||||
enemy_distribution:
|
||||
pentagon: 40
|
||||
quadrat: 35
|
||||
cuadrado: 35
|
||||
molinillo: 25
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.1
|
||||
@@ -80,7 +80,7 @@ stages:
|
||||
spawn_interval: 1.5
|
||||
enemy_distribution:
|
||||
pentagon: 35
|
||||
quadrat: 35
|
||||
cuadrado: 35
|
||||
molinillo: 30
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.2
|
||||
@@ -96,7 +96,7 @@ stages:
|
||||
spawn_interval: 1.3
|
||||
enemy_distribution:
|
||||
pentagon: 30
|
||||
quadrat: 30
|
||||
cuadrado: 30
|
||||
molinillo: 40
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.3
|
||||
@@ -112,7 +112,7 @@ stages:
|
||||
spawn_interval: 1.0
|
||||
enemy_distribution:
|
||||
pentagon: 25
|
||||
quadrat: 30
|
||||
cuadrado: 30
|
||||
molinillo: 45
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.4
|
||||
@@ -128,7 +128,7 @@ stages:
|
||||
spawn_interval: 0.8
|
||||
enemy_distribution:
|
||||
pentagon: 20
|
||||
quadrat: 30
|
||||
cuadrado: 30
|
||||
molinillo: 50
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.5
|
||||
@@ -144,7 +144,7 @@ stages:
|
||||
spawn_interval: 0.6
|
||||
enemy_distribution:
|
||||
pentagon: 15
|
||||
quadrat: 25
|
||||
cuadrado: 25
|
||||
molinillo: 60
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.6
|
||||
@@ -160,7 +160,7 @@ stages:
|
||||
spawn_interval: 0.5
|
||||
enemy_distribution:
|
||||
pentagon: 10
|
||||
quadrat: 20
|
||||
cuadrado: 20
|
||||
molinillo: 70
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.8
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 450
|
||||
|
||||
// Fragment shader para líneas vectoriales.
|
||||
// Pinta el color interpolado per-vertex que viene del vertex shader.
|
||||
// (Postprocesado / bloom / glow son responsabilidad de la Fase 8.)
|
||||
|
||||
layout(location = 0) in vec4 frag_color;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
out_color = frag_color;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#version 450
|
||||
|
||||
// Vertex shader para líneas vectoriales.
|
||||
// Las líneas se proveen ya extrudidas en CPU como quads (2 triángulos por línea)
|
||||
// con grosor configurable. El vertex shader solo:
|
||||
// 1. Transforma de píxeles lógicos (0..viewport_size) a clip-space (-1..+1).
|
||||
// 2. Pasa el color RGBA al fragment shader.
|
||||
//
|
||||
// Slot de uniform buffer 0 (vertex): viewport size para la transformación.
|
||||
// Convención SDL_gpu: SDL_PushGPUVertexUniformData(cmd, 0, &ubo, sizeof(ubo)).
|
||||
|
||||
layout(set = 1, binding = 0) uniform UBO {
|
||||
vec2 viewport_size; // ancho y alto en píxeles lógicos (ej. 1280, 720)
|
||||
vec2 _padding; // alineamiento a 16 bytes
|
||||
} ubo;
|
||||
|
||||
layout(location = 0) in vec2 in_position; // píxeles lógicos
|
||||
layout(location = 1) in vec4 in_color; // RGBA 0..1
|
||||
|
||||
layout(location = 0) out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
// Píxeles lógicos -> NDC (-1..+1)
|
||||
vec2 ndc = (in_position / ubo.viewport_size) * 2.0 - 1.0;
|
||||
// Y flip: SDL screen-Y va hacia abajo, clip-Y hacia arriba.
|
||||
ndc.y = -ndc.y;
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
frag_color = in_color;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#version 450
|
||||
|
||||
// Fragment shader del pase de postprocesado.
|
||||
// Lee la textura offscreen (escena vectorial sobre fondo negro) y produce
|
||||
// el fragmento final aplicando:
|
||||
// 1. Bloom kernel 5×5 con high-pass (solo los brillos por encima de
|
||||
// threshold contribuyen).
|
||||
// 2. Flicker: multiplicador global de brillo modulado por tiempo
|
||||
// (sustituye al oscilador CPU del legacy).
|
||||
// 3. Background pulse: color de fondo que oscila entre min y max y se
|
||||
// suma a la imagen (las líneas brillan por encima).
|
||||
//
|
||||
// Resource sets (SDL_gpu):
|
||||
// set=2, binding=0 → sampler2D (escena offscreen)
|
||||
// set=3, binding=0 → uniform buffer (parámetros del postpro)
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||
|
||||
layout(set = 3, binding = 0) uniform PostFxUBO {
|
||||
float time;
|
||||
float bloom_intensity;
|
||||
float bloom_threshold;
|
||||
float bloom_radius_px;
|
||||
|
||||
float flicker_amplitude;
|
||||
float flicker_frequency_hz;
|
||||
float background_pulse_freq_hz;
|
||||
float _pad_a;
|
||||
|
||||
vec4 background_min; // RGB en [0..1], A=1
|
||||
vec4 background_max; // RGB en [0..1], A=1
|
||||
|
||||
vec2 texel_size; // 1.0 / texture_size
|
||||
vec2 _pad_b;
|
||||
} ubo;
|
||||
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 frag;
|
||||
|
||||
const float TAU = 6.28318530718;
|
||||
|
||||
void main() {
|
||||
// === BLOOM ===
|
||||
// Kernel 5×5 con muestreo radial y high-pass por luminancia (max RGB).
|
||||
// Pesos gaussianos: w = exp(-(dx²+dy²) / 4).
|
||||
vec3 src = texture(scene, v_uv).rgb;
|
||||
vec3 bloom = vec3(0.0);
|
||||
float total_weight = 0.0;
|
||||
for (int dy = -2; dy <= 2; ++dy) {
|
||||
for (int dx = -2; dx <= 2; ++dx) {
|
||||
vec2 offset = vec2(float(dx), float(dy)) * ubo.texel_size * ubo.bloom_radius_px;
|
||||
vec3 c = texture(scene, v_uv + offset).rgb;
|
||||
float luma = max(c.r, max(c.g, c.b));
|
||||
float high_pass = max(0.0, luma - ubo.bloom_threshold);
|
||||
float w = exp(-(float(dx * dx + dy * dy)) / 4.0);
|
||||
bloom += c * high_pass * w;
|
||||
total_weight += w;
|
||||
}
|
||||
}
|
||||
if (total_weight > 0.0) {
|
||||
bloom /= total_weight;
|
||||
}
|
||||
bloom *= ubo.bloom_intensity;
|
||||
|
||||
// === FLICKER ===
|
||||
// Multiplicador global de brillo. Oscila entre (1.0 - amplitude) y 1.0.
|
||||
// amplitude=0 → sin flicker; amplitude=1 → pulsa entre apagado y máximo.
|
||||
float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
|
||||
float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse));
|
||||
|
||||
// === BACKGROUND PULSE ===
|
||||
// Suma de color de fondo oscilante. min..max se interpolan con sin(t).
|
||||
float bg_pulse = (sin(ubo.time * ubo.background_pulse_freq_hz * TAU) * 0.5) + 0.5;
|
||||
vec3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
|
||||
|
||||
// === COMPOSICIÓN ===
|
||||
// El offscreen viene con clear=black, por lo que solo las líneas y el
|
||||
// bloom aportan luz. Sumamos el fondo y luego multiplicamos por flicker
|
||||
// para que el pulso afecte a todo (líneas + bloom + bg).
|
||||
vec3 lines_and_glow = (src + bloom) * flicker;
|
||||
frag = vec4(background + lines_and_glow, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#version 450
|
||||
|
||||
// Vertex shader del pase de postprocesado.
|
||||
// Emite un solo triángulo que cubre toda la pantalla (técnica del "fullscreen
|
||||
// triangle"): tres vértices en (-1,-1), (3,-1), (-1,3) → la región visible
|
||||
// [-1..1]² queda completamente cubierta y el clip recorta el resto. No hace
|
||||
// falta vertex buffer; el draw es DrawPrimitives(vertex_count=3).
|
||||
|
||||
layout(location = 0) out vec2 v_uv;
|
||||
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](
|
||||
vec2(-1.0, -1.0),
|
||||
vec2( 3.0, -1.0),
|
||||
vec2(-1.0, 3.0)
|
||||
);
|
||||
// UV.y invertida para compensar la diferencia entre la convención de
|
||||
// clip-space del line shader (ndc.y flipeado, GL-style) y la convención
|
||||
// de muestreo de SDL_gpu/Vulkan (origen de textura en top-left). Sin esta
|
||||
// inversión, el offscreen se ve cabeza-abajo en el composite.
|
||||
vec2 uvs[3] = vec2[3](
|
||||
vec2(0.0, 1.0),
|
||||
vec2(2.0, 1.0),
|
||||
vec2(0.0, -1.0)
|
||||
);
|
||||
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||
v_uv = uvs[gl_VertexIndex];
|
||||
}
|
||||
+217
-109
@@ -1,183 +1,291 @@
|
||||
#include "audio.hpp"
|
||||
#include "core/audio/audio.hpp"
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
|
||||
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
|
||||
|
||||
#include <algorithm> // Para clamp
|
||||
#include <iostream> // Para std::cout
|
||||
#include <cstdio> // Para std::fprintf
|
||||
|
||||
// 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_adapter.hpp" // Para AudioResource::getMusic/getSound
|
||||
#include "core/audio/jail_audio.hpp" // Para Ja::* (motor jailgames)
|
||||
#include "core/audio/sound_effects_config.hpp" // Para SoundEffectsConfig
|
||||
#include "core/defaults.hpp" // Para Defaults::Audio::FREQUENCY
|
||||
|
||||
#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
|
||||
// Invariant compile-time: tots los valors d'Audio::Group han de cabre als slots
|
||||
// de volum per grup que manté l'engine. Si s'afegeix una nueva entrada a Group
|
||||
// y no s'incrementa Ja::MAX_GROUPS, este assert falla antes de compilar.
|
||||
static_assert(static_cast<int>(Audio::Group::INTERFACE) < Ja::MAX_GROUPS,
|
||||
"Audio::Group té més entrades que slots té Ja::MAX_GROUPS");
|
||||
|
||||
// Singleton
|
||||
Audio* Audio::instance = nullptr;
|
||||
std::unique_ptr<Audio> Audio::instance;
|
||||
|
||||
// Inicializa la instancia única del singleton
|
||||
void Audio::init() { Audio::instance = new Audio(); }
|
||||
// Inicialitza la instància única del singleton con la configuración rebuda
|
||||
void Audio::init(const Config& config) { Audio::instance = std::unique_ptr<Audio>(new Audio(config)); }
|
||||
|
||||
// Libera la instancia
|
||||
void Audio::destroy() { delete Audio::instance; }
|
||||
// Allibera la instància
|
||||
void Audio::destroy() { Audio::instance.reset(); }
|
||||
|
||||
// Obtiene la instancia
|
||||
auto Audio::get() -> Audio* { return Audio::instance; }
|
||||
// Obté la instància
|
||||
auto Audio::get() -> Audio* { return Audio::instance.get(); }
|
||||
|
||||
// Constructor
|
||||
Audio::Audio() { initSDLAudio(); }
|
||||
Audio::Audio(const Config& config)
|
||||
: config_(config) { initSDLAudio(); }
|
||||
|
||||
// Destructor
|
||||
Audio::~Audio() {
|
||||
JA_Quit();
|
||||
}
|
||||
// Destructor: engine_ es std::unique_ptr, el seu dtor tanca el device SDL i
|
||||
// desregistra Ja::Engine::active_. Cap crida explícita necessària.
|
||||
Audio::~Audio() = default;
|
||||
|
||||
// Método principal
|
||||
// Método principal: l'estat de la música el manté el motor (única font de
|
||||
// veritat), per tant no cal sin sincronització aquí.
|
||||
void Audio::update() {
|
||||
JA_Update();
|
||||
if (instance && instance->engine_) { instance->engine_->update(); }
|
||||
}
|
||||
|
||||
// Reproduce la música
|
||||
void Audio::playMusic(const std::string& name, const int loop) {
|
||||
bool new_loop = (loop != 0);
|
||||
// Reprodueix la música per nom (amb crossfade opcional)
|
||||
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
|
||||
const 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) {
|
||||
// Si ya sona exactament la misma pista i mismo mode loop, no fem res
|
||||
if (getMusicState() == 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;
|
||||
}
|
||||
if (!music_enabled_) { 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
|
||||
}
|
||||
auto* resource = AudioResource::getMusic(name);
|
||||
if (resource == nullptr) { return; }
|
||||
|
||||
// Llamada al motor para reproducir la nueva pista
|
||||
JA_PlayMusic(resource, loop);
|
||||
|
||||
// Actualizar estado y metadatos después de iniciar con éxito
|
||||
playMusicInternal(resource, loop, crossfade_ms);
|
||||
music_.name = name;
|
||||
music_.loop = new_loop;
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
|
||||
// Pausa la música
|
||||
// Reprodueix la música per punter (amb crossfade opcional)
|
||||
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||
if (!music_enabled_ || music == nullptr) { return; }
|
||||
|
||||
playMusicInternal(music, loop, crossfade_ms);
|
||||
// Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el
|
||||
// recuperem porque getCurrentMusicName() no menteixi. Si no, music_.name
|
||||
// queda buit — el contracte d'este overload no garanteix el nom.
|
||||
music_.name = music->filename;
|
||||
}
|
||||
|
||||
// Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i
|
||||
// actualitza el loop cachejat. Els callers s'encarreguen del gating
|
||||
// (music_enabled_, nullptr, same-track early return) y del nom. L'estat el
|
||||
// manté Ja (Ja::playMusic posa PLAYING al Ja::Music* corresponent).
|
||||
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
|
||||
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
|
||||
engine_->crossfadeMusic(music, crossfade_ms, loop);
|
||||
} else {
|
||||
if (CURRENTLY_PLAYING) {
|
||||
engine_->stopMusic();
|
||||
}
|
||||
engine_->playMusic(music, loop);
|
||||
}
|
||||
|
||||
music_.loop = (loop != 0);
|
||||
}
|
||||
|
||||
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
|
||||
void Audio::pauseMusic() {
|
||||
if (music_enabled_ && music_.state == MusicState::PLAYING) {
|
||||
JA_PauseMusic();
|
||||
music_.state = MusicState::PAUSED;
|
||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||
engine_->pauseMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Continua la música pausada
|
||||
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
||||
void Audio::resumeMusic() {
|
||||
if (music_enabled_ && music_.state == MusicState::PAUSED) {
|
||||
JA_ResumeMusic();
|
||||
music_.state = MusicState::PLAYING;
|
||||
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
||||
engine_->resumeMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Detiene la música
|
||||
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
||||
void Audio::stopMusic() {
|
||||
if (music_enabled_) {
|
||||
JA_StopMusic();
|
||||
music_.state = MusicState::STOPPED;
|
||||
engine_->stopMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por nombre
|
||||
void Audio::playSound(const std::string& name, Group group) const {
|
||||
void Audio::setMusicSpeed(float ratio) {
|
||||
if (music_enabled_) {
|
||||
engine_->setMusicSpeed(ratio);
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so per nom
|
||||
void Audio::playSound(const std::string& name, Group group) {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(AudioCache::getSound(name), 0, static_cast<int>(group));
|
||||
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por puntero directo
|
||||
void Audio::playSound(JA_Sound_t* sound, Group group) const {
|
||||
// Reprodueix un so per punter directe
|
||||
void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
if (sound_enabled_ && sound != nullptr) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Variant con velocitat (i to) escalats. Apliquem el ratio al canal
|
||||
// just retornat per `playSound`: así el `SDL_AudioStream` recent creat
|
||||
// processa tot el sample con el ratio des del primer pull del callback.
|
||||
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
||||
// la crida al ratio — sin efectes col·laterals.
|
||||
void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
if (CH >= 0 && speed != 1.0F) {
|
||||
engine_->setChannelSpeed(CH, speed);
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
|
||||
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
|
||||
// sec — l'usuari sent el so aún que la cua no s'apliqui.
|
||||
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
const auto* params = SoundEffectsConfig::get().findEcho(preset_name);
|
||||
if (params == nullptr) {
|
||||
std::fprintf(stderr, "Audio: preset d'eco '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine_->playSoundWithEcho(sound, *params, static_cast<int>(group)) < 0) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
||||
// fallback que playSoundWithEcho.
|
||||
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
const auto* params = SoundEffectsConfig::get().findReverb(preset_name);
|
||||
if (params == nullptr) {
|
||||
std::fprintf(stderr, "Audio: preset de reverb '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine_->playSoundWithReverb(sound, *params, static_cast<int>(group)) < 0) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Atura tots los sons
|
||||
void Audio::stopAllSounds() {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(sound, 0, static_cast<int>(group));
|
||||
engine_->stopChannel(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Detiene todos los sonidos
|
||||
void Audio::stopAllSounds() const {
|
||||
if (sound_enabled_) {
|
||||
JA_StopChannel(-1);
|
||||
// Fa una fosa de sortida de la música
|
||||
void Audio::fadeOutMusic(int milliseconds) {
|
||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||
engine_->fadeOutMusic(milliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
// Realiza un fundido de salida de la música
|
||||
void Audio::fadeOutMusic(int milliseconds) const {
|
||||
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
|
||||
JA_FadeOutMusic(milliseconds);
|
||||
}
|
||||
// Registra un callback que el motor dispararà cuando la pista actual acabi de
|
||||
// drenar (times == 0 + stream buit). S'executa al mismo thread que
|
||||
// Audio::update (render loop); los consumidors no poden fer I/O blocant.
|
||||
void Audio::setOnMusicEnded(std::function<void()> callback) {
|
||||
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
|
||||
}
|
||||
|
||||
// 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:
|
||||
// Resol el nom contra el cache de recursos i retorna la duración pre-calculada
|
||||
// al `loadMusic`. 0 si la pista no existeix — así el caller pot decidir
|
||||
// fallback (p. ex. usar un timeout fix) sin haver de propagar errors.
|
||||
auto Audio::getMusicDurationMs(const std::string& name) -> int {
|
||||
auto* music = AudioResource::getMusic(name);
|
||||
return (music != nullptr) ? music->duration_ms : 0;
|
||||
}
|
||||
|
||||
// Consulta directament l'estat a Ja y el projecta al subconjunt d'estats que
|
||||
// exposa Audio (INVALID/DISABLED de Ja col·lapsen a STOPPED — la capa d'usuari
|
||||
// solo vol saber si está sonant, pausat o parat).
|
||||
auto Audio::getMusicState() -> MusicState {
|
||||
if (!instance || !instance->engine_) { return MusicState::STOPPED; }
|
||||
switch (instance->engine_->getMusicState()) {
|
||||
case Ja::MusicState::PLAYING:
|
||||
return MusicState::PLAYING;
|
||||
case JA_MUSIC_PAUSED:
|
||||
case Ja::MusicState::PAUSED:
|
||||
return MusicState::PAUSED;
|
||||
case JA_MUSIC_STOPPED:
|
||||
case JA_MUSIC_INVALID:
|
||||
case JA_MUSIC_DISABLED:
|
||||
case Ja::MusicState::STOPPED:
|
||||
case Ja::MusicState::INVALID:
|
||||
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));
|
||||
}
|
||||
// Aplica el gate master (enabled_) + el gate del canal (sound/music_enabled_)
|
||||
// i retorna el volum escalat pel master config_.volume. 0 si algun gate está
|
||||
// tancat. Así los dos setters comparteixen la misma política.
|
||||
auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
|
||||
volume = std::clamp(volume, MIN_VOLUME, MAX_VOLUME);
|
||||
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// Estableix el volum dels sons (float 0.0..1.0)
|
||||
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
||||
}
|
||||
|
||||
// Aplica la configuración
|
||||
void Audio::applySettings() {
|
||||
enable(Options::audio.enabled);
|
||||
// Estableix el volum de la música (float 0.0..1.0)
|
||||
void Audio::setMusicVolume(float music_volume) {
|
||||
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
||||
}
|
||||
|
||||
// Establecer estado general
|
||||
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
|
||||
void Audio::applySettings(const Config& config) {
|
||||
config_ = config;
|
||||
sound_enabled_ = config_.sound_enabled;
|
||||
music_enabled_ = config_.music_enabled;
|
||||
enable(config_.enabled);
|
||||
}
|
||||
|
||||
// Estableix l'estat general
|
||||
void Audio::enable(bool value) {
|
||||
enabled_ = value;
|
||||
|
||||
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
|
||||
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
|
||||
setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME);
|
||||
setMusicVolume(enabled_ ? config_.music_volume : MIN_VOLUME);
|
||||
}
|
||||
|
||||
// Inicializa SDL Audio
|
||||
// Estableix l'estat dels sons i reaplica el volum porque los canals actius
|
||||
// responguin a l'instant (evita que el toggle solo surti efecte al pròxim
|
||||
// setSoundVolume explícit).
|
||||
void Audio::enableSound(bool value) {
|
||||
sound_enabled_ = value;
|
||||
setSoundVolume(config_.sound_volume);
|
||||
}
|
||||
|
||||
// Estableix l'estat de la música i reaplica el volum per la misma raó.
|
||||
void Audio::enableMusic(bool value) {
|
||||
music_enabled_ = value;
|
||||
setMusicVolume(config_.music_volume);
|
||||
}
|
||||
|
||||
// Inicialitza SDL Audio y el motor Ja::Engine owned.
|
||||
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";
|
||||
std::fprintf(stderr, "Audio: SDL_AUDIO could not initialize! SDL Error: %s\n", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
engine_ = std::make_unique<Ja::Engine>(Defaults::Audio::FREQUENCY, Defaults::Audio::FORMAT, Defaults::Audio::CHANNELS);
|
||||
sound_enabled_ = config_.sound_enabled;
|
||||
music_enabled_ = config_.music_enabled;
|
||||
enable(config_.enabled);
|
||||
}
|
||||
+124
-58
@@ -1,97 +1,163 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath> // Para std::lround
|
||||
#include <cstdint> // Para int8_t, uint8_t
|
||||
#include <functional> // Para std::function
|
||||
#include <memory> // Para std::unique_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
// --- Clase Audio: gestor de audio (singleton) ---
|
||||
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
|
||||
// tres símbols (Music/Sound para el punter que exposa la API i Engine per al
|
||||
// std::unique_ptr<Engine> membre) s'usen solo per punter al header, así que
|
||||
// el forward-decl basta. El ~Audio() en .cpp veu la definició completa i
|
||||
// instancia correctament el dtor de l'unique_ptr.
|
||||
namespace Ja {
|
||||
class Engine;
|
||||
struct Music;
|
||||
struct Sound;
|
||||
} // namespace Ja
|
||||
|
||||
// --- Clase Audio: gestor d'àudio (singleton) ---
|
||||
// Port del subsistema d'àudio del projecte ../aee, desacoblat d'Options:
|
||||
// la configuración entra per la struct Audio::Config a init()/applySettings(),
|
||||
// en lloc de llegir directament Options::audio. Això deixa audio.cpp independent
|
||||
// del layout d'Options i permet substituir la font de configuración.
|
||||
//
|
||||
// Els volums es manegen internament como a float 0.0–1.0; la capa de
|
||||
// presentació (menús, notificacions) usa las helpers toPercent/fromPercent
|
||||
// per mostrar 0–100 a l'usuari.
|
||||
class Audio {
|
||||
public:
|
||||
// --- Configuración injectada (Options la construeix via buildAudioConfig) ---
|
||||
struct Config {
|
||||
bool enabled{true};
|
||||
float volume{1.0F}; // Master 0..1
|
||||
bool music_enabled{true};
|
||||
float music_volume{0.8F};
|
||||
bool sound_enabled{true};
|
||||
float sound_volume{1.0F};
|
||||
};
|
||||
|
||||
// --- Enums ---
|
||||
enum class Group : int {
|
||||
ALL = -1, // Todos los grupos
|
||||
GAME = 0, // Sonidos del juego
|
||||
INTERFACE = 1 // Sonidos de la interfaz
|
||||
enum class Group : std::int8_t {
|
||||
ALL = -1, // Tots los grups
|
||||
GAME = 0, // Sons del joc
|
||||
INTERFACE = 1 // Sons de la interfície
|
||||
};
|
||||
|
||||
enum class MusicState {
|
||||
PLAYING, // Reproduciendo música
|
||||
enum class MusicState : std::uint8_t {
|
||||
PLAYING, // Reproduint música
|
||||
PAUSED, // Música pausada
|
||||
STOPPED, // Música detenida
|
||||
STOPPED, // Música aturada
|
||||
};
|
||||
|
||||
// --- 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
|
||||
// --- Constants ---
|
||||
static constexpr float MAX_VOLUME = 1.0F; // Volum màxim (float 0..1)
|
||||
static constexpr float MIN_VOLUME = 0.0F; // Volum mínim (float 0..1)
|
||||
|
||||
// --- 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 init(const Config& config); // Inicialitza con la configuración rebuda
|
||||
static void destroy(); // Allibera l'objecte Audio
|
||||
static auto get() -> Audio*; // Obté el punter a l'objecte Audio
|
||||
~Audio(); // Destructor (públic para std::unique_ptr)
|
||||
Audio(const Audio&) = delete; // Evitar còpia
|
||||
Audio(Audio&&) = delete;
|
||||
auto operator=(const Audio&) -> Audio& = delete; // Evitar assignació
|
||||
auto operator=(Audio&&) -> Audio& = delete;
|
||||
|
||||
static void update(); // Actualización del sistema de audio
|
||||
static void update(); // Actualització del sistema d'àudio
|
||||
|
||||
// --- 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 playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproduir música per nom (amb crossfade opcional)
|
||||
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproduir música per punter (amb crossfade opcional)
|
||||
void pauseMusic(); // Pausar la reproducció 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
|
||||
void stopMusic(); // Aturar completament la música
|
||||
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
|
||||
void setOnMusicEnded(std::function<void()> callback); // Callback disparat cuando la pista actual acaba de drenar (CONV-03)
|
||||
// Multiplicador de velocitat de la música actual. 1.0 = normal,
|
||||
// 1.5 = un 50% més ràpid (efecte "chipmunk" — también puja el to).
|
||||
// Es reseteja a 1.0 implícitament a cada `playMusic`. No-op si no
|
||||
// hay música activa.
|
||||
void setMusicSpeed(float ratio);
|
||||
|
||||
// --- 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 sons ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
||||
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
||||
// Reprodueix un so con la velocitat (i to) escalats per `speed`:
|
||||
// 1.0 = normal, 0.95 ≈ -5% (més greu i lent), 1.05 ≈ +5% (més
|
||||
// agut i ràpid). Mateixa semàntica que `setMusicSpeed`. Útil per a
|
||||
// variacions subtils que eviten la fatiga d'escoltar el mismo
|
||||
// sample idèntic (p.ex. obertures de sarcòfag, picks d'ítems).
|
||||
void playSound(const std::string& name, Group group, float speed);
|
||||
// Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
|
||||
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||
// o el motor está al sin de canals con efecte, fa fallback a playSound
|
||||
// sec — l'usuari sent el so igualment, sin la cua.
|
||||
void playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||
void playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||
void stopAllSounds(); // Aturar tots los sons (muta globals de Ja)
|
||||
|
||||
// --- 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
|
||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
||||
void setMusicVolume(float volume); // Ajusta el volum de la música
|
||||
|
||||
// --- Helpers de conversió para la capa de presentació ---
|
||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||
// No són constexpr porque std::lround no ho es en C++20; s'usen en runtime.
|
||||
static auto toPercent(float volume) -> int {
|
||||
return static_cast<int>(std::lround(volume * 100.0F));
|
||||
}
|
||||
static auto fromPercent(int percent) -> float {
|
||||
return static_cast<float>(percent) / 100.0F;
|
||||
}
|
||||
|
||||
// --- Configuración general ---
|
||||
void enable(bool value); // Establecer estado general
|
||||
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
|
||||
void applySettings(); // Aplica la configuración
|
||||
void enable(bool value); // Estableix l'estat general (reaplica volums)
|
||||
void toggleEnabled() { enable(!enabled_); } // Alterna l'estat general (reaplica volums)
|
||||
void applySettings(const Config& config); // Aplica una nueva 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 sons ---
|
||||
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
|
||||
void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (reaplica volum)
|
||||
|
||||
// --- 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
|
||||
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
|
||||
void toggleMusic() { enableMusic(!music_enabled_); } // Alterna l'estat de la música (reaplica volum)
|
||||
|
||||
// --- Consultas de estado ---
|
||||
// --- Consultes d'estat ---
|
||||
[[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]] static auto getMusicState() -> MusicState; // Estat real consultat a Ja::
|
||||
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
||||
// Duración de la pista resolta per nom (mil·lisegons). 0 si la pista no
|
||||
// existeix al cache de recursos o si el seu header OGG no permet
|
||||
// calcular-la. Pensat para clients que necessiten un timeline
|
||||
// determinista (p. ex. RoomFsm) sin dependre de callbacks de fi.
|
||||
[[nodiscard]] static auto getMusicDurationMs(const std::string& name) -> int;
|
||||
|
||||
private:
|
||||
// --- Tipos anidados ---
|
||||
// --- Tipus anidats ---
|
||||
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
|
||||
std::string name; // Última pista de música reproduïda (buida si es va passar per punter sin filename)
|
||||
bool loop{false}; // Si el play actual es en bucle
|
||||
};
|
||||
|
||||
// --- Métodos ---
|
||||
Audio(); // Constructor privado
|
||||
~Audio(); // Destructor privado
|
||||
void initSDLAudio(); // Inicializa SDL Audio
|
||||
// --- Mètodes ---
|
||||
explicit Audio(const Config& config); // Constructor privat: rep la config
|
||||
void initSDLAudio(); // Inicialitza SDL Audio
|
||||
void playMusicInternal(Ja::Music* music, int loop, int crossfade_ms); // Camí comú dels dos overloads de playMusic
|
||||
[[nodiscard]] auto effectiveVolume(float volume, bool channel_enabled) const -> float; // Gate master+channel: 0 si algun está off, clamp 0..1 altrament
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Audio* instance; // Instancia única de Audio
|
||||
// --- Variables membre ---
|
||||
static std::unique_ptr<Audio> instance; // Instància única d'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
|
||||
std::unique_ptr<Ja::Engine> engine_; // Motor de baix nivell (owned); viu mentre Audio viu.
|
||||
Config config_{}; // Configuración injectada (volums, enables)
|
||||
Music music_; // Estat de la música (nom + loop cachejats)
|
||||
bool enabled_{true}; // Estat general de l'àudio
|
||||
bool sound_enabled_{true}; // Estat dels efectes de so
|
||||
bool music_enabled_{true}; // Estat de la música
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
// audio_adapter.cpp - Implementación de AudioResource para orni_attack
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Implementa AudioResource::getMusic / getSound delegando a
|
||||
// Resource::Helper::loadFile (que abstrae el resources.pack y el fallback
|
||||
// a filesystem). Cache local de Ja::Music* / Ja::Sound* con lazy load:
|
||||
// cada recurso se carga la primera vez que se pide y se mantiene vivo
|
||||
// hasta el shutdown.
|
||||
|
||||
#include "core/audio/audio_adapter.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/audio/jail_audio.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Cachés locales: indexados por nombre lógico ("title.ogg", "effects/laser_shoot.wav", etc.)
|
||||
// Mantienen ownership con unique_ptr; se liberan al salir del programa.
|
||||
std::unordered_map<std::string, std::unique_ptr<Ja::Music>>& musicCache() {
|
||||
static std::unordered_map<std::string, std::unique_ptr<Ja::Music>> cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::unique_ptr<Ja::Sound>>& soundCache() {
|
||||
static std::unordered_map<std::string, std::unique_ptr<Ja::Sound>> cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
// Normaliza el nombre añadiendo la subcarpeta correspondiente si no la trae:
|
||||
// "title.ogg" -> "music/title.ogg"
|
||||
// "music/title.ogg" -> "music/title.ogg"
|
||||
// "effects/laser.wav" -> "sounds/effects/laser.wav"
|
||||
std::string normalizeMusicPath(const std::string& name) {
|
||||
return (name.rfind("music/", 0) == 0) ? name : "music/" + name;
|
||||
}
|
||||
|
||||
std::string normalizeSoundPath(const std::string& name) {
|
||||
return (name.rfind("sounds/", 0) == 0) ? name : "sounds/" + name;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace AudioResource {
|
||||
|
||||
auto getMusic(const std::string& name) -> Ja::Music* {
|
||||
auto& cache = musicCache();
|
||||
if (auto it = cache.find(name); it != cache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
const std::string path = normalizeMusicPath(name);
|
||||
auto bytes = Resource::Helper::loadFile(path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "[AudioResource] no se ha podido cargar música: " << path << "\n";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Ja::Music* raw = Ja::loadMusic(bytes.data(), static_cast<std::uint32_t>(bytes.size()), name.c_str());
|
||||
if (raw == nullptr) {
|
||||
std::cerr << "[AudioResource] decodificación de música falló: " << path << "\n";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
cache.emplace(name, std::unique_ptr<Ja::Music>(raw));
|
||||
std::cout << "[AudioResource] música cargada: " << path << "\n";
|
||||
return raw;
|
||||
}
|
||||
|
||||
auto getSound(const std::string& name) -> Ja::Sound* {
|
||||
auto& cache = soundCache();
|
||||
if (auto it = cache.find(name); it != cache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
const std::string path = normalizeSoundPath(name);
|
||||
auto bytes = Resource::Helper::loadFile(path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "[AudioResource] no se ha podido cargar sonido: " << path << "\n";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Ja::Sound* raw = Ja::loadSound(bytes.data(), static_cast<std::uint32_t>(bytes.size()));
|
||||
if (raw == nullptr) {
|
||||
std::cerr << "[AudioResource] decodificación de sonido falló: " << path << "\n";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
cache.emplace(name, std::unique_ptr<Ja::Sound>(raw));
|
||||
std::cout << "[AudioResource] sonido cargado: " << path << "\n";
|
||||
return raw;
|
||||
}
|
||||
|
||||
} // namespace AudioResource
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// --- Audio Resource Adapter ---
|
||||
// Este archivo exposa una interfície comuna a Audio per obtenir Ja::Music* /
|
||||
// Ja::Sound* per nom. Cada projecte la implementa en audio_adapter.cpp delegant
|
||||
// al seu singleton de recursos (Resource::Cache::get(), ...). Así audio.hpp
|
||||
// i audio.cpp es poden compartir entre projectes.
|
||||
|
||||
#include <string> // Para string
|
||||
|
||||
namespace Ja {
|
||||
struct Music;
|
||||
struct Sound;
|
||||
} // namespace Ja
|
||||
|
||||
namespace AudioResource {
|
||||
auto getMusic(const std::string& name) -> Ja::Music*;
|
||||
auto getSound(const std::string& name) -> Ja::Sound*;
|
||||
} // namespace AudioResource
|
||||
@@ -1,142 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// 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);
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
#include "core/audio/audio_effects.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/audio/jail_audio.hpp"
|
||||
|
||||
namespace AudioEffects {
|
||||
|
||||
namespace {
|
||||
|
||||
// --- Caps de cua ---
|
||||
constexpr float ECHO_TAIL_MS = 800.0F;
|
||||
constexpr float REVERB_TAIL_MS = 1500.0F;
|
||||
|
||||
// --- Constants Freeverb ---
|
||||
// Delays de comb i allpass tunats para 44.1 kHz; los reescalem per
|
||||
// freqüència real de la font.
|
||||
constexpr int COMB_REFERENCE_RATE = 44100;
|
||||
constexpr std::array<int, 8> COMB_DELAYS_L = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617};
|
||||
constexpr std::array<int, 4> ALLPASS_DELAYS_L = {556, 441, 341, 225};
|
||||
constexpr int STEREO_SPREAD = 23;
|
||||
|
||||
// Mapeig de Schroeder/Dattorro/Freeverb estàndard.
|
||||
constexpr float FIXED_GAIN = 0.015F;
|
||||
constexpr float SCALE_ROOM = 0.28F;
|
||||
constexpr float OFFSET_ROOM = 0.7F;
|
||||
constexpr float SCALE_DAMP = 0.4F;
|
||||
|
||||
// --- Decodificació a float -1..1 ---
|
||||
// Suporta U8/S16, mono/estèreo. Mono es duplica a L i R (la cadena
|
||||
// d'efectes treballa siempre con dos canals per simplicitat).
|
||||
auto decodeToStereoFloat(const Ja::Sound& src, std::vector<float>& left, std::vector<float>& right) -> bool {
|
||||
const auto& spec = src.spec;
|
||||
const Uint8* buf = src.buffer.get();
|
||||
if (buf == nullptr || src.length == 0) { return false; }
|
||||
|
||||
int bytes_per_sample = 0;
|
||||
if (spec.format == SDL_AUDIO_S16) {
|
||||
bytes_per_sample = 2;
|
||||
} else if (spec.format == SDL_AUDIO_U8) {
|
||||
bytes_per_sample = 1;
|
||||
} else {
|
||||
std::cerr << "[AudioEffects] formato de sonido no soportado (solo U8 o S16)\n";
|
||||
return false;
|
||||
}
|
||||
if (spec.channels < 1 || spec.channels > 2) {
|
||||
std::cerr << "[AudioEffects] el sonido debe ser mono o estéreo\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t TOTAL_FRAMES = src.length / static_cast<std::size_t>(bytes_per_sample * spec.channels);
|
||||
left.resize(TOTAL_FRAMES);
|
||||
right.resize(TOTAL_FRAMES);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_FRAMES; ++i) {
|
||||
float sample_l = 0.0F;
|
||||
float sample_r = 0.0F;
|
||||
if (spec.format == SDL_AUDIO_S16) {
|
||||
const auto* p = reinterpret_cast<const std::int16_t*>(buf + (i * spec.channels * 2));
|
||||
sample_l = static_cast<float>(p[0]) / 32768.0F;
|
||||
sample_r = (spec.channels == 2) ? static_cast<float>(p[1]) / 32768.0F : sample_l;
|
||||
} else { // U8
|
||||
const Uint8* p = buf + (i * spec.channels);
|
||||
sample_l = (static_cast<float>(p[0]) - 128.0F) / 128.0F;
|
||||
sample_r = (spec.channels == 2) ? (static_cast<float>(p[1]) - 128.0F) / 128.0F : sample_l;
|
||||
}
|
||||
left[i] = sample_l;
|
||||
right[i] = sample_r;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empaqueta dos canals float (-1..1) a S16 entrellaçat.
|
||||
void encodeStereoS16(const std::vector<float>& left, const std::vector<float>& right, std::vector<std::uint8_t>& out) {
|
||||
const std::size_t LEN = left.size();
|
||||
out.resize(LEN * 2 * sizeof(std::int16_t));
|
||||
auto* dst = reinterpret_cast<std::int16_t*>(out.data());
|
||||
for (std::size_t i = 0; i < LEN; ++i) {
|
||||
const float L = std::clamp(left[i], -1.0F, 1.0F);
|
||||
const float R = std::clamp(right[i], -1.0F, 1.0F);
|
||||
dst[(i * 2) + 0] = static_cast<std::int16_t>(std::lround(L * 32767.0F));
|
||||
dst[(i * 2) + 1] = static_cast<std::int16_t>(std::lround(R * 32767.0F));
|
||||
}
|
||||
}
|
||||
|
||||
// Reescala un delay de la taula de Freeverb para la freqüència real.
|
||||
auto scaledDelay(int reference_delay, int rate) -> int {
|
||||
const long SCALED = std::lround(static_cast<double>(reference_delay) * static_cast<double>(rate) / static_cast<double>(COMB_REFERENCE_RATE));
|
||||
return std::max(1, static_cast<int>(SCALED));
|
||||
}
|
||||
|
||||
// --- Filtres bàsics ---
|
||||
struct Comb {
|
||||
std::vector<float> buf;
|
||||
std::size_t idx{0};
|
||||
float feedback{0.0F};
|
||||
float damp1{0.0F};
|
||||
float damp2{1.0F};
|
||||
float store{0.0F};
|
||||
|
||||
void init(int delay, float fb, float damping) {
|
||||
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||
idx = 0;
|
||||
feedback = fb;
|
||||
damp1 = damping;
|
||||
damp2 = 1.0F - damping;
|
||||
store = 0.0F;
|
||||
}
|
||||
auto tick(float in) -> float {
|
||||
const float OUT = buf[idx];
|
||||
store = (OUT * damp2) + (store * damp1);
|
||||
buf[idx] = in + (store * feedback);
|
||||
idx = (idx + 1) % buf.size();
|
||||
return OUT;
|
||||
}
|
||||
};
|
||||
|
||||
struct Allpass {
|
||||
std::vector<float> buf;
|
||||
std::size_t idx{0};
|
||||
|
||||
void init(int delay) {
|
||||
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||
idx = 0;
|
||||
}
|
||||
auto tick(float in) -> float {
|
||||
const float BUFOUT = buf[idx];
|
||||
const float OUT = -in + BUFOUT;
|
||||
buf[idx] = in + (BUFOUT * 0.5F);
|
||||
idx = (idx + 1) % buf.size();
|
||||
return OUT;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound> {
|
||||
std::vector<float> left;
|
||||
std::vector<float> right;
|
||||
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||
|
||||
const int RATE = src.spec.freq;
|
||||
const int DELAY_SAMPLES = std::max(1, static_cast<int>(std::lround(params.delay_ms * 0.001F * static_cast<float>(RATE))));
|
||||
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(ECHO_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||
|
||||
const float FEEDBACK = std::clamp(params.feedback, 0.0F, 0.95F);
|
||||
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||
const float DRY = 1.0F - WET;
|
||||
|
||||
const std::size_t INPUT_LEN = left.size();
|
||||
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||
|
||||
std::vector<float> ring_l(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||
std::vector<float> ring_r(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||
std::size_t cursor = 0;
|
||||
|
||||
std::vector<float> out_l(TOTAL_LEN);
|
||||
std::vector<float> out_r(TOTAL_LEN);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||
|
||||
const float DELAYED_L = ring_l[cursor];
|
||||
const float DELAYED_R = ring_r[cursor];
|
||||
|
||||
out_l[i] = (DRY * IN_L) + (WET * DELAYED_L);
|
||||
out_r[i] = (DRY * IN_R) + (WET * DELAYED_R);
|
||||
|
||||
ring_l[cursor] = IN_L + (DELAYED_L * FEEDBACK);
|
||||
ring_r[cursor] = IN_R + (DELAYED_R * FEEDBACK);
|
||||
cursor = (cursor + 1) % static_cast<std::size_t>(DELAY_SAMPLES);
|
||||
}
|
||||
|
||||
ProcessedSound result;
|
||||
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||
encodeStereoS16(out_l, out_r, result.bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound> {
|
||||
std::vector<float> left;
|
||||
std::vector<float> right;
|
||||
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||
|
||||
const int RATE = src.spec.freq;
|
||||
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(REVERB_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||
|
||||
const float ROOM_SIZE = std::clamp(params.room_size, 0.0F, 1.0F);
|
||||
const float DAMPING = std::clamp(params.damping, 0.0F, 1.0F);
|
||||
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||
const float DRY = 1.0F - WET;
|
||||
|
||||
const float FEEDBACK = (ROOM_SIZE * SCALE_ROOM) + OFFSET_ROOM; // 0.7..0.98
|
||||
const float DAMP1 = DAMPING * SCALE_DAMP; // 0..0.4
|
||||
|
||||
// Inicialitza los 8 comb filters per cada canal i los 4 allpass.
|
||||
std::array<Comb, 8> comb_l;
|
||||
std::array<Comb, 8> comb_r;
|
||||
for (std::size_t i = 0; i < COMB_DELAYS_L.size(); ++i) {
|
||||
comb_l[i].init(scaledDelay(COMB_DELAYS_L[i], RATE), FEEDBACK, DAMP1);
|
||||
comb_r[i].init(scaledDelay(COMB_DELAYS_L[i] + STEREO_SPREAD, RATE), FEEDBACK, DAMP1);
|
||||
}
|
||||
std::array<Allpass, 4> allpass_l;
|
||||
std::array<Allpass, 4> allpass_r;
|
||||
for (std::size_t i = 0; i < ALLPASS_DELAYS_L.size(); ++i) {
|
||||
allpass_l[i].init(scaledDelay(ALLPASS_DELAYS_L[i], RATE));
|
||||
allpass_r[i].init(scaledDelay(ALLPASS_DELAYS_L[i] + STEREO_SPREAD, RATE));
|
||||
}
|
||||
|
||||
const std::size_t INPUT_LEN = left.size();
|
||||
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||
std::vector<float> out_l(TOTAL_LEN);
|
||||
std::vector<float> out_r(TOTAL_LEN);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||
const float MONO_INPUT = (IN_L + IN_R) * FIXED_GAIN;
|
||||
|
||||
// 8 comb filters en paral·lel, sumats.
|
||||
float wet_l = 0.0F;
|
||||
float wet_r = 0.0F;
|
||||
for (std::size_t k = 0; k < comb_l.size(); ++k) {
|
||||
wet_l += comb_l[k].tick(MONO_INPUT);
|
||||
wet_r += comb_r[k].tick(MONO_INPUT);
|
||||
}
|
||||
// 4 allpass en sèrie.
|
||||
for (std::size_t k = 0; k < allpass_l.size(); ++k) {
|
||||
wet_l = allpass_l[k].tick(wet_l);
|
||||
wet_r = allpass_r[k].tick(wet_r);
|
||||
}
|
||||
|
||||
out_l[i] = (DRY * IN_L) + (WET * wet_l);
|
||||
out_r[i] = (DRY * IN_R) + (WET * wet_r);
|
||||
}
|
||||
|
||||
ProcessedSound result;
|
||||
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||
encodeStereoS16(out_l, out_r, result.bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace AudioEffects
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// Forward-declaració per no incloure jail_audio.hpp (cicle d'inclusió: este
|
||||
// header viu sota los params declarats a jail_audio.hpp, i alhora jail_audio
|
||||
// usa applyEcho/applyReverb).
|
||||
namespace Ja {
|
||||
struct Sound;
|
||||
struct EchoParams;
|
||||
struct ReverbParams;
|
||||
} // namespace Ja
|
||||
|
||||
// Processadors d'efectes para sons puntuals. Reben un Ja::Sound (qualsevol
|
||||
// format suportat pel decodificador WAV: U8/S16, mono o estèreo) i tornen un
|
||||
// buffer PCM en S16 + el seu spec, llest per empenyer a un SDL_AudioStream.
|
||||
//
|
||||
// El buffer de sortida inclou la cua (decay) generada per l'efecte: per al
|
||||
// reverb, hasta a 1500 ms; para l'eco, hasta a 800 ms. Aquests caps eviten
|
||||
// allargar indefinidament la reproducció cuando los parámetros reinjecten mucho.
|
||||
//
|
||||
// Si el format del so d'origen no es pot processar, retornen std::nullopt
|
||||
// (el caller ha de fer fallback a reproducció seca).
|
||||
namespace AudioEffects {
|
||||
|
||||
struct ProcessedSound {
|
||||
std::vector<std::uint8_t> bytes; // PCM S16 entrellaçat (LRLRLR... si stereo)
|
||||
SDL_AudioSpec spec; // Format/canals/freqüència del buffer
|
||||
};
|
||||
|
||||
[[nodiscard]] auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound>;
|
||||
[[nodiscard]] auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound>;
|
||||
|
||||
} // namespace AudioEffects
|
||||
@@ -0,0 +1,645 @@
|
||||
#include "core/audio/jail_audio.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/audio_effects.hpp"
|
||||
|
||||
// Solo declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
|
||||
// d'implementació. Les definicions las aporta source/external/stb_vorbis_impl.cpp
|
||||
// (TU aïllat porque clang-analyzer no dispari fals positius al nostre codi).
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis es single-file: la macro de dalt limita este TU a solo-declaracions; la implementació viu a external/stb_vorbis_impl.cpp.
|
||||
#include "external/stb_vorbis.c"
|
||||
// clang-format on
|
||||
|
||||
namespace Ja {
|
||||
|
||||
// --- Streaming internals (file-scope constants) ---
|
||||
namespace {
|
||||
// Bytes-per-sample per canal (siempre s16)
|
||||
constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
|
||||
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
||||
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
||||
constexpr int MUSIC_CHUNK_SHORTS = 8192;
|
||||
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
||||
constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
|
||||
} // namespace
|
||||
|
||||
// --- Engine::active_ storage ---
|
||||
Engine* Engine::active_ = nullptr;
|
||||
|
||||
auto Engine::active() noexcept -> Engine* { return active_; }
|
||||
|
||||
// --- Ctor/Dtor ---
|
||||
|
||||
Engine::Engine(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||
assert(active_ == nullptr && "Ja::Engine: més d'una instància activa no está suportat");
|
||||
active_ = this;
|
||||
|
||||
audio_spec_ = {.format = format, .channels = num_channels, .freq = freq};
|
||||
sdl_audio_device_ = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec_);
|
||||
if (sdl_audio_device_ == 0) { std::fprintf(stderr, "Ja::Engine: Failed to initialize SDL audio!\n"); }
|
||||
for (auto& channel : channels_) { channel.state = ChannelState::FREE; }
|
||||
}
|
||||
|
||||
Engine::~Engine() {
|
||||
if (outgoing_music_.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
}
|
||||
if (sdl_audio_device_ != 0) { SDL_CloseAudioDevice(sdl_audio_device_); }
|
||||
sdl_audio_device_ = 0;
|
||||
|
||||
if (active_ == this) { active_ = nullptr; }
|
||||
}
|
||||
|
||||
// --- Helpers stateless (no toquen membres d'Engine) ---
|
||||
namespace {
|
||||
|
||||
auto feedMusicChunk(Music* music) -> int {
|
||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }
|
||||
|
||||
short chunk[MUSIC_CHUNK_SHORTS];
|
||||
const int NUM_CHANNELS = music->spec.channels;
|
||||
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
|
||||
music->vorbis,
|
||||
NUM_CHANNELS,
|
||||
static_cast<short*>(chunk),
|
||||
MUSIC_CHUNK_SHORTS);
|
||||
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
|
||||
|
||||
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(music->stream, static_cast<const void*>(chunk), BYTES);
|
||||
return SAMPLES_PER_CHANNEL;
|
||||
}
|
||||
|
||||
void pumpMusic(Music* music) {
|
||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
|
||||
|
||||
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
|
||||
const int DECODED = feedMusicChunk(music);
|
||||
if (DECODED > 0) { continue; }
|
||||
|
||||
// EOF: si queden loops, rebobinar; si no, tallar y deixar drenar.
|
||||
if (music->times != 0) {
|
||||
stb_vorbis_seek_start(music->vorbis);
|
||||
if (music->times > 0) { music->times--; }
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void preFillOutgoing(Music* music, const int duration_ms) {
|
||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
|
||||
|
||||
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
|
||||
const int DECODED = feedMusicChunk(music);
|
||||
if (DECODED <= 0) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única
|
||||
// font de la corba del fade: si es vol canviar a logarítmica/quadràtica,
|
||||
// s'edita aquí i afecta fade-in i fade-out alhora.
|
||||
auto fadeProgress(const FadeState& fade) -> float {
|
||||
if (fade.duration_ms <= 0) { return 1.0F; }
|
||||
const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time;
|
||||
if (ELAPSED >= static_cast<Uint64>(fade.duration_ms)) { return 1.0F; }
|
||||
return static_cast<float>(ELAPSED) / static_cast<float>(fade.duration_ms);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Engine::updateOutgoingFade() {
|
||||
if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }
|
||||
|
||||
// Mentre la fosa está activa, mantenim el stream con una reserva
|
||||
// de samples per davant del cursor (mismo patró que pumpMusic
|
||||
// para el current_music_). Así el stream no es buida ni cuando SDL
|
||||
// drena més ràpid del previst en haver sounds bound a la misma
|
||||
// device. Si l'OGG arriba a EOF, rebobina (la fosa pot ser més
|
||||
// llarga que la pista).
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
const Music& music = *outgoing_music_.music;
|
||||
const int BYTES_PER_SECOND = music.spec.freq * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
const int LOW_WATER = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||
while (SDL_GetAudioStreamAvailable(outgoing_music_.stream) < LOW_WATER) {
|
||||
short chunk[MUSIC_CHUNK_SHORTS];
|
||||
const int SAMPLES = stb_vorbis_get_samples_short_interleaved(
|
||||
music.vorbis,
|
||||
music.spec.channels,
|
||||
static_cast<short*>(chunk),
|
||||
MUSIC_CHUNK_SHORTS);
|
||||
if (SAMPLES <= 0) {
|
||||
stb_vorbis_seek_start(music.vorbis);
|
||||
continue;
|
||||
}
|
||||
const int BYTES = SAMPLES * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(outgoing_music_.stream, static_cast<const void*>(chunk), BYTES);
|
||||
}
|
||||
}
|
||||
|
||||
const float PROGRESS = fadeProgress(outgoing_music_.fade);
|
||||
if (PROGRESS >= 1.0F) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
// Deixem el Vorbis del Music original en un estat conegut per
|
||||
// a la pròxima reproducció. (playMusic también fa seek_start,
|
||||
// pero fer-ho ací evita estats intermedis si algú consulta.)
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
|
||||
}
|
||||
outgoing_music_.music = nullptr;
|
||||
} else {
|
||||
SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS));
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::updateIncomingFade() {
|
||||
if (!incoming_fade_.active) { return; }
|
||||
|
||||
const float PROGRESS = fadeProgress(incoming_fade_);
|
||||
if (PROGRESS >= 1.0F) {
|
||||
incoming_fade_.active = false;
|
||||
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||
} else {
|
||||
SDL_SetAudioStreamGain(current_music_->stream, music_volume_ * PROGRESS);
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::updateCurrentMusic() {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||
|
||||
updateIncomingFade();
|
||||
|
||||
pumpMusic(current_music_);
|
||||
if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) {
|
||||
// La pista ha acabat de drenar naturalment. L'aturem primer (deixa
|
||||
// l'engine en estat consistent) i entonces invoquem el callback;
|
||||
// así un eventual playMusic des del callback comença net.
|
||||
stopMusic();
|
||||
if (on_music_ended_) { on_music_ended_(); }
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::updateSoundChannels() {
|
||||
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||
if (channels_[i].state != ChannelState::PLAYING) { continue; }
|
||||
|
||||
if (channels_[i].times != 0) {
|
||||
if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(channels_[i].stream)) < (channels_[i].sound->length / 2)) {
|
||||
SDL_PutAudioStreamData(channels_[i].stream, channels_[i].sound->buffer.get(), channels_[i].sound->length);
|
||||
if (channels_[i].times > 0) { channels_[i].times--; }
|
||||
}
|
||||
} else if (SDL_GetAudioStreamAvailable(channels_[i].stream) == 0) {
|
||||
stopChannel(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::stealCurrentIntoOutgoing(const int duration_ms) {
|
||||
if (outgoing_music_.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
}
|
||||
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING || current_music_->stream == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
preFillOutgoing(current_music_, duration_ms);
|
||||
|
||||
outgoing_music_.stream = current_music_->stream;
|
||||
// Guardem la referència al Music porque updateOutgoingFade puga
|
||||
// seguir bombant Vorbis sin al stream durante tota la fosa. NO fem
|
||||
// seek_start ací: la decompressió ha de continuar des d'on estava
|
||||
// porque el so siga continu. El seek_start es farà cuando la fosa
|
||||
// acabe (o cuando playMusic la interrompi via stopMusic).
|
||||
outgoing_music_.music = current_music_;
|
||||
outgoing_music_.fade = {
|
||||
.active = true,
|
||||
.start_time = SDL_GetTicks(),
|
||||
.duration_ms = duration_ms,
|
||||
.initial_volume = music_volume_,
|
||||
};
|
||||
current_music_->stream = nullptr;
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void Engine::forEachTargetChannel(const int channel, Fn&& fn) {
|
||||
if (channel == -1) {
|
||||
for (auto& ch : channels_) { fn(ch); }
|
||||
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
|
||||
fn(channels_[channel]);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Engine public API ---
|
||||
|
||||
void Engine::update() {
|
||||
updateOutgoingFade();
|
||||
updateCurrentMusic();
|
||||
updateSoundChannels();
|
||||
}
|
||||
|
||||
void Engine::playMusic(Music* music, const int loop) {
|
||||
if (music == nullptr || music->vorbis == nullptr) { return; }
|
||||
|
||||
stopMusic();
|
||||
|
||||
current_music_ = music;
|
||||
current_music_->state = MusicState::PLAYING;
|
||||
current_music_->times = loop;
|
||||
|
||||
stb_vorbis_seek_start(current_music_->vorbis);
|
||||
|
||||
current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_);
|
||||
if (current_music_->stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::playMusic: Failed to create audio stream!\n");
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
return;
|
||||
}
|
||||
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||
|
||||
pumpMusic(current_music_);
|
||||
|
||||
if (!SDL_BindAudioStream(sdl_audio_device_, current_music_->stream)) {
|
||||
std::fprintf(stderr, "Ja::Engine::playMusic: SDL_BindAudioStream failed!\n");
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::setMusicSpeed(float ratio) {
|
||||
if (current_music_ == nullptr || current_music_->stream == nullptr) { return; }
|
||||
SDL_SetAudioStreamFrequencyRatio(current_music_->stream, ratio);
|
||||
}
|
||||
|
||||
void Engine::pauseMusic() {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||
|
||||
current_music_->state = MusicState::PAUSED;
|
||||
SDL_UnbindAudioStream(current_music_->stream);
|
||||
}
|
||||
|
||||
void Engine::resumeMusic() {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PAUSED) { return; }
|
||||
|
||||
current_music_->state = MusicState::PLAYING;
|
||||
SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
|
||||
}
|
||||
|
||||
void Engine::stopMusic() {
|
||||
if (outgoing_music_.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
|
||||
}
|
||||
outgoing_music_.music = nullptr;
|
||||
}
|
||||
incoming_fade_.active = false;
|
||||
|
||||
if (current_music_ == nullptr || current_music_->state == MusicState::INVALID || current_music_->state == MusicState::STOPPED) { return; }
|
||||
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
if (current_music_->stream != nullptr) {
|
||||
SDL_DestroyAudioStream(current_music_->stream);
|
||||
current_music_->stream = nullptr;
|
||||
}
|
||||
if (current_music_->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(current_music_->vorbis);
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::fadeOutMusic(const int milliseconds) {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||
|
||||
stealCurrentIntoOutgoing(milliseconds);
|
||||
incoming_fade_.active = false;
|
||||
}
|
||||
|
||||
void Engine::crossfadeMusic(Music* music, const int crossfade_ms, const int loop) {
|
||||
if (music == nullptr || music->vorbis == nullptr) { return; }
|
||||
|
||||
stealCurrentIntoOutgoing(crossfade_ms);
|
||||
|
||||
current_music_ = music;
|
||||
current_music_->state = MusicState::PLAYING;
|
||||
current_music_->times = loop;
|
||||
|
||||
stb_vorbis_seek_start(current_music_->vorbis);
|
||||
current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_);
|
||||
if (current_music_->stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::crossfadeMusic: Failed to create audio stream!\n");
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
return;
|
||||
}
|
||||
SDL_SetAudioStreamGain(current_music_->stream, 0.0F);
|
||||
pumpMusic(current_music_);
|
||||
SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
|
||||
|
||||
incoming_fade_ = {
|
||||
.active = true,
|
||||
.start_time = SDL_GetTicks(),
|
||||
.duration_ms = crossfade_ms,
|
||||
.initial_volume = 0.0F,
|
||||
};
|
||||
}
|
||||
|
||||
auto Engine::getMusicState() const -> MusicState {
|
||||
if (current_music_ == nullptr) { return MusicState::INVALID; }
|
||||
return current_music_->state;
|
||||
}
|
||||
|
||||
auto Engine::setMusicVolume(float volume) -> float {
|
||||
music_volume_ = SDL_clamp(volume, 0.0F, 1.0F);
|
||||
if (current_music_ != nullptr && current_music_->stream != nullptr) {
|
||||
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||
}
|
||||
return music_volume_;
|
||||
}
|
||||
|
||||
void Engine::setOnMusicEnded(std::function<void()> callback) {
|
||||
on_music_ended_ = std::move(callback);
|
||||
}
|
||||
|
||||
void Engine::onMusicDeleted(const Music* music) {
|
||||
if (music == nullptr) { return; }
|
||||
if (current_music_ == music) {
|
||||
stopMusic();
|
||||
current_music_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sound ---
|
||||
|
||||
auto Engine::playSound(Sound* sound, const int loop, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
|
||||
int channel = 0;
|
||||
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { channel++; }
|
||||
if (channel == MAX_SIMULTANEOUS_CHANNELS) {
|
||||
// No hay canal libre, reemplazamos el primero
|
||||
channel = 0;
|
||||
}
|
||||
|
||||
return playSoundOnChannel(sound, channel, loop, group);
|
||||
}
|
||||
|
||||
auto Engine::playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; }
|
||||
|
||||
stopChannel(channel);
|
||||
|
||||
channels_[channel].sound = sound;
|
||||
channels_[channel].times = loop;
|
||||
channels_[channel].pos = 0;
|
||||
channels_[channel].group = group;
|
||||
channels_[channel].state = ChannelState::PLAYING;
|
||||
channels_[channel].stream = SDL_CreateAudioStream(&channels_[channel].sound->spec, &audio_spec_);
|
||||
|
||||
if (channels_[channel].stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::playSoundOnChannel: Failed to create audio stream!\n");
|
||||
channels_[channel].state = ChannelState::FREE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
SDL_PutAudioStreamData(channels_[channel].stream, channels_[channel].sound->buffer.get(), channels_[channel].sound->length);
|
||||
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[group]);
|
||||
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
void Engine::setChannelSpeed(const int channel, const float ratio) {
|
||||
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return; }
|
||||
if (channels_[channel].stream == nullptr) { return; }
|
||||
SDL_SetAudioStreamFrequencyRatio(channels_[channel].stream, ratio);
|
||||
}
|
||||
|
||||
void Engine::pauseChannel(const int channel) {
|
||||
forEachTargetChannel(channel, [](Channel& ch) {
|
||||
if (ch.state == ChannelState::PLAYING) {
|
||||
ch.state = ChannelState::PAUSED;
|
||||
SDL_UnbindAudioStream(ch.stream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Engine::resumeChannel(const int channel) {
|
||||
const SDL_AudioDeviceID DEVICE = sdl_audio_device_;
|
||||
forEachTargetChannel(channel, [DEVICE](Channel& ch) {
|
||||
if (ch.state == ChannelState::PAUSED) {
|
||||
ch.state = ChannelState::PLAYING;
|
||||
SDL_BindAudioStream(DEVICE, ch.stream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Engine::stopChannel(const int channel) {
|
||||
forEachTargetChannel(channel, [this](Channel& ch) {
|
||||
if (ch.state != ChannelState::FREE) {
|
||||
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
||||
ch.stream = nullptr;
|
||||
ch.state = ChannelState::FREE;
|
||||
ch.pos = 0;
|
||||
ch.sound = nullptr;
|
||||
if (ch.has_effect) {
|
||||
ch.has_effect = false;
|
||||
if (effect_channels_active_ > 0) { --effect_channels_active_; }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto Engine::setSoundVolume(float volume, const int group) -> float {
|
||||
const float V = SDL_clamp(volume, 0.0F, 1.0F);
|
||||
|
||||
if (group == -1) {
|
||||
std::ranges::fill(sound_volume_, V);
|
||||
} else if (group >= 0 && group < MAX_GROUPS) {
|
||||
sound_volume_[group] = V;
|
||||
} else {
|
||||
return V;
|
||||
}
|
||||
|
||||
for (auto& ch : channels_) {
|
||||
if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) {
|
||||
if (group == -1 || ch.group == group) {
|
||||
if (ch.stream != nullptr) {
|
||||
SDL_SetAudioStreamGain(ch.stream, sound_volume_[ch.group]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return V;
|
||||
}
|
||||
|
||||
void Engine::onSoundDeleted(const Sound* sound) {
|
||||
if (sound == nullptr) { return; }
|
||||
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||
if (channels_[i].sound == sound) { stopChannel(i); }
|
||||
}
|
||||
}
|
||||
|
||||
auto Engine::playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, const int group) -> int {
|
||||
// El sin de canals con efecte es valida antes de reservar slot —
|
||||
// así evitem crear y destruir un stream solo per descartar el play.
|
||||
if (effect_channels_active_ >= MAX_EFFECT_CHANNELS) { return -1; }
|
||||
|
||||
int channel = 0;
|
||||
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { ++channel; }
|
||||
if (channel == MAX_SIMULTANEOUS_CHANNELS) { channel = 0; }
|
||||
|
||||
stopChannel(channel);
|
||||
|
||||
// El stream es crea contra l'spec del buffer processat (S16, ...)
|
||||
// porque SDL faci el resampling sin a audio_spec_ del device.
|
||||
channels_[channel].stream = SDL_CreateAudioStream(&spec, &audio_spec_);
|
||||
if (channels_[channel].stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::playProcessedOnFreeChannel: Failed to create audio stream!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
channels_[channel].sound = nullptr; // El buffer no es propietat de sin Ja::Sound.
|
||||
channels_[channel].times = 0;
|
||||
channels_[channel].pos = 0;
|
||||
const int CLAMPED_GROUP = (group >= 0 && group < MAX_GROUPS) ? group : 0;
|
||||
channels_[channel].group = CLAMPED_GROUP;
|
||||
channels_[channel].state = ChannelState::PLAYING;
|
||||
channels_[channel].has_effect = true;
|
||||
++effect_channels_active_;
|
||||
|
||||
SDL_PutAudioStreamData(channels_[channel].stream, bytes.data(), static_cast<int>(bytes.size()));
|
||||
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[CLAMPED_GROUP]);
|
||||
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& params, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
auto processed = AudioEffects::applyEcho(*sound, params);
|
||||
if (!processed) { return -1; }
|
||||
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||
}
|
||||
|
||||
auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& params, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
auto processed = AudioEffects::applyReverb(*sound, params);
|
||||
if (!processed) { return -1; }
|
||||
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||
}
|
||||
|
||||
// --- Factories y destructors (permanents) ---
|
||||
|
||||
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||
if (buffer == nullptr || length == 0) { return nullptr; }
|
||||
|
||||
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
|
||||
// como a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||
// persistent al buffer; como que ací no el resize'jem, el .data() es
|
||||
// estable durante tot el cicle de vida del music.
|
||||
auto music = std::make_unique<Music>();
|
||||
music->ogg_data.assign(buffer, buffer + length);
|
||||
|
||||
int vorbis_error = 0;
|
||||
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||
static_cast<int>(length),
|
||||
&vorbis_error,
|
||||
nullptr);
|
||||
if (music->vorbis == nullptr) {
|
||||
std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
|
||||
music->spec.channels = static_cast<int>(INFO.channels);
|
||||
music->spec.freq = static_cast<int>(INFO.sample_rate);
|
||||
music->spec.format = SDL_AUDIO_S16;
|
||||
// Pre-cálculo de la duración en ms a partir del header. stb_vorbis ya
|
||||
// ha decodificat la informació necessària a `stb_vorbis_open_memory`;
|
||||
// esta consulta no descodifica àudio, solo llig el comptador
|
||||
// de samples. Si el sample_rate fos 0 (header malmès) deixem
|
||||
// duration_ms a 0.
|
||||
if (INFO.sample_rate > 0) {
|
||||
const auto SAMPLES = stb_vorbis_stream_length_in_samples(music->vorbis);
|
||||
music->duration_ms = static_cast<int>((static_cast<std::uint64_t>(SAMPLES) * 1000ULL) / INFO.sample_rate);
|
||||
}
|
||||
music->state = MusicState::STOPPED;
|
||||
|
||||
return music.release();
|
||||
}
|
||||
|
||||
// Overload con filename. Resource::Cache l'usa per registrar el path dins
|
||||
// del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar
|
||||
// el nom después d'un playMusic(Ja::Music*, ...) — veure PATCH-02.
|
||||
auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* {
|
||||
Music* music = loadMusic(buffer, length);
|
||||
if (music != nullptr && filename != nullptr) { music->filename = filename; }
|
||||
return music;
|
||||
}
|
||||
|
||||
void deleteMusic(Music* music) {
|
||||
if (music == nullptr) { return; }
|
||||
// Notifiquem el motor actiu porque pari la pista si es la current_music.
|
||||
// Si no hay motor (shutdown-order invertit), passem: los recursos
|
||||
// propis del Music es lliberen igualment a sota.
|
||||
if (Engine* eng = Engine::active()) { eng->onMusicDeleted(music); }
|
||||
|
||||
if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
|
||||
if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); }
|
||||
delete music;
|
||||
}
|
||||
|
||||
auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
|
||||
auto sound = std::make_unique<Sound>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
|
||||
std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||
return sound.release();
|
||||
}
|
||||
|
||||
void deleteSound(Sound* sound) {
|
||||
if (sound == nullptr) { return; }
|
||||
if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); }
|
||||
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
|
||||
delete sound;
|
||||
}
|
||||
|
||||
} // namespace Ja
|
||||
|
||||
// --- stb_vorbis macro leak cleanup ---
|
||||
// stb_vorbis.c filtra noms curts (L, C, R i PLAYBACK_*) al TU que el compila.
|
||||
// Xocarien con parámetros de plantilla d'altres headers si estas definicions
|
||||
// s'escapessin. Els netegem al final del TU per tancar la porta.
|
||||
// clang-format off
|
||||
#undef L
|
||||
#undef C
|
||||
#undef R
|
||||
#undef PLAYBACK_MONO
|
||||
#undef PLAYBACK_LEFT
|
||||
#undef PLAYBACK_RIGHT
|
||||
// clang-format on
|
||||
+242
-466
@@ -2,481 +2,257 @@
|
||||
|
||||
// --- 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
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// --- 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 };
|
||||
// Forward-declaració del decoder de vorbis. La implementació viu a
|
||||
// jail_audio.cpp (únic TU que compila external/stb_vorbis.c). Qualsevol caller
|
||||
// solo necessita `stb_vorbis*` per punter — nunca per valor — así que el
|
||||
// forward decl n'hay prou i evita arrossegar el .c a tots los TU.
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) — nom imposat per l'API de stb_vorbis
|
||||
struct stb_vorbis;
|
||||
|
||||
// --- 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};
|
||||
// Deleter stateless para buffers reservats con `SDL_malloc` / `SDL_LoadWAV*`.
|
||||
// Compatible con `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size overhead
|
||||
// gràcies a EBO, igual que un unique_ptr con default_delete.
|
||||
struct SdlFreeDeleter {
|
||||
void operator()(Uint8* p) const noexcept {
|
||||
if (p != nullptr) { SDL_free(p); }
|
||||
}
|
||||
};
|
||||
|
||||
struct JA_Channel_t {
|
||||
JA_Sound_t* sound{nullptr};
|
||||
// Motor de baix nivell d'àudio del projecte jailgames: streaming OGG
|
||||
// (stb_vorbis) + N canals d'efectes (SDL3 audio). No depèn d'Options ni de sin
|
||||
// singleton del joc; solo de SDL3 i stb_vorbis. La capa superior (Audio) li
|
||||
// passa recursos pel punter i fa el bookkeeping d'usuari.
|
||||
namespace Ja {
|
||||
|
||||
// --- Public Enums ---
|
||||
enum class ChannelState : std::uint8_t {
|
||||
FREE,
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
};
|
||||
|
||||
enum class MusicState : std::uint8_t {
|
||||
INVALID, // Music carregat pero nunca play-ejat
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
STOPPED,
|
||||
};
|
||||
|
||||
// --- Constants ---
|
||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||
inline constexpr int MAX_GROUPS = 2;
|
||||
// Cap superior de canals que poden estar simultàniament reproduint un so
|
||||
// con efecte (eco/reverb). Si está al límit, las noves crides con efecte
|
||||
// cauen al camí sec — l'usuari sent el so igualment, sin la cua.
|
||||
inline constexpr int MAX_EFFECT_CHANNELS = 4;
|
||||
|
||||
// --- Paràmetres d'efectes ---
|
||||
// Els camps los fixa el caller (Audio) llegint sounds.yaml; el motor solo
|
||||
// los passa a AudioEffects::applyEcho/applyReverb. Els defaults són
|
||||
// sensats pero los presets los sobreescriuen.
|
||||
struct EchoParams {
|
||||
float delay_ms{220.0F}; // Temps hasta al primer rebot.
|
||||
float feedback{0.45F}; // Reinjecció (0..0.95).
|
||||
float wet{0.35F}; // Mescla humida (0..1).
|
||||
};
|
||||
|
||||
struct ReverbParams {
|
||||
float room_size{0.7F}; // Tamaño percebuda (0..1).
|
||||
float damping{0.5F}; // Atenuació d'aguts per rebot (0..1).
|
||||
float wet{0.4F}; // Mescla humida (0..1).
|
||||
};
|
||||
|
||||
// Spec de fallback del dispositiu. S'aplica antes que l'Engine s'iniciï i
|
||||
// como a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor
|
||||
// d'Engine, alimentat des de Defaults::Audio via Audio.
|
||||
inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000};
|
||||
|
||||
// --- Struct Definitions ---
|
||||
struct Sound {
|
||||
SDL_AudioSpec spec{DEFAULT_SPEC};
|
||||
Uint32 length{0};
|
||||
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
|
||||
// via SDL_malloc; el deleter `SdlFreeDeleter` allibera con SDL_free.
|
||||
std::unique_ptr<Uint8[], SdlFreeDeleter> buffer;
|
||||
};
|
||||
|
||||
// L'ordre (punters primer, ints después, enum de 8 bits al final) minimitza
|
||||
// el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding).
|
||||
struct Channel {
|
||||
Sound* sound{nullptr};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
int pos{0};
|
||||
int times{0};
|
||||
int group{0};
|
||||
ChannelState state{ChannelState::FREE};
|
||||
// Marca si este canal va arrencar con so processat per un efecte.
|
||||
// El motor compta canals actius con efecte per fer complir
|
||||
// MAX_EFFECT_CHANNELS i alliberar el comptador en parar.
|
||||
bool has_effect{false};
|
||||
};
|
||||
|
||||
struct Music {
|
||||
SDL_AudioSpec spec{DEFAULT_SPEC};
|
||||
|
||||
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
|
||||
// d'entrada una sola vegada en loadMusic i es descomprimix en chunks
|
||||
// per streaming. Como que stb_vorbis guarda un punter persistent al
|
||||
// `.data()` d'este vector, no el podem resize'jar un cop establert
|
||||
// (una reallocation invalidaria el punter que el decoder conserva).
|
||||
std::vector<Uint8> ogg_data;
|
||||
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del Music
|
||||
|
||||
std::string filename;
|
||||
|
||||
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
|
||||
// Duración total de la pista en mil·lisegons, mesurada via
|
||||
// `stb_vorbis_stream_length_in_samples / sample_rate` al
|
||||
// `loadMusic`. 0 si el cálculo no es possible (header malmès).
|
||||
// L'usen consumidors que necessiten un timeline pre-calculat —
|
||||
// p. ex. la FSM de sala — sin dependre de callbacks de fi.
|
||||
int duration_ms{0};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_Channel_state state{JA_CHANNEL_FREE};
|
||||
};
|
||||
MusicState state{MusicState::INVALID};
|
||||
};
|
||||
|
||||
struct JA_Music_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
Uint32 length{0};
|
||||
Uint8* buffer{nullptr};
|
||||
char* filename{nullptr};
|
||||
struct FadeState {
|
||||
bool active{false};
|
||||
Uint64 start_time{0};
|
||||
int duration_ms{0};
|
||||
float initial_volume{0.0F};
|
||||
};
|
||||
|
||||
int pos{0};
|
||||
int times{0};
|
||||
struct OutgoingMusic {
|
||||
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;
|
||||
}
|
||||
// Referència al Music original porque updateOutgoingFade puga
|
||||
// continuar descomprimint des de Vorbis sin al stream durante
|
||||
// tota la fosa. Sense això, solo tenim el pre-fill puntual i
|
||||
// SDL drena el stream més ràpid del previst cuando hay sounds
|
||||
// bound a la misma device (~2x), buidant-lo a meitat del
|
||||
// fade i sentint-se como un tall sec.
|
||||
Music* music{nullptr};
|
||||
FadeState fade;
|
||||
};
|
||||
|
||||
// --- Engine ---
|
||||
// Encapsula tot l'estat que antes vivia como a globals inline. Un sol Engine
|
||||
// viu per procés (enforceat via assert al ctor contra `active_`). El ctor
|
||||
// obre el device SDL; el dtor el tanca (RAII). Els deleters
|
||||
// `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via
|
||||
// `Engine::active()` per parar canals antes d'alliberar.
|
||||
class Engine {
|
||||
public:
|
||||
Engine(int freq, SDL_AudioFormat format, int num_channels);
|
||||
~Engine();
|
||||
Engine(const Engine&) = delete;
|
||||
auto operator=(const Engine&) -> Engine& = delete;
|
||||
Engine(Engine&&) = delete;
|
||||
auto operator=(Engine&&) -> Engine& = delete;
|
||||
|
||||
// Retorna el motor actiu o nullptr si sin ha estat construït. L'usen
|
||||
// los deleters de recursos porque no los arriba sin referència directa.
|
||||
[[nodiscard]] static auto active() noexcept -> Engine*;
|
||||
|
||||
void update();
|
||||
|
||||
// --- Música ---
|
||||
void playMusic(Music* music, int loop = -1);
|
||||
void pauseMusic();
|
||||
void resumeMusic();
|
||||
void stopMusic();
|
||||
void fadeOutMusic(int milliseconds);
|
||||
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
||||
[[nodiscard]] auto getMusicState() const -> MusicState;
|
||||
auto setMusicVolume(float volume) -> float;
|
||||
// Multiplicador de velocitat de reproducció de la música actual
|
||||
// via `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal, 2.0 =
|
||||
// doble velocitat. Cal saber que también puja el to (efecte
|
||||
// "chipmunk") — es el comportament arcade clàssic dels comptes
|
||||
// enrere. Cada `playMusic` crea un stream nuevo con ratio 1.0,
|
||||
// así que un canvi de track reseteja la velocitat
|
||||
// implícitament. No-op si no hay música activa.
|
||||
void setMusicSpeed(float ratio);
|
||||
// Registra un callback que es disparà cuando la música actual acabi de
|
||||
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
|
||||
// stopMusic, así que el callback pot invocar playMusic sin córrer.
|
||||
// S'executa al mismo thread que Engine::update (render loop); no fer
|
||||
// operacions blocants.
|
||||
void setOnMusicEnded(std::function<void()> callback);
|
||||
// Notifica al motor que un Music s'está destruint: si es el current_music
|
||||
// s'atura antes que los seus recursos (stream/vorbis) deixin de ser vàlids.
|
||||
void onMusicDeleted(const Music* music);
|
||||
|
||||
// --- So ---
|
||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||
// Ajusta la velocitat de reproducció d'un canal actiu via
|
||||
// `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal. Igual que a
|
||||
// `setMusicSpeed`, puja/baixa el to junt con la velocitat
|
||||
// (efecte "chipmunk"); para SFX curts arcade es el que volem.
|
||||
// No-op si el canal no está actiu. Cridar-lo just después de
|
||||
// `playSound`/`playSoundOnChannel` porque el ratio cobreixi
|
||||
// tota la reproducció.
|
||||
void setChannelSpeed(int channel, float ratio);
|
||||
// Reproducció con so processat per un efecte. Retorna el canal
|
||||
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
|
||||
// El sound original solo s'usa per consultar el spec/buffer; el
|
||||
// canal manipula el buffer ya processat (no reapunta a `sound`).
|
||||
auto playSoundWithEcho(const Sound* sound, const EchoParams& params, int group = 0) -> int;
|
||||
auto playSoundWithReverb(const Sound* sound, const ReverbParams& params, int group = 0) -> int;
|
||||
void pauseChannel(int channel);
|
||||
void resumeChannel(int channel);
|
||||
void stopChannel(int channel);
|
||||
auto setSoundVolume(float volume, int group = -1) -> float;
|
||||
// Notifica al motor que un Sound s'está destruint: los canals que el
|
||||
// referenciïn es paren antes d'alliberar el buffer.
|
||||
void onSoundDeleted(const Sound* sound);
|
||||
|
||||
private:
|
||||
void stealCurrentIntoOutgoing(int duration_ms);
|
||||
void updateOutgoingFade();
|
||||
void updateIncomingFade();
|
||||
void updateCurrentMusic();
|
||||
void updateSoundChannels();
|
||||
// Empenta un buffer ya processat (S16) a un canal lliure y el deixa
|
||||
// sonar sin bucle. Camí comú dels dos overloads playSoundWith*.
|
||||
// Retorna el canal o -1 si no queden slots.
|
||||
auto playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, int group) -> int;
|
||||
|
||||
template <typename Fn>
|
||||
void forEachTargetChannel(int channel, Fn&& fn);
|
||||
|
||||
Music* current_music_{nullptr};
|
||||
Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{};
|
||||
SDL_AudioSpec audio_spec_{DEFAULT_SPEC};
|
||||
float music_volume_{1.0F};
|
||||
float sound_volume_[MAX_GROUPS]{};
|
||||
SDL_AudioDeviceID sdl_audio_device_{0};
|
||||
OutgoingMusic outgoing_music_;
|
||||
FadeState incoming_fade_;
|
||||
std::function<void()> on_music_ended_;
|
||||
// Comptador derivat de Channel::has_effect — evita haver-lo de
|
||||
// recalcular cada vegada que algú demana un play con efecte.
|
||||
int effect_channels_active_{0};
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static con sufix _
|
||||
static Engine* active_;
|
||||
};
|
||||
|
||||
// --- Factories y destructors (permanents) ---
|
||||
// No depenen de l'estat del motor: loadMusic/loadSound solo construeixen
|
||||
// objectes, deleteMusic/deleteSound consulten Engine::active() per parar
|
||||
// canals antes d'alliberar (si el motor aún viu).
|
||||
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*;
|
||||
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*;
|
||||
void deleteMusic(Music* music);
|
||||
[[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
|
||||
void deleteSound(Sound* sound);
|
||||
|
||||
} // namespace Ja
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "core/audio/sound_effects_config.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Lector de camp con fallback: deixa el destí intacte si la clau no
|
||||
// existeix (los defaults dels Ja::*Params s'inicialitzen al ctor del
|
||||
// struct, así que el comportament es "preset parcial = preset complet
|
||||
// con defaults per als camps que falten").
|
||||
template <typename T>
|
||||
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||
if (node.contains(key)) { dst = node[key].get_value<T>(); }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
|
||||
static SoundEffectsConfig instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void SoundEffectsConfig::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "[SoundEffectsConfig] no se ha podido abrir " << file_path
|
||||
<< " — sin presets de efecto disponibles\n";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||
const auto* end = begin + bytes.size();
|
||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||
|
||||
if (yaml.contains("echo") && yaml["echo"].is_mapping()) {
|
||||
for (auto it = yaml["echo"].begin(); it != yaml["echo"].end(); ++it) {
|
||||
const auto NAME = it.key().get_value<std::string>();
|
||||
const auto& node = it.value();
|
||||
Ja::EchoParams params{};
|
||||
readField(node, "delay_ms", params.delay_ms);
|
||||
readField(node, "feedback", params.feedback);
|
||||
readField(node, "wet", params.wet);
|
||||
echoes_[NAME] = params;
|
||||
}
|
||||
}
|
||||
|
||||
if (yaml.contains("reverb") && yaml["reverb"].is_mapping()) {
|
||||
for (auto it = yaml["reverb"].begin(); it != yaml["reverb"].end(); ++it) {
|
||||
const auto NAME = it.key().get_value<std::string>();
|
||||
const auto& node = it.value();
|
||||
Ja::ReverbParams params{};
|
||||
readField(node, "room_size", params.room_size);
|
||||
readField(node, "damping", params.damping);
|
||||
readField(node, "wet", params.wet);
|
||||
reverbs_[NAME] = params;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[SoundEffectsConfig] " << echoes_.size() << " preset(s) de echo y "
|
||||
<< reverbs_.size() << " de reverb desde " << file_path << "\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[SoundEffectsConfig] error parseando " << file_path << ": " << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
auto SoundEffectsConfig::findEcho(const std::string& name) const -> const Ja::EchoParams* {
|
||||
const auto IT = echoes_.find(name);
|
||||
return (IT == echoes_.end()) ? nullptr : &IT->second;
|
||||
}
|
||||
|
||||
auto SoundEffectsConfig::findReverb(const std::string& name) const -> const Ja::ReverbParams* {
|
||||
const auto IT = reverbs_.find(name);
|
||||
return (IT == reverbs_.end()) ? nullptr : &IT->second;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Para Ja::EchoParams / Ja::ReverbParams
|
||||
|
||||
// Catàleg de presets d'efectes carregat des de data/config/sounds.yaml. La capa
|
||||
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
|
||||
// preset no existeix, el so es reprodueix sec con un avís a stderr.
|
||||
//
|
||||
// Patró Meyers idèntic a UiConfig/Locale: un sol load() a l'arrencada, sense
|
||||
// hot-reload. Si el archivo no existeix, el catàleg queda buit (sin preset
|
||||
// disponible) i tots los playSoundWith* es comporten como playSound dry.
|
||||
class SoundEffectsConfig {
|
||||
public:
|
||||
static auto get() -> SoundEffectsConfig&;
|
||||
|
||||
SoundEffectsConfig(const SoundEffectsConfig&) = delete;
|
||||
SoundEffectsConfig(SoundEffectsConfig&&) = delete;
|
||||
auto operator=(const SoundEffectsConfig&) -> SoundEffectsConfig& = delete;
|
||||
auto operator=(SoundEffectsConfig&&) -> SoundEffectsConfig& = delete;
|
||||
|
||||
void load(const std::string& file_path);
|
||||
|
||||
// Retorna nullptr si el preset no existeix.
|
||||
[[nodiscard]] auto findEcho(const std::string& name) const -> const Ja::EchoParams*;
|
||||
[[nodiscard]] auto findReverb(const std::string& name) const -> const Ja::ReverbParams*;
|
||||
|
||||
private:
|
||||
SoundEffectsConfig() = default;
|
||||
~SoundEffectsConfig() = default;
|
||||
|
||||
std::unordered_map<std::string, Ja::EchoParams> echoes_;
|
||||
std::unordered_map<std::string, Ja::ReverbParams> reverbs_;
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
// postfx_config.cpp - Implementación del cargador de YAML del postpro.
|
||||
|
||||
#include "core/config/postfx_config.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace Config::PostFx {
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper: lee `key` en `node` solo si existe; deja `dst` intacto en caso
|
||||
// contrario. Así, un YAML parcial sigue funcionando con los defaults del
|
||||
// struct para los campos que falten.
|
||||
template <typename T>
|
||||
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||
if (node.contains(key)) {
|
||||
dst = node[key].get_value<T>();
|
||||
}
|
||||
}
|
||||
|
||||
// Lee un array RGB [r, g, b] (0..255) y lo normaliza a [0..1] sobre tres
|
||||
// destinos floats. Si la clave no existe o no es secuencia de 3, deja los
|
||||
// destinos como están.
|
||||
void readRgb255(const fkyaml::node& node, const char* key,
|
||||
float& dst_r, float& dst_g, float& dst_b) {
|
||||
if (!node.contains(key)) {
|
||||
return;
|
||||
}
|
||||
const auto& arr = node[key];
|
||||
if (!arr.is_sequence() || arr.size() < 3) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const auto R = arr[0].get_value<int>();
|
||||
const auto G = arr[1].get_value<int>();
|
||||
const auto B = arr[2].get_value<int>();
|
||||
dst_r = static_cast<float>(R) / 255.0F;
|
||||
dst_g = static_cast<float>(G) / 255.0F;
|
||||
dst_b = static_cast<float>(B) / 255.0F;
|
||||
} catch (...) {
|
||||
// Mantiene los defaults si algún elemento no es entero.
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto load(const std::string& path) -> Rendering::GPU::PostFxParams {
|
||||
Rendering::GPU::PostFxParams params{}; // valores por defecto del struct
|
||||
|
||||
auto bytes = Resource::Helper::loadFile(path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "[PostFxConfig] No se pudo cargar " << path
|
||||
<< " — usando defaults built-in\n";
|
||||
return params;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||
const auto* end = begin + bytes.size();
|
||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||
|
||||
if (yaml.contains("bloom") && yaml["bloom"].is_mapping()) {
|
||||
const auto& node = yaml["bloom"];
|
||||
readField(node, "enabled", params.bloom_enabled);
|
||||
readField(node, "intensity", params.bloom_intensity);
|
||||
readField(node, "threshold", params.bloom_threshold);
|
||||
readField(node, "radius_px", params.bloom_radius_px);
|
||||
}
|
||||
|
||||
if (yaml.contains("flicker") && yaml["flicker"].is_mapping()) {
|
||||
const auto& node = yaml["flicker"];
|
||||
readField(node, "enabled", params.flicker_enabled);
|
||||
readField(node, "amplitude", params.flicker_amplitude);
|
||||
readField(node, "frequency_hz", params.flicker_frequency_hz);
|
||||
}
|
||||
|
||||
if (yaml.contains("background") && yaml["background"].is_mapping()) {
|
||||
const auto& node = yaml["background"];
|
||||
readField(node, "enabled", params.background_enabled);
|
||||
readRgb255(node, "color_min",
|
||||
params.background_min_r,
|
||||
params.background_min_g,
|
||||
params.background_min_b);
|
||||
readRgb255(node, "color_max",
|
||||
params.background_max_r,
|
||||
params.background_max_g,
|
||||
params.background_max_b);
|
||||
readField(node, "pulse_frequency_hz", params.background_pulse_freq_hz);
|
||||
}
|
||||
|
||||
std::cout << "[PostFxConfig] Cargado " << path
|
||||
<< " (bloom=" << (params.bloom_enabled ? "on" : "off")
|
||||
<< " intensity=" << params.bloom_intensity
|
||||
<< ", flicker=" << (params.flicker_enabled ? "on" : "off")
|
||||
<< " amp=" << params.flicker_amplitude
|
||||
<< ", bg=" << (params.background_enabled ? "on" : "off")
|
||||
<< ")\n";
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "[PostFxConfig] Error parseando " << path << ": " << e.what()
|
||||
<< " — usando defaults built-in\n";
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
} // namespace Config::PostFx
|
||||
@@ -0,0 +1,21 @@
|
||||
// postfx_config.hpp - Carga de los parámetros del shader de postpro desde YAML.
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Lee `config/postfx.yaml` (dentro de resources.pack) y devuelve un struct
|
||||
// PostFxParams listo para pasar a GpuFrameRenderer::setPostFx(). Si el YAML
|
||||
// no existe o falla el parser, retorna los defaults built-in.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||||
|
||||
namespace Config::PostFx {
|
||||
|
||||
// Carga desde el resource pack. Path relativo dentro del pack (p.ej.
|
||||
// "config/postfx.yaml"). Si falla, devuelve un PostFxParams construido por
|
||||
// defecto (valores embebidos en el struct).
|
||||
[[nodiscard]] auto load(const std::string& path) -> Rendering::GPU::PostFxParams;
|
||||
|
||||
} // namespace Config::PostFx
|
||||
+120
-125
@@ -8,38 +8,38 @@
|
||||
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;
|
||||
constexpr int WIDTH = 1280;
|
||||
constexpr int HEIGHT = 720;
|
||||
constexpr int MIN_WIDTH = 640; // Mínimo: mitad del baseline (16:9)
|
||||
constexpr int MIN_HEIGHT = 360;
|
||||
// Zoom system
|
||||
constexpr float BASE_ZOOM = 1.0F; // 640x480 baseline
|
||||
constexpr float MIN_ZOOM = 0.5F; // 320x240 minimum
|
||||
constexpr float BASE_ZOOM = 1.0F; // 1280x720 baseline (16:9)
|
||||
constexpr float MIN_ZOOM = 0.5F; // 640x360 minimum
|
||||
constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2)
|
||||
constexpr bool FULLSCREEN = true; // Pantalla completa activadapor defecto
|
||||
constexpr bool FULLSCREEN = true; // Pantalla completa activada por defecto
|
||||
} // namespace Window
|
||||
|
||||
// Dimensions base del joc (coordenades lògiques)
|
||||
// Dimensiones base del juego (coordenadas lógicas, 16:9)
|
||||
namespace Game {
|
||||
constexpr int WIDTH = 640;
|
||||
constexpr int HEIGHT = 480;
|
||||
constexpr int WIDTH = 1280;
|
||||
constexpr int HEIGHT = 720;
|
||||
} // namespace Game
|
||||
|
||||
// Zones del joc (SDL_FRect amb càlculs automàtics basat en percentatges)
|
||||
// Zones del juego (SDL_FRect con cálculos automáticos basat en porcentajes)
|
||||
namespace Zones {
|
||||
// --- CONFIGURACIÓ DE PORCENTATGES ---
|
||||
// Totes les zones definides com a percentatges de Game::WIDTH (640) i Game::HEIGHT (480)
|
||||
// Todas las zones definides como a porcentajes de Game::WIDTH (640) i Game::HEIGHT (480)
|
||||
|
||||
// Percentatges d'alçada (divisió vertical)
|
||||
// Percentatges de height (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)
|
||||
// Padding horizontal para PLAYAREA (dentro 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
|
||||
// Cálculos automáticos a partir dels porcentajes
|
||||
|
||||
// Alçades
|
||||
constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT;
|
||||
@@ -56,44 +56,44 @@ constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_P
|
||||
|
||||
// --- ZONES FINALS (SDL_FRect) ---
|
||||
|
||||
// Marcador superior (reservat per a futur ús)
|
||||
// Ocupa: 10% superior (0-48px)
|
||||
// Marcador superior (reservado para futuro uso)
|
||||
// Ocupa el 2% superior
|
||||
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
|
||||
static_cast<float>(Game::WIDTH), // ancho completo
|
||||
SCOREBOARD_TOP_H // alto
|
||||
};
|
||||
|
||||
// Àrea de joc principal (contenidor del 80% central, sense padding)
|
||||
// Ocupa: 10-90% (48-432px), ample complet
|
||||
// Área de juego principal (contenedor del 80% central, sin padding)
|
||||
// Ocupa el 88% central, ancho completo
|
||||
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
|
||||
MAIN_PLAYAREA_Y, // debajo del scoreboard superior
|
||||
static_cast<float>(Game::WIDTH), // ancho completo
|
||||
MAIN_PLAYAREA_H // alto
|
||||
};
|
||||
|
||||
// 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
|
||||
// Zona de juego real (con padding horizontal del 5%)
|
||||
// Ocupa: dentro de MAIN_PLAYAREA, con márgenes laterales
|
||||
// Se utiliza para límites del juego, colisiones, 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)
|
||||
PLAYAREA_PADDING_H, // padding horizontal
|
||||
MAIN_PLAYAREA_Y, // debajo del scoreboard superior (igual que MAIN_PLAYAREA)
|
||||
Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // ancho con padding
|
||||
MAIN_PLAYAREA_H // alto (igual que MAIN_PLAYAREA)
|
||||
};
|
||||
|
||||
// Marcador inferior (marcador actual)
|
||||
// Ocupa: 10% inferior (432-480px)
|
||||
// Ocupa el 10% inferior
|
||||
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
|
||||
SCOREBOARD_BOTTOM_Y, // fondo
|
||||
static_cast<float>(Game::WIDTH), // ancho completo
|
||||
SCOREBOARD_BOTTOM_H // alto
|
||||
};
|
||||
|
||||
// Padding horizontal del marcador (per alinear zones esquerra/dreta amb PLAYAREA)
|
||||
// Padding horizontal del marcador (para alinear zonas izquierda/derecha con PLAYAREA)
|
||||
constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f;
|
||||
} // namespace Zones
|
||||
|
||||
@@ -101,14 +101,24 @@ constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f;
|
||||
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)
|
||||
// Paleta semántica por tipo de entidad. Si una entity declara color, lo
|
||||
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se
|
||||
// usa el color global del oscilador (g_current_line_color).
|
||||
namespace Palette {
|
||||
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
||||
constexpr SDL_Color BULLET = {.r = 120, .g = 255, .b = 140, .a = 255}; // Verde laser
|
||||
constexpr SDL_Color PENTAGON = {.r = 120, .g = 170, .b = 255, .a = 255}; // Azul "esquivador"
|
||||
constexpr SDL_Color QUADRAT = {.r = 255, .g = 110, .b = 110, .a = 255}; // Rojo "tank"
|
||||
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 130, .b = 255, .a = 255}; // Magenta agresivo
|
||||
} // namespace Palette
|
||||
|
||||
// Ship (nave del player)
|
||||
namespace Ship {
|
||||
// Invulnerabilidad post-respawn
|
||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
|
||||
@@ -144,7 +154,7 @@ constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0F; // 0.0 = sin typewriter (d
|
||||
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)
|
||||
// RECT (rectángulo de márgenes)
|
||||
constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F;
|
||||
constexpr float INIT_HUD_RECT_RATIO_END = 0.85F;
|
||||
|
||||
@@ -152,11 +162,11 @@ constexpr float INIT_HUD_RECT_RATIO_END = 0.85F;
|
||||
constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F;
|
||||
constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F;
|
||||
|
||||
// SHIP1 (nave jugador 1)
|
||||
// SHIP1 (nave player 1)
|
||||
constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F;
|
||||
constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F;
|
||||
|
||||
// SHIP2 (nave jugador 2)
|
||||
// SHIP2 (nave player 2)
|
||||
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
|
||||
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F;
|
||||
|
||||
@@ -212,21 +222,21 @@ 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 VELOCITAT_BASE = 80.0F; // Velocidad 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 ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||
constexpr float ROTACIO_MAX = 0.3F; // Rotación 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)
|
||||
// Herència de velocity 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
|
||||
// Angular velocity sin 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)
|
||||
@@ -238,59 +248,46 @@ 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)
|
||||
// La antigua oscilación CPU (namespace Color) se ha migrado al shader de
|
||||
// postpro. Los parámetros de flicker / background pulse viven ahora en
|
||||
// data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl.
|
||||
|
||||
// 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)
|
||||
// Brillantor (control de intensitat per cada type de entidad)
|
||||
namespace Brightness {
|
||||
// Brillantor estàtica per entitats de joc (0.0-1.0)
|
||||
constexpr float NAU = 1.0F; // Màxima visibilitat (jugador)
|
||||
// Brillantor estàtica per entidades de juego (0.0-1.0)
|
||||
constexpr float NAU = 1.0F; // Màxima visibilitat (player)
|
||||
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)
|
||||
// Starfield: gradient segons distancia al centro
|
||||
// distancia_centre: 0.0 (centro) → 1.0 (vora pantalla)
|
||||
// brightness = MIN + (MAX - MIN) * distancia_centre
|
||||
constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centre)
|
||||
constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centro)
|
||||
constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla)
|
||||
} // namespace Brightness
|
||||
|
||||
// Renderització (V-Sync i altres opcions de render)
|
||||
// Renderització (V-Sync i altres opciones de render)
|
||||
namespace Rendering {
|
||||
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
|
||||
} // namespace Rendering
|
||||
|
||||
// Audio (sistema de so i música)
|
||||
// Audio (sistema de sonido y música) — usado por Audio::Config en init()
|
||||
namespace Audio {
|
||||
constexpr float VOLUME = 1.0F; // Volumen maestro (0.0 a 1.0)
|
||||
constexpr bool ENABLED = true; // Audio habilitado por defecto
|
||||
constexpr float VOLUME = 1.0F; // Volumen maestro (0..1)
|
||||
constexpr bool MUSIC_ENABLED = true; // Música habilitada
|
||||
constexpr float MUSIC_VOLUME = 0.8F; // Volumen música (0..1)
|
||||
constexpr bool SOUND_ENABLED = true; // Efectos habilitados
|
||||
constexpr float SOUND_VOLUME = 1.0F; // Volumen efectos (0..1)
|
||||
constexpr float VOLUME_STEP = 0.05F; // Paso UI (5%)
|
||||
constexpr int FREQUENCY = 48000; // Frecuencia de muestreo (Hz)
|
||||
constexpr int CROSSFADE_MS = 1500; // Crossfade por defecto entre pistas (ms)
|
||||
constexpr SDL_AudioFormat FORMAT = SDL_AUDIO_S16; // PCM 16-bit signed nativo
|
||||
constexpr int CHANNELS = 2; // Estéreo
|
||||
} // 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
|
||||
@@ -298,8 +295,6 @@ constexpr int FADE_DURATION_MS = 1000; // Fade out duration
|
||||
|
||||
// Efectes de so (sons puntuals)
|
||||
namespace Sound {
|
||||
constexpr float VOLUME = 1.0F; // Volumen efectos
|
||||
constexpr bool ENABLED = true; // Sonidos habilitados
|
||||
constexpr const char* 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
|
||||
@@ -307,7 +302,7 @@ constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; //
|
||||
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* START = "effects/start.wav"; // El player pulsa START
|
||||
constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
|
||||
} // namespace Sound
|
||||
|
||||
@@ -328,7 +323,7 @@ constexpr SDL_Keycode SHOOT = SDLK_LSHIFT;
|
||||
} // namespace P2
|
||||
} // namespace Controls
|
||||
|
||||
// Enemy type configuration (tipus d'enemics)
|
||||
// Enemy type configuration (type de enemigos)
|
||||
namespace Enemies {
|
||||
// Pentagon (esquivador - zigzag evasion)
|
||||
namespace Pentagon {
|
||||
@@ -340,15 +335,15 @@ 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 {
|
||||
// Cuadrado (perseguidor - tracks player)
|
||||
namespace Cuadrado {
|
||||
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
|
||||
} // namespace Cuadrado
|
||||
|
||||
// Molinillo (agressiu - fast straight lines, proximity spin-up)
|
||||
namespace Molinillo {
|
||||
@@ -396,54 +391,54 @@ 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)
|
||||
// Scoring system (puntuación per type de enemy)
|
||||
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 PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
|
||||
constexpr int QUADRAT_SCORE = 150; // Cuadrado (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)
|
||||
// Title scene ship animations (naves 3D flotantes a l'escena de título)
|
||||
namespace Title {
|
||||
namespace Ships {
|
||||
// ============================================================
|
||||
// PARÀMETRES BASE (ajustar aquí per experimentar)
|
||||
// ============================================================
|
||||
|
||||
// 1. Escala global de les naus
|
||||
// 1. Escala global de las naves
|
||||
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
|
||||
// 3. Radio orbital (distance radial desde centro en coordenadas polares)
|
||||
constexpr float CLOCK_RADIUS = 150.0F; // Distancia des del centro
|
||||
|
||||
// 4. Ángulos de posición (clock positions en coordenadas polares)
|
||||
// En coordenades de pantalla: 0° = dreta, 90° = baix, 180° = esquerra, 270° = dalt
|
||||
// En coordenadas de pantalla: 0° = derecha, 90° = baix, 180° = izquierda, 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)
|
||||
// 5. Radio máximo de la shape 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)
|
||||
// VALORS DERIVATS (calculats automáticoament - 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
|
||||
// Centro de la pantalla (point de referència)
|
||||
constexpr float CENTER_X = Game::WIDTH / 2.0F; // auto-derivado de Game::WIDTH
|
||||
constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // auto-derivado de Game::HEIGHT
|
||||
|
||||
// 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)
|
||||
// Posicions target (calculades dinàmicament des dels parámetros base)
|
||||
// Nota: std::cos/sin no són constexpr en C++20, pero funcionen en runtime
|
||||
// Les funciones inline són optimitzades por el compilador (zero overhead)
|
||||
inline float P1_TARGET_X() {
|
||||
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE));
|
||||
}
|
||||
@@ -457,27 +452,27 @@ 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
|
||||
// Escales de animación (relatives a SHIP_BASE_SCALE)
|
||||
constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més grande
|
||||
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base
|
||||
|
||||
// Offset d'entrada (ajustat automàticament a l'escala)
|
||||
// Fórmula: (radi màxim de la nau * escala d'entrada) + marge
|
||||
// Offset de entrada (ajustat automáticoament a l'scale)
|
||||
// Fórmula: (radi màxim de la ship * scale de entrada) + margen
|
||||
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
|
||||
// Vec2 de fuga (centro para l'animación de salida)
|
||||
constexpr float VANISHING_POINT_X = CENTER_X; // auto-derivado de Game::WIDTH
|
||||
constexpr float VANISHING_POINT_Y = CENTER_Y; // auto-derivado de Game::HEIGHT
|
||||
|
||||
// ============================================================
|
||||
// ANIMACIONS (durades, oscil·lacions, delays)
|
||||
// ============================================================
|
||||
|
||||
// Durades d'animació
|
||||
// Durades de animación
|
||||
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
|
||||
constexpr float EXIT_DURATION = 1.0F; // Sortida (segons)
|
||||
constexpr float EXIT_DURATION = 1.0F; // Salida (segons)
|
||||
|
||||
// Flotació (oscil·lació reduïda i diferenciada per nau)
|
||||
// Flotació (oscil·lació reduïda y diferenciada per ship)
|
||||
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
|
||||
constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels)
|
||||
|
||||
@@ -486,21 +481,21 @@ 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)
|
||||
// Delays de 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
|
||||
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s despué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
|
||||
// Delay global antes de start l'animación de entrada al state MAIN
|
||||
constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin
|
||||
|
||||
// Multiplicadors de freqüència per a cada nau (variació sutil ±12%)
|
||||
// Multiplicadors de freqüència para cada ship (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)
|
||||
// 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
|
||||
@@ -509,21 +504,21 @@ constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
|
||||
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
|
||||
// Factors de scale
|
||||
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)
|
||||
// Espaiat entre caràcters (usado per VectorText)
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
} // namespace Layout
|
||||
} // namespace Title
|
||||
|
||||
// Floating score numbers (números flotants de puntuació)
|
||||
// Floating score numbers (números flotantes de puntuación)
|
||||
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 VELOCITY_Y = -30.0F; // Velocidad vertical (px/s, negatiu = amunt)
|
||||
constexpr float VELOCITY_X = 0.0F; // Velocidad 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)
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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
|
||||
@@ -0,0 +1,72 @@
|
||||
// entity.hpp - Clase base abstracta para todas las entidades del juego
|
||||
// © 2025 Orni Attack - Arquitectura de entidades
|
||||
//
|
||||
// Cada Entity incluye un Physics::RigidBody como member. Las entidades que
|
||||
// se simulen físicamente lo configuran en init() y registran en el
|
||||
// PhysicsWorld del GameScene. Las que no, ignoran el body (queda con
|
||||
// defaults inocuos: mass=1, radius=0).
|
||||
//
|
||||
// Flujo por frame (gestionado por GameScene):
|
||||
// 1. entity.update(dt) — aplicar fuerzas, decidir lógica
|
||||
// 2. world.update(dt) — integrar bodies, resolver colisiones
|
||||
// 3. entity.postUpdate(dt) — sincronizar mirror (center_, angle_)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "core/graphics/shape.hpp"
|
||||
#include "core/physics/rigid_body.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Entities {
|
||||
|
||||
class Entity {
|
||||
public:
|
||||
virtual ~Entity() = default;
|
||||
|
||||
// Interfaz principal (virtual pur)
|
||||
virtual void init() = 0;
|
||||
virtual void update(float delta_time) = 0;
|
||||
virtual void draw() const = 0;
|
||||
[[nodiscard]] virtual bool isActive() const = 0;
|
||||
|
||||
// Sincronización post-física (override opcional).
|
||||
// Llamado por GameScene tras world.update(). Default: no-op.
|
||||
virtual void postUpdate(float /*delta_time*/) {}
|
||||
|
||||
// Interfaz de colisión (override opcional)
|
||||
[[nodiscard]] virtual float getCollisionRadius() const { return 0.0F; }
|
||||
[[nodiscard]] virtual bool isCollidable() const { return false; }
|
||||
|
||||
// Getters comunes (inline, sin overhead)
|
||||
[[nodiscard]] const Vec2& getCenter() const { return center_; }
|
||||
[[nodiscard]] float getAngle() const { return angle_; }
|
||||
[[nodiscard]] float getBrightness() const { return brightness_; }
|
||||
[[nodiscard]] const std::shared_ptr<Graphics::Shape>& getShape() const { return shape_; }
|
||||
|
||||
// Acceso al cuerpo físico (Fase 6+). El PhysicsWorld lo registra
|
||||
// por puntero; la entidad lo configura en init().
|
||||
[[nodiscard]] auto getBody() -> Physics::RigidBody& { return body_; }
|
||||
[[nodiscard]] auto getBody() const -> const Physics::RigidBody& { return body_; }
|
||||
|
||||
protected:
|
||||
// Estado común (acceso directo, sin overhead)
|
||||
Rendering::Renderer* renderer_;
|
||||
std::shared_ptr<Graphics::Shape> shape_;
|
||||
Vec2 center_;
|
||||
float angle_{0.0F};
|
||||
float brightness_{1.0F};
|
||||
|
||||
// Cuerpo físico (Fase 6). Las entidades que se mueven por
|
||||
// física actualizan center_/angle_ en postUpdate() desde body_.
|
||||
Physics::RigidBody body_;
|
||||
|
||||
// Constructor protegido (clase abstracta)
|
||||
Entity(Rendering::Renderer* renderer = nullptr)
|
||||
: renderer_(renderer),
|
||||
center_({.x = 0.0F, .y = 0.0F}) {}
|
||||
};
|
||||
|
||||
} // namespace Entities
|
||||
@@ -1,5 +1,5 @@
|
||||
// shape.cpp - Implementació del sistema de formes vectorials
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "core/graphics/shape.hpp"
|
||||
|
||||
@@ -11,31 +11,31 @@
|
||||
namespace Graphics {
|
||||
|
||||
Shape::Shape(const std::string& filepath)
|
||||
: centre_({.x = 0.0F, .y = 0.0F}),
|
||||
: center_({.x = 0.0F, .y = 0.0F}),
|
||||
escala_defecte_(1.0F),
|
||||
nom_("unnamed") {
|
||||
carregar(filepath);
|
||||
load(filepath);
|
||||
}
|
||||
|
||||
bool Shape::carregar(const std::string& filepath) {
|
||||
// Llegir fitxer
|
||||
bool Shape::load(const std::string& filepath) {
|
||||
// Llegir file
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Llegir tot el contingut
|
||||
// Llegir todo el contingut
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string contingut = buffer.str();
|
||||
file.close();
|
||||
|
||||
// Parsejar
|
||||
return parsejar_fitxer(contingut);
|
||||
return parseFile(contingut);
|
||||
}
|
||||
|
||||
bool Shape::parsejar_fitxer(const std::string& contingut) {
|
||||
bool Shape::parseFile(const std::string& contingut) {
|
||||
std::istringstream iss(contingut);
|
||||
std::string line;
|
||||
|
||||
@@ -55,7 +55,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
|
||||
try {
|
||||
escala_defecte_ = std::stof(extract_value(line));
|
||||
} catch (...) {
|
||||
std::cerr << "[Shape] Warning: escala invàlida, usant 1.0" << '\n';
|
||||
std::cerr << "[Shape] Warning: scale invàlida, usant 1.0" << '\n';
|
||||
escala_defecte_ = 1.0F;
|
||||
}
|
||||
} else if (starts_with(line, "center:")) {
|
||||
@@ -65,7 +65,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
|
||||
if (points.size() >= 2) {
|
||||
primitives_.push_back({PrimitiveType::POLYLINE, points});
|
||||
} else {
|
||||
std::cerr << "[Shape] Warning: polyline amb menys de 2 punts ignorada"
|
||||
std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada"
|
||||
<< '\n';
|
||||
}
|
||||
} else if (starts_with(line, "line:")) {
|
||||
@@ -73,7 +73,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
|
||||
if (points.size() == 2) {
|
||||
primitives_.push_back({PrimitiveType::LINE, points});
|
||||
} else {
|
||||
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 punts"
|
||||
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points"
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
|
||||
}
|
||||
|
||||
if (primitives_.empty()) {
|
||||
std::cerr << "[Shape] Error: cap primitiva carregada" << '\n';
|
||||
std::cerr << "[Shape] Error: sin primitiva carregada" << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -124,18 +124,18 @@ void Shape::parse_center(const std::string& 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)));
|
||||
center_.x = std::stof(trim(val.substr(0, comma)));
|
||||
center_.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};
|
||||
std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n';
|
||||
center_ = {.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::vector<Vec2> Shape::parse_points(const std::string& str) const {
|
||||
std::vector<Vec2> points;
|
||||
std::istringstream iss(trim(str));
|
||||
std::string pair;
|
||||
|
||||
@@ -147,7 +147,7 @@ std::vector<Punt> Shape::parse_points(const std::string& str) const {
|
||||
float y = std::stof(pair.substr(comma + 1));
|
||||
points.push_back({x, y});
|
||||
} catch (...) {
|
||||
std::cerr << "[Shape] Warning: punt invàlid ignorat: " << pair
|
||||
std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// shape.hpp - Sistema de formes vectorials
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -10,38 +10,38 @@
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
// Tipus de primitiva dins d'una forma
|
||||
// Tipo de primitiva dins de una shape
|
||||
enum class PrimitiveType {
|
||||
POLYLINE, // Seqüència de punts connectats
|
||||
LINE // Línia individual (2 punts)
|
||||
POLYLINE, // Secuencia de points connectats
|
||||
LINE // Línia individual (2 points)
|
||||
};
|
||||
|
||||
// Primitiva individual (polyline o line)
|
||||
struct ShapePrimitive {
|
||||
PrimitiveType type;
|
||||
std::vector<Punt> points; // 2+ punts per polyline, exactament 2 per line
|
||||
std::vector<Vec2> points; // 2+ points per polyline, exactament 2 per line
|
||||
};
|
||||
|
||||
// Classe Shape - representa una forma vectorial carregada des de .shp
|
||||
// Clase Shape - representa una shape vectorial carregada desde .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);
|
||||
// Carregar shape desde file .shp
|
||||
bool load(const std::string& filepath);
|
||||
|
||||
// Parsejar forma des de buffer de memòria (per al sistema de recursos)
|
||||
bool parsejar_fitxer(const std::string& contingut);
|
||||
// Parsejar shape desde buffer de memòria (per al sistema de recursos)
|
||||
bool parseFile(const std::string& contingut);
|
||||
|
||||
// Getters
|
||||
[[nodiscard]] const std::vector<ShapePrimitive>& get_primitives() const {
|
||||
return primitives_;
|
||||
}
|
||||
[[nodiscard]] const Punt& get_centre() const { return centre_; }
|
||||
[[nodiscard]] const Vec2& getCenter() const { return center_; }
|
||||
[[nodiscard]] float get_escala_defecte() const { return escala_defecte_; }
|
||||
[[nodiscard]] bool es_valida() const { return !primitives_.empty(); }
|
||||
[[nodiscard]] bool isValid() const { return !primitives_.empty(); }
|
||||
|
||||
// Info de depuració
|
||||
[[nodiscard]] std::string get_nom() const { return nom_; }
|
||||
@@ -49,16 +49,16 @@ class Shape {
|
||||
|
||||
private:
|
||||
std::vector<ShapePrimitive> primitives_;
|
||||
Punt centre_; // Centre/origen de la forma
|
||||
Vec2 center_; // Centro/origin de la shape
|
||||
float escala_defecte_; // Escala per defecte (normalment 1.0)
|
||||
std::string nom_; // Nom de la forma (per depuració)
|
||||
std::string nom_; // Nom de la shape (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;
|
||||
[[nodiscard]] std::vector<Vec2> parse_points(const std::string& str) const;
|
||||
};
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// shape_loader.cpp - Implementació del carregador amb caché
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// shape_loader.cpp - Implementació del carregador con caché
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "core/graphics/shape_loader.hpp"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
// Inicialització de variables estàtiques
|
||||
// Inicialización de variables estàtiques
|
||||
std::unordered_map<std::string, std::shared_ptr<Shape>> ShapeLoader::cache_;
|
||||
std::string ShapeLoader::base_path_ = "data/shapes/";
|
||||
|
||||
@@ -32,7 +32,7 @@ std::shared_ptr<Shape> ShapeLoader::load(const std::string& filename) {
|
||||
// Load from resource system
|
||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
||||
if (data.empty()) {
|
||||
std::cerr << "[ShapeLoader] Error: no s'ha pogut carregar " << normalized
|
||||
std::cerr << "[ShapeLoader] Error: no s'ha pogut load " << normalized
|
||||
<< '\n';
|
||||
return nullptr;
|
||||
}
|
||||
@@ -40,15 +40,15 @@ std::shared_ptr<Shape> ShapeLoader::load(const std::string& filename) {
|
||||
// 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)) {
|
||||
if (!shape->parseFile(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';
|
||||
if (!shape->isValid()) {
|
||||
std::cerr << "[ShapeLoader] Error: shape invàlida " << normalized << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -69,17 +69,17 @@ void ShapeLoader::clear_cache() {
|
||||
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
|
||||
// Si es un path absolut (comença con '/'), usar-lo directament
|
||||
if (!filename.empty() && filename[0] == '/') {
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Si ja conté el prefix base_path, usar-lo directament
|
||||
// Si ya conté el prefix base_path, usar-lo directament
|
||||
if (filename.starts_with(base_path_)) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Altrament, afegir base_path (ara suporta subdirectoris)
|
||||
// Altrament, añadir base_path (ara suporta subdirectoris)
|
||||
return base_path_ + filename;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// shape_loader.hpp - Carregador estàtic de formes amb caché
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// shape_loader.hpp - Carregador estàtic de formes con caché
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
// Carregador estàtic de formes amb caché
|
||||
// Carregador estàtic de formes con caché
|
||||
class ShapeLoader {
|
||||
public:
|
||||
// No instanciable (tot estàtic)
|
||||
ShapeLoader() = delete;
|
||||
|
||||
// Carregar forma des de fitxer (amb caché)
|
||||
// Carregar shape desde file (con 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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// starfield.cpp - Implementació del sistema d'estrelles de fons
|
||||
// starfield.cpp - Implementació del sistema de estrelles de fons
|
||||
// © 2025 Orni Attack
|
||||
|
||||
#include "core/graphics/starfield.hpp"
|
||||
@@ -14,38 +14,38 @@
|
||||
namespace Graphics {
|
||||
|
||||
// Constructor
|
||||
Starfield::Starfield(SDL_Renderer* renderer,
|
||||
const Punt& punt_fuga,
|
||||
Starfield::Starfield(Rendering::Renderer* renderer,
|
||||
const Vec2& punt_fuga,
|
||||
const SDL_FRect& area,
|
||||
int densitat)
|
||||
: renderer_(renderer),
|
||||
punt_fuga_(punt_fuga),
|
||||
area_(area),
|
||||
densitat_(densitat) {
|
||||
// Carregar forma d'estrella amb ShapeLoader
|
||||
// Carregar shape de estrella con 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';
|
||||
if (!shape_estrella_ || !shape_estrella_->isValid()) {
|
||||
std::cerr << "ERROR: No s'ha pogut load star.shp" << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// Configurar 3 capes amb diferents velocitats i escales
|
||||
// Capa 0: Fons llunyà (lenta, petita)
|
||||
// Configurar 3 capes con diferents velocitats i escales
|
||||
// Capa 0: Fons llunyà (lenta, pequeña)
|
||||
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)
|
||||
// Capa 2: Primer pla (ràpida, grande)
|
||||
capes_.push_back({80.0F, 0.8F, 2.0F, densitat / 3});
|
||||
|
||||
// Calcular radi màxim (distància del centre al racó més llunyà)
|
||||
// Calcular radi màxim (distancia del centro 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)
|
||||
// Inicialitzar estrelles con 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++) {
|
||||
@@ -55,55 +55,55 @@ Starfield::Starfield(SDL_Renderer* renderer,
|
||||
// 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
|
||||
// Distancia aleatòria (0.0 a 1.0) per omplir toda la pantalla
|
||||
estrella.distancia_centre = static_cast<float>(rand()) / RAND_MAX;
|
||||
|
||||
// Calcular posició des de la distància
|
||||
// Calcular posición desde la distancia
|
||||
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));
|
||||
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
|
||||
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle));
|
||||
|
||||
estrelles_.push_back(estrella);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inicialitzar una estrella (nova o regenerada)
|
||||
// Inicialitzar una estrella (nueva o regenerada)
|
||||
void Starfield::inicialitzar_estrella(Estrella& estrella) const {
|
||||
// Angle aleatori des del punt de fuga cap a fora
|
||||
// Angle aleatori des del point de fuga hacia fuera
|
||||
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
|
||||
// Distancia inicial pequeña (5% del radi màxim) - neix prop del centro
|
||||
estrella.distancia_centre = 0.05F;
|
||||
|
||||
// Posició inicial: molt prop del punt de fuga
|
||||
// Posición inicial: mucho prop del point 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));
|
||||
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
|
||||
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle));
|
||||
}
|
||||
|
||||
// Verificar si una estrella està fora de l'àrea
|
||||
// Verificar si una estrella está fuera 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);
|
||||
return (estrella.position.x < area_.x ||
|
||||
estrella.position.x > area_.x + area_.w ||
|
||||
estrella.position.y < area_.y ||
|
||||
estrella.position.y > area_.y + area_.h);
|
||||
}
|
||||
|
||||
// Calcular escala dinàmica segons distància del centre
|
||||
// Calcular scale dinàmica segons distancia del centro
|
||||
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)
|
||||
// Interpolació lineal basada en distancia del centro
|
||||
// distancia_centre: 0.0 (centro) → 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
|
||||
// Calcular brightness dinàmica segons distancia del centro
|
||||
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)
|
||||
// distancia_centre: 0.0 (centro, llunyanes) → 1.0 (vora, properes)
|
||||
float brightness_base = Defaults::Brightness::STARFIELD_MIN +
|
||||
((Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) *
|
||||
estrella.distancia_centre);
|
||||
@@ -112,23 +112,23 @@ float Starfield::calcular_brightness(const Estrella& estrella) const {
|
||||
return std::min(1.0F, brightness_base * multiplicador_brightness_);
|
||||
}
|
||||
|
||||
// Actualitzar posicions de les estrelles
|
||||
void Starfield::actualitzar(float delta_time) {
|
||||
// Actualitzar posicions de las estrelles
|
||||
void Starfield::update(float delta_time) {
|
||||
for (auto& estrella : estrelles_) {
|
||||
// Obtenir configuració de la capa
|
||||
// Obtenir configuración 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;
|
||||
// Moure hacia fuera des del centro
|
||||
float velocity = capa.velocitat_base;
|
||||
float dx = velocity * std::cos(estrella.angle) * delta_time;
|
||||
float dy = velocity * std::sin(estrella.angle) * delta_time;
|
||||
|
||||
estrella.posicio.x += dx;
|
||||
estrella.posicio.y += dy;
|
||||
estrella.position.x += dx;
|
||||
estrella.position.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;
|
||||
// Actualitzar distancia del centro
|
||||
float dx_centre = estrella.position.x - punt_fuga_.x;
|
||||
float dy_centre = estrella.position.y - punt_fuga_.y;
|
||||
float dist_px = std::sqrt((dx_centre * dx_centre) + (dy_centre * dy_centre));
|
||||
estrella.distancia_centre = dist_px / radi_max_;
|
||||
|
||||
@@ -144,26 +144,25 @@ 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()) {
|
||||
// Dibuixar todas las estrelles
|
||||
void Starfield::draw() {
|
||||
if (!shape_estrella_->isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& estrella : estrelles_) {
|
||||
// Calcular escala i brightness dinàmicament
|
||||
float escala = calcular_escala(estrella);
|
||||
// Calcular scale i brightness dinàmicament
|
||||
float scale = calcular_escala(estrella);
|
||||
float brightness = calcular_brightness(estrella);
|
||||
|
||||
// Renderitzar estrella sense rotació
|
||||
// Renderizar estrella sin rotación
|
||||
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)
|
||||
estrella.position,
|
||||
0.0F, // angle (las estrelles no giren)
|
||||
scale, // scale dinàmica
|
||||
1.0F, // progress (siempre visible)
|
||||
brightness // brightness dinàmica
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// starfield.hpp - Sistema d'estrelles de fons amb efecte de profunditat
|
||||
// starfield.hpp - Sistema de estrelles de fons con efecte de profunditat
|
||||
// © 2025 Orni Attack
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
@@ -13,70 +15,70 @@
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
// Configuració per cada capa de profunditat
|
||||
// Configuración 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 velocitat_base; // Velocidad base de esta capa (px/s)
|
||||
float escala_min; // Escala mínima prop del centro
|
||||
float escala_max; // Escala màxima al límit de pantalla
|
||||
int num_estrelles; // Nombre d'estrelles en aquesta capa
|
||||
int num_estrelles; // Nombre de estrelles en esta capa
|
||||
};
|
||||
|
||||
// Classe Starfield - camp d'estrelles animat amb efecte de profunditat
|
||||
// Clase Starfield - camp de estrelles animat con 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,
|
||||
// - punt_fuga: point de origin/fuga des de on surten las estrelles
|
||||
// - area: rectangle on actuen las estrelles (SDL_FRect)
|
||||
// - densitat: nombre total de estrelles (es divideix entre capes)
|
||||
Starfield(Rendering::Renderer* renderer,
|
||||
const Vec2& punt_fuga,
|
||||
const SDL_FRect& area,
|
||||
int densitat = 150);
|
||||
|
||||
// Actualitzar posicions de les estrelles
|
||||
void actualitzar(float delta_time);
|
||||
// Actualitzar posicions de las estrelles
|
||||
void update(float delta_time);
|
||||
|
||||
// Dibuixar totes les estrelles
|
||||
void dibuixar();
|
||||
// Dibuixar todas las estrelles
|
||||
void draw();
|
||||
|
||||
// Setters per ajustar paràmetres en temps real
|
||||
void set_punt_fuga(const Punt& punt) { punt_fuga_ = punt; }
|
||||
// Setters per ajustar parámetros en time real
|
||||
void set_punt_fuga(const Vec2& point) { punt_fuga_ = point; }
|
||||
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)
|
||||
Vec2 position; // Posición actual
|
||||
float angle; // Angle de movement (radians)
|
||||
float distancia_centre; // Distancia normalitzada del centro (0.0-1.0)
|
||||
int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop)
|
||||
};
|
||||
|
||||
// Inicialitzar una estrella (nova o regenerada)
|
||||
// Inicialitzar una estrella (nueva o regenerada)
|
||||
void inicialitzar_estrella(Estrella& estrella) const;
|
||||
|
||||
// Verificar si una estrella està fora de l'àrea
|
||||
// Verificar si una estrella está fuera de l'àrea
|
||||
[[nodiscard]] bool fora_area(const Estrella& estrella) const;
|
||||
|
||||
// Calcular escala dinàmica segons distància del centre
|
||||
// Calcular scale dinàmica segons distancia del centro
|
||||
[[nodiscard]] float calcular_escala(const Estrella& estrella) const;
|
||||
|
||||
// Calcular brightness dinàmica segons distància del centre
|
||||
// Calcular brightness dinàmica segons distancia del centro
|
||||
[[nodiscard]] float calcular_brightness(const Estrella& estrella) const;
|
||||
|
||||
// Dades
|
||||
std::vector<Estrella> estrelles_;
|
||||
std::vector<CapaConfig> capes_; // Configuració de les 3 capes
|
||||
std::vector<CapaConfig> capes_; // Configuración de las 3 capes
|
||||
std::shared_ptr<Shape> shape_estrella_;
|
||||
SDL_Renderer* renderer_;
|
||||
Rendering::Renderer* renderer_;
|
||||
|
||||
// Configuració
|
||||
Punt punt_fuga_; // Punt d'origen de les estrelles
|
||||
// Configuración
|
||||
Vec2 punt_fuga_; // Vec2 de origin de las 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)
|
||||
float radi_max_; // Distancia màxima del centro al límit de pantalla
|
||||
int densitat_; // Nombre total de estrelles
|
||||
float multiplicador_brightness_{1.0F}; // Multiplicador de brightness (1.0 = default)
|
||||
};
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// vector_text.cpp - Implementació del sistema de text vectorial
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
// Test pre-commit hook
|
||||
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
@@ -11,11 +11,11 @@
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
// Constants per a mides base dels caràcters
|
||||
// Constants para 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)
|
||||
VectorText::VectorText(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {
|
||||
load_charset();
|
||||
}
|
||||
@@ -26,10 +26,10 @@ void VectorText::load_charset() {
|
||||
std::string filename = get_shape_filename(c);
|
||||
auto shape = ShapeLoader::load(filename);
|
||||
|
||||
if (shape && shape->es_valida()) {
|
||||
if (shape && shape->isValid()) {
|
||||
chars_[c] = shape;
|
||||
} else {
|
||||
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
|
||||
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
@@ -39,10 +39,10 @@ void VectorText::load_charset() {
|
||||
std::string filename = get_shape_filename(c);
|
||||
auto shape = ShapeLoader::load(filename);
|
||||
|
||||
if (shape && shape->es_valida()) {
|
||||
if (shape && shape->isValid()) {
|
||||
chars_[c] = shape;
|
||||
} else {
|
||||
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
|
||||
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
@@ -54,25 +54,25 @@ void VectorText::load_charset() {
|
||||
std::string filename = get_shape_filename(c);
|
||||
auto shape = ShapeLoader::load(filename);
|
||||
|
||||
if (shape && shape->es_valida()) {
|
||||
if (shape && shape->isValid()) {
|
||||
chars_[c] = shape;
|
||||
} else {
|
||||
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
|
||||
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar símbolo de copyright (©) - UTF-8 U+00A9
|
||||
// Usem el segon byte (0xA9) com a key interna
|
||||
// Usem el segon byte (0xA9) como 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()) {
|
||||
if (shape && shape->isValid()) {
|
||||
chars_[c] = shape;
|
||||
} else {
|
||||
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
|
||||
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ void VectorText::load_charset() {
|
||||
}
|
||||
|
||||
std::string VectorText::get_shape_filename(char c) const {
|
||||
// Mapeo carácter → nombre de archivo (amb prefix "font/")
|
||||
// Mapeo carácter → nombre de archivo (con prefix "font/")
|
||||
switch (c) {
|
||||
case '0':
|
||||
case '1':
|
||||
@@ -168,7 +168,7 @@ std::string VectorText::get_shape_filename(char c) const {
|
||||
case '?':
|
||||
return "font/char_question.shp";
|
||||
case ' ':
|
||||
return ""; // Espai es maneja sense carregar shape
|
||||
return ""; // Espai es maneja sin load shape
|
||||
|
||||
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
|
||||
return "font/char_copyright.shp";
|
||||
@@ -182,23 +182,23 @@ 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 {
|
||||
void VectorText::render(const std::string& text, const Vec2& position, float scale, 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;
|
||||
// Ancho de un carácter base (20 px a scale 1.0)
|
||||
const float char_width_scaled = char_width * scale;
|
||||
|
||||
// Spacing escalado
|
||||
const float spacing_scaled = spacing * escala;
|
||||
const float spacing_scaled = spacing * scale;
|
||||
|
||||
// Altura de un carácter escalado (necesario para ajustar Y)
|
||||
const float char_height_scaled = char_height * escala;
|
||||
const float char_height_scaled = char_height * scale;
|
||||
|
||||
// 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;
|
||||
float current_x = position.x;
|
||||
|
||||
// Iterar sobre cada byte del string (con detecció UTF-8)
|
||||
for (size_t i = 0; i < text.length(); i++) {
|
||||
@@ -207,7 +207,7 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca
|
||||
// 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
|
||||
c = 0xA9; // Usar segon byte como a key
|
||||
i++; // Saltar el següent byte
|
||||
}
|
||||
|
||||
@@ -221,10 +221,10 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca
|
||||
auto it = chars_.find(c);
|
||||
if (it != chars_.end()) {
|
||||
// Renderizar carácter
|
||||
// Ajustar X e Y para que posicio represente esquina superior izquierda
|
||||
// Ajustar X e Y para que position 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);
|
||||
Vec2 char_pos = {.x = current_x + (char_width_scaled / 2.0F), .y = position.y + (char_height_scaled / 2.0F)};
|
||||
Rendering::render_shape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness);
|
||||
|
||||
// Avanzar posición
|
||||
current_x += char_width_scaled + spacing_scaled;
|
||||
@@ -237,28 +237,28 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca
|
||||
}
|
||||
}
|
||||
|
||||
void VectorText::render_centered(const std::string& text, const Punt& centre_punt, float escala, float spacing, float brightness) const {
|
||||
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, 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);
|
||||
float text_width = get_text_width(text, scale, spacing);
|
||||
float text_height = get_text_height(scale);
|
||||
|
||||
// Calcular posició de l'esquina superior esquerra
|
||||
// restant la meitat de les dimensions del punt central
|
||||
Punt posicio_esquerra = {
|
||||
// Calcular posición de l'esquina superior izquierda
|
||||
// restant la meitat de las dimensions del point central
|
||||
Vec2 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);
|
||||
// Delegar al método render() existent
|
||||
render(text, posicio_esquerra, scale, spacing, brightness);
|
||||
}
|
||||
|
||||
float VectorText::get_text_width(const std::string& text, float escala, float spacing) const {
|
||||
float VectorText::get_text_width(const std::string& text, float scale, float spacing) const {
|
||||
if (text.empty()) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
const float char_width_scaled = char_width * escala;
|
||||
const float spacing_scaled = spacing * escala;
|
||||
const float char_width_scaled = char_width * scale;
|
||||
const float spacing_scaled = spacing * scale;
|
||||
|
||||
// Contar caracteres visuals (no bytes) - manejar UTF-8
|
||||
size_t visual_chars = 0;
|
||||
@@ -279,8 +279,8 @@ float VectorText::get_text_width(const std::string& text, float escala, float sp
|
||||
return (visual_chars * char_width_scaled) + ((visual_chars - 1) * spacing_scaled);
|
||||
}
|
||||
|
||||
float VectorText::get_text_height(float escala) const {
|
||||
return char_height * escala;
|
||||
float VectorText::get_text_height(float scale) const {
|
||||
return char_height * scale;
|
||||
}
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// vector_text.hpp - Sistema de texto vectorial con display de 7-segmentos
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
@@ -16,36 +18,36 @@ namespace Graphics {
|
||||
|
||||
class VectorText {
|
||||
public:
|
||||
VectorText(SDL_Renderer* renderer);
|
||||
VectorText(Rendering::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;
|
||||
// - position: posición inicial (esquina superior izquierda)
|
||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||
void render(const std::string& text, const Vec2& position, float scale = 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;
|
||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 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;
|
||||
[[nodiscard]] float get_text_width(const std::string& text, float scale = 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;
|
||||
[[nodiscard]] float get_text_height(float scale = 1.0F) const;
|
||||
|
||||
// Verificar si un carácter está soportado
|
||||
[[nodiscard]] bool is_supported(char c) const;
|
||||
|
||||
private:
|
||||
SDL_Renderer* renderer_;
|
||||
Rendering::Renderer* renderer_;
|
||||
std::unordered_map<char, std::shared_ptr<Shape>> chars_;
|
||||
|
||||
void load_charset();
|
||||
|
||||
@@ -30,7 +30,7 @@ Input::Input(std::string game_controller_db_path)
|
||||
// Inicializar bindings del teclado (valores por defecto)
|
||||
// Estos serán sobrescritos por applyPlayer1BindingsFromOptions()
|
||||
keyboard_.bindings = {
|
||||
// Movimiento del jugador
|
||||
// Movimiento del player
|
||||
{Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}},
|
||||
{Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}},
|
||||
{Action::THRUST, KeyState{.scancode = SDL_SCANCODE_UP}},
|
||||
@@ -188,7 +188,7 @@ auto Input::checkAnyButton(bool repeat) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Comprueba si algún jugador (P1 o P2) presionó alguna acción de una lista
|
||||
// Comprueba si algún player (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)) {
|
||||
@@ -388,14 +388,14 @@ void Input::update() {
|
||||
binding.second.is_held = key_is_down_now;
|
||||
}
|
||||
|
||||
// Actualizar bindings de jugador 1
|
||||
// Actualizar bindings de player 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
|
||||
// Actualizar bindings de player 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;
|
||||
@@ -493,7 +493,7 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
|
||||
|
||||
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
|
||||
|
||||
// Aplica configuración de controles del jugador 1
|
||||
// Aplica configuración de controles del player 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;
|
||||
@@ -527,7 +527,7 @@ void Input::applyPlayer1BindingsFromOptions() {
|
||||
player1_gamepad_ = gamepad;
|
||||
}
|
||||
|
||||
// Aplica configuración de controles del jugador 2
|
||||
// Aplica configuración de controles del player 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;
|
||||
@@ -561,7 +561,7 @@ void Input::applyPlayer2BindingsFromOptions() {
|
||||
player2_gamepad_ = gamepad;
|
||||
}
|
||||
|
||||
// Consulta de input para jugador 1
|
||||
// Consulta de input para player 1
|
||||
auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
|
||||
// Comprobar teclado con el mapa específico de P1
|
||||
bool keyboard_active = false;
|
||||
@@ -583,7 +583,7 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
|
||||
return keyboard_active || gamepad_active;
|
||||
}
|
||||
|
||||
// Consulta de input para jugador 2
|
||||
// Consulta de input para player 2
|
||||
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
|
||||
// Comprobar teclado con el mapa específico de P2
|
||||
bool keyboard_active = false;
|
||||
|
||||
@@ -58,7 +58,7 @@ class Input {
|
||||
name(std::string(SDL_GetGamepadName(gamepad))),
|
||||
path(std::string(SDL_GetGamepadPath(pad))),
|
||||
bindings{
|
||||
// Movimiento y acciones del jugador
|
||||
// Movimiento y acciones del player
|
||||
{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)}},
|
||||
@@ -92,7 +92,7 @@ class Input {
|
||||
void applyKeyboardBindingsFromOptions();
|
||||
void applyGamepadBindingsFromOptions();
|
||||
|
||||
// Configuración por jugador (Orni - dos jugadores)
|
||||
// Configuración por player (Orni - dos jugadores)
|
||||
void applyPlayer1BindingsFromOptions();
|
||||
void applyPlayer2BindingsFromOptions();
|
||||
|
||||
@@ -105,7 +105,7 @@ class Input {
|
||||
auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||
void resetInputStates();
|
||||
|
||||
// Consulta por jugador (Orni - dos jugadores)
|
||||
// Consulta por player (Orni - dos jugadores)
|
||||
auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
|
||||
auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
|
||||
|
||||
@@ -152,11 +152,11 @@ class Input {
|
||||
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)
|
||||
// Referencias cacheadas a gamepads por player (Orni)
|
||||
std::shared_ptr<Gamepad> player1_gamepad_;
|
||||
std::shared_ptr<Gamepad> player2_gamepad_;
|
||||
|
||||
// Mapas de bindings separados por jugador (Orni - dos jugadores)
|
||||
// Mapas de bindings separados por player (Orni - dos jugadores)
|
||||
std::unordered_map<Action, KeyState> player1_keyboard_bindings_;
|
||||
std::unordered_map<Action, KeyState> player2_keyboard_bindings_;
|
||||
};
|
||||
@@ -13,7 +13,7 @@ enum class InputAction : int { // Acciones de entrada posibles en el juego
|
||||
RIGHT, // Rotar derecha
|
||||
THRUST, // Acelerar
|
||||
SHOOT, // Disparar
|
||||
START, // Empezar partida
|
||||
START, // Empezar match
|
||||
|
||||
// Inputs de sistema (globales)
|
||||
WINDOW_INC_ZOOM, // F2
|
||||
|
||||
@@ -12,19 +12,19 @@ bool cursor_visible = false; // Estado del cursor (inicia ocult)
|
||||
// SDLManager controla esto mediante llamadas a setForceHidden().
|
||||
bool force_hidden = false;
|
||||
|
||||
// Temps d'inicialització per ignorar esdeveniments fantasma de SDL
|
||||
// Temps de inicialización 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
|
||||
// Forçar ocultació sincronitzant state SDL i state intern
|
||||
std::cout << "[Mouse::forceHide] Ocultant cursor i sincronitzant state. 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';
|
||||
initialization_time = SDL_GetTicks(); // Marcar time per ignorar esdeveniments inicials
|
||||
std::cout << "[Mouse::forceHide] Ignorant moviments durante " << IGNORE_MOTION_DURATION << "ms" << '\n';
|
||||
}
|
||||
|
||||
void setForceHidden(bool force) {
|
||||
@@ -56,16 +56,16 @@ void handleEvent(const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
Uint32 current_time = SDL_GetTicks();
|
||||
|
||||
// Ignorar esdeveniments fantasma de SDL durant el període inicial
|
||||
// Ignorar esdeveniments fantasma de SDL durante 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';
|
||||
std::cout << "[Mouse::handleEvent] Ignorant movement fantasma de SDL. time=" << current_time
|
||||
<< " (inicialización hace " << (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';
|
||||
std::cout << "[Mouse::handleEvent] Mostrant cursor per movement REAL. time=" << last_mouse_move_time << '\n';
|
||||
SDL_ShowCursor();
|
||||
cursor_visible = true;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el c
|
||||
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 forceHide(); // Forçar ocultació del cursor (sincronitza state intern)
|
||||
void handleEvent(const SDL_Event& event);
|
||||
void updateCursorVisibility();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// easing.hpp - Funcions d'interpolació i easing
|
||||
// easing.hpp - Funciones de interpolació i easing
|
||||
// © 2025 Orni Attack
|
||||
|
||||
#pragma once
|
||||
@@ -7,21 +7,21 @@ namespace Easing {
|
||||
|
||||
// Ease-out quadratic: empieza rápido, desacelera suavemente
|
||||
// t = progreso normalizado [0.0 - 1.0]
|
||||
// retorna valor interpolado [0.0 - 1.0]
|
||||
// retorna value 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]
|
||||
// retorna value 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]
|
||||
// retorna value interpolado [0.0 - 1.0]
|
||||
inline float ease_in_out_quad(float t) {
|
||||
return (t < 0.5F)
|
||||
? 2.0F * t * t
|
||||
@@ -30,7 +30,7 @@ inline float ease_in_out_quad(float t) {
|
||||
|
||||
// Ease-out cubic: desaceleración más suave que quadratic
|
||||
// t = progreso normalizado [0.0 - 1.0]
|
||||
// retorna valor interpolado [0.0 - 1.0]
|
||||
// retorna value interpolado [0.0 - 1.0]
|
||||
inline float ease_out_cubic(float t) {
|
||||
float t1 = 1.0F - t;
|
||||
return 1.0F - (t1 * t1 * t1);
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
// collision.hpp - Utilitats de detecció de col·lisions
|
||||
// collision.hpp - Utilitats de detecció de colisiones
|
||||
// © 2025 Orni Attack - Sistema de física
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/entities/entitat.hpp"
|
||||
#include "core/entities/entity.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) {
|
||||
// Comprobación genèrica de colisión entre dues entidades
|
||||
inline bool check_collision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) {
|
||||
// Comprovar si ambdós són col·lisionables
|
||||
if (!a.es_collidable() || !b.es_collidable()) {
|
||||
if (!a.isCollidable() || !b.isCollidable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calcular radi combinat (amb amplificador per hitbox generós)
|
||||
float suma_radis = (a.get_collision_radius() + b.get_collision_radius()) * amplifier;
|
||||
// Calcular radi combinat (con amplificador per hitbox generós)
|
||||
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * 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();
|
||||
// Comprobación distancia al cuadrado (sin sqrt)
|
||||
const Vec2& pos_a = a.getCenter();
|
||||
const Vec2& pos_b = b.getCenter();
|
||||
float dx = pos_a.x - pos_b.x;
|
||||
float dy = pos_a.y - pos_b.y;
|
||||
float dist_sq = (dx * dx) + (dy * dy);
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// physics_world.cpp - Implementación del mundo físico
|
||||
// © 2025 Orni Attack
|
||||
|
||||
#include "core/physics/physics_world.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "core/physics/rigid_body.hpp"
|
||||
|
||||
namespace Physics {
|
||||
|
||||
void PhysicsWorld::addBody(RigidBody* body) {
|
||||
if (body == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (std::ranges::find(bodies_, body) == bodies_.end()) {
|
||||
bodies_.push_back(body);
|
||||
}
|
||||
}
|
||||
|
||||
void PhysicsWorld::removeBody(RigidBody* body) {
|
||||
std::erase(bodies_, body);
|
||||
}
|
||||
|
||||
void PhysicsWorld::update(float dt) {
|
||||
integrate(dt);
|
||||
if (has_bounds_) {
|
||||
resolveBoundsCollisions();
|
||||
}
|
||||
resolveBodyCollisions();
|
||||
}
|
||||
|
||||
// Integración semi-implícita de Euler:
|
||||
// v(t+dt) = v(t) + (F/m) * dt
|
||||
// x(t+dt) = x(t) + v(t+dt) * dt
|
||||
// Más estable que Euler explícito para juegos. Damping exponencial.
|
||||
void PhysicsWorld::integrate(float dt) {
|
||||
for (auto* body : bodies_) {
|
||||
if (body == nullptr || body->isStatic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Aplicar fuerzas acumuladas → aceleración
|
||||
const Vec2 acceleration = body->force_accumulator * body->inverse_mass;
|
||||
body->velocity += acceleration * dt;
|
||||
|
||||
// Damping exponencial: equivalente a v *= exp(-damping * dt)
|
||||
// Aproximación lineal cuando damping*dt es pequeño.
|
||||
if (body->linear_damping > 0.0F) {
|
||||
const float DAMP = std::exp(-body->linear_damping * dt);
|
||||
body->velocity *= DAMP;
|
||||
}
|
||||
if (body->angular_damping > 0.0F) {
|
||||
const float DAMP = std::exp(-body->angular_damping * dt);
|
||||
body->angular_velocity *= DAMP;
|
||||
}
|
||||
|
||||
// Actualizar posición y rotación
|
||||
body->position += body->velocity * dt;
|
||||
body->angle += body->angular_velocity * dt;
|
||||
|
||||
body->clearAccumulators();
|
||||
}
|
||||
}
|
||||
|
||||
// Rebote contra los 4 bordes del rectángulo bounds_.
|
||||
// Refleja la componente normal de la velocidad por la restitución.
|
||||
void PhysicsWorld::resolveBoundsCollisions() {
|
||||
const float MIN_X = bounds_.x;
|
||||
const float MAX_X = bounds_.x + bounds_.w;
|
||||
const float MIN_Y = bounds_.y;
|
||||
const float MAX_Y = bounds_.y + bounds_.h;
|
||||
|
||||
for (auto* body : bodies_) {
|
||||
if (body == nullptr || body->isStatic()) {
|
||||
continue;
|
||||
}
|
||||
const float R = body->radius;
|
||||
|
||||
// Pared izquierda
|
||||
if (body->position.x - R < MIN_X) {
|
||||
body->position.x = MIN_X + R;
|
||||
if (body->velocity.x < 0.0F) {
|
||||
body->velocity.x = -body->velocity.x * body->restitution;
|
||||
}
|
||||
}
|
||||
// Pared derecha
|
||||
if (body->position.x + R > MAX_X) {
|
||||
body->position.x = MAX_X - R;
|
||||
if (body->velocity.x > 0.0F) {
|
||||
body->velocity.x = -body->velocity.x * body->restitution;
|
||||
}
|
||||
}
|
||||
// Pared superior
|
||||
if (body->position.y - R < MIN_Y) {
|
||||
body->position.y = MIN_Y + R;
|
||||
if (body->velocity.y < 0.0F) {
|
||||
body->velocity.y = -body->velocity.y * body->restitution;
|
||||
}
|
||||
}
|
||||
// Pared inferior
|
||||
if (body->position.y + R > MAX_Y) {
|
||||
body->position.y = MAX_Y - R;
|
||||
if (body->velocity.y > 0.0F) {
|
||||
body->velocity.y = -body->velocity.y * body->restitution;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Colisiones cuerpo-cuerpo: O(n²) círculo-círculo + resolución por impulso.
|
||||
// Para 15 enemigos + 6 balas + 2 naves = ~23 cuerpos → 253 pares. Sobra.
|
||||
//
|
||||
// Fórmula del impulso elástico (referencia: Chris Hecker / Box2D):
|
||||
// j = -(1 + e) * (v_rel · n) / (1/m_a + 1/m_b)
|
||||
// donde n es la normal del contacto (de a hacia b) y v_rel = v_a - v_b.
|
||||
void PhysicsWorld::resolveBodyCollisions() {
|
||||
const std::size_t COUNT = bodies_.size();
|
||||
for (std::size_t i = 0; i < COUNT; ++i) {
|
||||
for (std::size_t j = i + 1; j < COUNT; ++j) {
|
||||
auto* a = bodies_[i];
|
||||
auto* b = bodies_[j];
|
||||
if (a == nullptr || b == nullptr) {
|
||||
continue;
|
||||
}
|
||||
// Dos cuerpos estáticos no necesitan resolución
|
||||
if (a->isStatic() && b->isStatic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Vec2 DELTA = b->position - a->position;
|
||||
const float DIST_SQ = DELTA.lengthSquared();
|
||||
const float SUM_R = a->radius + b->radius;
|
||||
if (DIST_SQ > SUM_R * SUM_R || DIST_SQ <= 0.0F) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DIST = std::sqrt(DIST_SQ);
|
||||
const Vec2 NORMAL = DELTA / DIST; // de A hacia B
|
||||
|
||||
// Corrección posicional (resolver penetración)
|
||||
const float PENETRATION = SUM_R - DIST;
|
||||
const float TOTAL_INV_MASS = a->inverse_mass + b->inverse_mass;
|
||||
if (TOTAL_INV_MASS > 0.0F) {
|
||||
const Vec2 CORRECTION = NORMAL * (PENETRATION / TOTAL_INV_MASS);
|
||||
if (!a->isStatic()) {
|
||||
a->position -= CORRECTION * a->inverse_mass;
|
||||
}
|
||||
if (!b->isStatic()) {
|
||||
b->position += CORRECTION * b->inverse_mass;
|
||||
}
|
||||
}
|
||||
|
||||
// Velocidad relativa proyectada sobre la normal
|
||||
const Vec2 V_REL = b->velocity - a->velocity;
|
||||
const float VEL_ALONG_NORMAL = V_REL.dot(NORMAL);
|
||||
// Si se están separando, no aplicar impulso
|
||||
if (VEL_ALONG_NORMAL > 0.0F) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Restitución promedio (Box2D usa max; promedio es más permisivo)
|
||||
const float E = (a->restitution + b->restitution) * 0.5F;
|
||||
const float J = -(1.0F + E) * VEL_ALONG_NORMAL / TOTAL_INV_MASS;
|
||||
const Vec2 IMPULSE = NORMAL * J;
|
||||
|
||||
if (!a->isStatic()) {
|
||||
a->velocity -= IMPULSE * a->inverse_mass;
|
||||
}
|
||||
if (!b->isStatic()) {
|
||||
b->velocity += IMPULSE * b->inverse_mass;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Physics
|
||||
@@ -0,0 +1,64 @@
|
||||
// physics_world.hpp - Mundo físico 2D
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Gestiona un conjunto de RigidBody, integra sus movimientos y detecta
|
||||
// colisiones por frame. Diseño minimalista para arcade: broadphase trivial
|
||||
// O(n²) suficiente para <50 cuerpos (15 enemigos + balas + paredes).
|
||||
//
|
||||
// Los RigidBody viven en las entidades (las entidades poseen sus bodies);
|
||||
// PhysicsWorld solo guarda punteros no-owning. La entidad es responsable
|
||||
// de añadir/quitar su body del mundo en init/destroy.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Physics {
|
||||
|
||||
struct RigidBody;
|
||||
|
||||
class PhysicsWorld {
|
||||
public:
|
||||
PhysicsWorld() = default;
|
||||
|
||||
// Añade un cuerpo al mundo (no toma ownership).
|
||||
void addBody(RigidBody* body);
|
||||
|
||||
// Elimina un cuerpo. No-op si no está registrado.
|
||||
void removeBody(RigidBody* body);
|
||||
|
||||
// Vacía la lista (no destruye los cuerpos).
|
||||
void clear() { bodies_.clear(); }
|
||||
|
||||
// Define los límites del mundo (paredes implícitas). Pasa un rect
|
||||
// PLAYAREA para que los cuerpos reboten contra los bordes según su
|
||||
// restitution. Vacío = sin paredes.
|
||||
void setBounds(const SDL_FRect& bounds) {
|
||||
bounds_ = bounds;
|
||||
has_bounds_ = true;
|
||||
}
|
||||
void clearBounds() { has_bounds_ = false; }
|
||||
|
||||
// Avanza la simulación dt segundos:
|
||||
// 1. Integra cada cuerpo (semi-implicit Euler + damping)
|
||||
// 2. Resuelve colisiones contra los bounds (si configurados)
|
||||
// 3. Resuelve colisiones cuerpo-cuerpo (impulsos elásticos)
|
||||
void update(float dt);
|
||||
|
||||
// Consultas
|
||||
[[nodiscard]] auto getBodyCount() const -> std::size_t { return bodies_.size(); }
|
||||
[[nodiscard]] auto getBodies() const -> const std::vector<RigidBody*>& { return bodies_; }
|
||||
|
||||
private:
|
||||
std::vector<RigidBody*> bodies_;
|
||||
SDL_FRect bounds_{0.0F, 0.0F, 0.0F, 0.0F};
|
||||
bool has_bounds_{false};
|
||||
|
||||
void integrate(float dt);
|
||||
void resolveBoundsCollisions();
|
||||
void resolveBodyCollisions();
|
||||
};
|
||||
|
||||
} // namespace Physics
|
||||
@@ -0,0 +1,75 @@
|
||||
// rigid_body.hpp - Cuerpo rígido 2D para el sistema de física
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Estructura POD-like que encapsula el estado físico de una entidad:
|
||||
// posición, velocidad lineal/angular, masa, restitución y damping.
|
||||
// El integrador es semi-implícito de Euler (estable para juegos arcade).
|
||||
//
|
||||
// Convenciones:
|
||||
// - position: coordenadas lógicas (px), donde la entidad está en el mundo
|
||||
// - angle: radianes; 0 apunta hacia arriba (eje Y negativo en SDL)
|
||||
// - velocity: px/s en cartesianas (NO polares — adiós a cos/sin por entidad)
|
||||
// - mass = 0 (inverse_mass = 0) representa un cuerpo estático (masa infinita)
|
||||
// - restitution 0 = inelástico, 1 = elástico perfecto
|
||||
// - linear_damping en s⁻¹ (fricción exponencial: v *= exp(-damping * dt))
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Physics {
|
||||
|
||||
struct RigidBody {
|
||||
// --- Estado cinemático ---
|
||||
Vec2 position{}; // Posición del centro (px)
|
||||
Vec2 velocity{}; // Velocidad lineal (px/s)
|
||||
float angle{0.0F}; // Orientación (rad)
|
||||
float angular_velocity{0.0F}; // Velocidad angular (rad/s)
|
||||
|
||||
// --- Propiedades físicas ---
|
||||
float mass{1.0F}; // Masa (kg, escala libre)
|
||||
float inverse_mass{1.0F}; // 1/mass cacheado (0 = estático)
|
||||
float restitution{0.5F}; // Elasticidad (0..1)
|
||||
float linear_damping{0.0F}; // Fricción lineal (s⁻¹)
|
||||
float angular_damping{0.0F}; // Fricción angular (s⁻¹)
|
||||
float radius{0.0F}; // Radio de colisión (círculo)
|
||||
|
||||
// --- Fuerzas acumuladas (reseteadas tras cada integrate) ---
|
||||
Vec2 force_accumulator{};
|
||||
float torque_accumulator{0.0F};
|
||||
|
||||
// Configura la masa y precalcula inverse_mass.
|
||||
// mass <= 0 marca el cuerpo como estático (inmovible por impulsos).
|
||||
void setMass(float new_mass) {
|
||||
mass = new_mass;
|
||||
inverse_mass = (new_mass > 0.0F) ? 1.0F / new_mass : 0.0F;
|
||||
}
|
||||
|
||||
// Marca el cuerpo como estático (paredes, obstáculos fijos).
|
||||
void setStatic() {
|
||||
mass = 0.0F;
|
||||
inverse_mass = 0.0F;
|
||||
velocity = Vec2{};
|
||||
angular_velocity = 0.0F;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto isStatic() const -> bool { return inverse_mass == 0.0F; }
|
||||
|
||||
// Aplica una fuerza instantánea (acumulada para el siguiente integrate).
|
||||
void applyForce(const Vec2& force) { force_accumulator += force; }
|
||||
|
||||
// Aplica un impulso (cambio inmediato de velocidad: Δv = J / m).
|
||||
void applyImpulse(const Vec2& impulse) {
|
||||
if (!isStatic()) {
|
||||
velocity += impulse * inverse_mass;
|
||||
}
|
||||
}
|
||||
|
||||
// Resetea los acumuladores tras la integración.
|
||||
void clearAccumulators() {
|
||||
force_accumulator = Vec2{};
|
||||
torque_accumulator = 0.0F;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Physics
|
||||
@@ -1,68 +0,0 @@
|
||||
// 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
|
||||
@@ -1,29 +0,0 @@
|
||||
// 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
|
||||
@@ -1,11 +1,11 @@
|
||||
// coordinate_transform.cpp - Inicialització de variables globals
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// coordinate_transform.cpp - Inicialización de variables globals
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "core/rendering/coordinate_transform.hpp"
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
// Factor d'escala global (inicialitzat a 1.0 per defecte)
|
||||
// Factor de scale global (inicialitzat a 1.0 per defecte)
|
||||
float g_current_scale_factor = 1.0F;
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// coordinate_transform.hpp - Transformació de coordenades lògiques a físiques
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
// Factor d'escala global (actualitzat cada frame per SDLManager)
|
||||
// Factor de scale global (actualitzat cada frame per SDLManager)
|
||||
extern float g_current_scale_factor;
|
||||
|
||||
// Transforma coordenada lògica a física amb arrodoniment
|
||||
// Transforma coordenada lógica a física con arrodoniment
|
||||
inline int transform_x(int logical_x, float scale) {
|
||||
return static_cast<int>(std::round(logical_x * scale));
|
||||
}
|
||||
@@ -19,7 +19,7 @@ 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
|
||||
// Variant que usa el factor de scale global
|
||||
inline int transform_x(int logical_x) {
|
||||
return transform_x(logical_x, g_current_scale_factor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// gpu_device.cpp - Implementación del wrapper de SDL_GPUDevice
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/utils/path_utils.hpp"
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
GpuDevice::~GpuDevice() { destroy(); }
|
||||
|
||||
auto GpuDevice::init(SDL_Window* window) -> bool {
|
||||
window_ = window;
|
||||
|
||||
// Solicitar backends en orden de preferencia: Vulkan (Linux/Windows),
|
||||
// Metal (macOS). Sin DirectX según decisión de proyecto.
|
||||
// SDL_GPU_SHADERFORMAT_SPIRV: shaders compilados con glslc.
|
||||
// SDL_GPU_SHADERFORMAT_MSL: pendiente para macOS (Fase futura).
|
||||
constexpr SDL_GPUShaderFormat SHADER_FORMATS =
|
||||
SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL;
|
||||
|
||||
device_ = SDL_CreateGPUDevice(SHADER_FORMATS, /*debug=*/true, /*name=*/nullptr);
|
||||
if (device_ == nullptr) {
|
||||
std::cerr << "[GpuDevice] SDL_CreateGPUDevice falló: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* driver = SDL_GetGPUDeviceDriver(device_);
|
||||
std::cout << "[GpuDevice] Backend GPU: " << (driver != nullptr ? driver : "?") << '\n';
|
||||
|
||||
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||
std::cerr << "[GpuDevice] SDL_ClaimWindowForGPUDevice falló: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuDevice::destroy() {
|
||||
if (device_ != nullptr) {
|
||||
if (window_ != nullptr) {
|
||||
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||
}
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
}
|
||||
window_ = nullptr;
|
||||
swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
|
||||
}
|
||||
|
||||
auto GpuDevice::loadShader(const std::string& spv_filename,
|
||||
SDL_GPUShaderStage stage,
|
||||
uint32_t num_uniform_buffers,
|
||||
uint32_t num_samplers) const -> SDL_GPUShader* {
|
||||
if (device_ == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Los .spv viven en build/shaders/ junto al binario.
|
||||
const std::string PATH = Utils::getExecutableDirectory() + "/shaders/" + spv_filename;
|
||||
|
||||
std::ifstream file(PATH, std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "[GpuDevice] No s'ha pogut obrir el shader: " << PATH << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
const std::streamsize SIZE = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<uint8_t> buffer(static_cast<size_t>(SIZE));
|
||||
if (!file.read(reinterpret_cast<char*>(buffer.data()), SIZE)) {
|
||||
std::cerr << "[GpuDevice] Error llegint shader: " << PATH << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SDL_GPUShaderCreateInfo info{};
|
||||
info.code = buffer.data();
|
||||
info.code_size = buffer.size();
|
||||
info.entrypoint = "main";
|
||||
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
info.stage = stage;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_storage_buffers = 0;
|
||||
info.num_storage_textures = 0;
|
||||
|
||||
SDL_GPUShader* shader = SDL_CreateGPUShader(device_, &info);
|
||||
if (shader == nullptr) {
|
||||
std::cerr << "[GpuDevice] SDL_CreateGPUShader (" << spv_filename << "): " << SDL_GetError() << '\n';
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,59 @@
|
||||
// gpu_device.hpp - Wrapper de SDL_GPUDevice
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Ownership del SDL_GPUDevice y del claim del window. Backend preferido:
|
||||
// Vulkan (Linux, Windows) y Metal (macOS). Sin DirectX.
|
||||
//
|
||||
// Uso:
|
||||
// GpuDevice device;
|
||||
// if (!device.init(window)) return -1; // claim del window
|
||||
// ... renderer setup ...
|
||||
// device.destroy(); // unclaim + destroy device
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
class GpuDevice {
|
||||
public:
|
||||
GpuDevice() = default;
|
||||
~GpuDevice();
|
||||
|
||||
// No copia / move (RAII propietario del device).
|
||||
GpuDevice(const GpuDevice&) = delete;
|
||||
auto operator=(const GpuDevice&) -> GpuDevice& = delete;
|
||||
GpuDevice(GpuDevice&&) = delete;
|
||||
auto operator=(GpuDevice&&) -> GpuDevice& = delete;
|
||||
|
||||
// Crea el device y claim el window. Devuelve false si no hay backend
|
||||
// soportado o si el driver no permite el claim.
|
||||
[[nodiscard]] auto init(SDL_Window* window) -> bool;
|
||||
void destroy();
|
||||
|
||||
// Carga un shader SPIR-V desde build/shaders/{name}.spv. Devuelve un
|
||||
// SDL_GPUShader* del que ahora es responsable el caller (libera con
|
||||
// SDL_ReleaseGPUShader). Retorna nullptr si falla.
|
||||
//
|
||||
// num_uniform_buffers: nº de uniform buffers que usa el shader (slot 0..N-1).
|
||||
// num_samplers: nº de samplers (combined image+sampler) usados por el shader.
|
||||
[[nodiscard]] auto loadShader(const std::string& spv_filename,
|
||||
SDL_GPUShaderStage stage,
|
||||
uint32_t num_uniform_buffers,
|
||||
uint32_t num_samplers = 0) const -> SDL_GPUShader*;
|
||||
|
||||
[[nodiscard]] auto get() const -> SDL_GPUDevice* { return device_; }
|
||||
[[nodiscard]] auto window() const -> SDL_Window* { return window_; }
|
||||
[[nodiscard]] auto swapchainFormat() const -> SDL_GPUTextureFormat { return swapchain_format_; }
|
||||
|
||||
private:
|
||||
SDL_GPUDevice* device_{nullptr};
|
||||
SDL_Window* window_{nullptr};
|
||||
SDL_GPUTextureFormat swapchain_format_{SDL_GPU_TEXTUREFORMAT_INVALID};
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,400 @@
|
||||
// gpu_frame_renderer.cpp - Implementación del FrameRenderer con offscreen + postpro
|
||||
|
||||
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
GpuFrameRenderer::~GpuFrameRenderer() { destroy(); }
|
||||
|
||||
auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool {
|
||||
logical_w_ = logical_w;
|
||||
logical_h_ = logical_h;
|
||||
|
||||
if (!device_.init(window)) {
|
||||
return false;
|
||||
}
|
||||
// Pipeline de líneas: escribe sobre el offscreen (formato fijo).
|
||||
if (!line_pipeline_.init(device_, offscreen_format_)) {
|
||||
device_.destroy();
|
||||
return false;
|
||||
}
|
||||
// Pipeline de postpro: escribe sobre swapchain (formato del swapchain).
|
||||
if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) {
|
||||
line_pipeline_.destroy();
|
||||
device_.destroy();
|
||||
return false;
|
||||
}
|
||||
if (!createOffscreen()) {
|
||||
postfx_pipeline_.destroy();
|
||||
line_pipeline_.destroy();
|
||||
device_.destroy();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto GpuFrameRenderer::createOffscreen() -> bool {
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
if (dev == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Textura offscreen del tamaño lógico del juego, COLOR_TARGET + SAMPLER.
|
||||
SDL_GPUTextureCreateInfo tex_info{};
|
||||
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
tex_info.format = offscreen_format_;
|
||||
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
tex_info.width = static_cast<uint32_t>(logical_w_);
|
||||
tex_info.height = static_cast<uint32_t>(logical_h_);
|
||||
tex_info.layer_count_or_depth = 1;
|
||||
tex_info.num_levels = 1;
|
||||
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
offscreen_texture_ = SDL_CreateGPUTexture(dev, &tex_info);
|
||||
if (offscreen_texture_ == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (offscreen): "
|
||||
<< SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sampler lineal con clamp-to-edge (evita sangrado en los bordes del bloom).
|
||||
SDL_GPUSamplerCreateInfo sampler_info{};
|
||||
sampler_info.min_filter = SDL_GPU_FILTER_LINEAR;
|
||||
sampler_info.mag_filter = SDL_GPU_FILTER_LINEAR;
|
||||
sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR;
|
||||
sampler_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
sampler_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
sampler_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
linear_sampler_ = SDL_CreateGPUSampler(dev, &sampler_info);
|
||||
if (linear_sampler_ == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] SDL_CreateGPUSampler: "
|
||||
<< SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::destroyOffscreen() {
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
if (dev == nullptr) {
|
||||
offscreen_texture_ = nullptr;
|
||||
linear_sampler_ = nullptr;
|
||||
return;
|
||||
}
|
||||
if (offscreen_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(dev, offscreen_texture_);
|
||||
offscreen_texture_ = nullptr;
|
||||
}
|
||||
if (linear_sampler_ != nullptr) {
|
||||
SDL_ReleaseGPUSampler(dev, linear_sampler_);
|
||||
linear_sampler_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::destroy() {
|
||||
destroyOffscreen();
|
||||
postfx_pipeline_.destroy();
|
||||
line_pipeline_.destroy();
|
||||
device_.destroy();
|
||||
vertices_.clear();
|
||||
indices_.clear();
|
||||
}
|
||||
|
||||
auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool {
|
||||
// Los clear_* se ignoran: el fondo lo pinta el postpro. Mantenemos la
|
||||
// firma para no romper el SDLManager.
|
||||
(void)clear_r;
|
||||
(void)clear_g;
|
||||
(void)clear_b;
|
||||
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
if (dev == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cmd_buffer_ = SDL_AcquireGPUCommandBuffer(dev);
|
||||
if (cmd_buffer_ == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] SDL_AcquireGPUCommandBuffer: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(),
|
||||
&swapchain_texture_, nullptr, nullptr)) {
|
||||
std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (swapchain_texture_ == nullptr) {
|
||||
// Ventana minimizada o swapchain no disponible: solo submit y salir.
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Abrir render pass sobre OFFSCREEN con clear a negro.
|
||||
SDL_GPUColorTargetInfo color_target{};
|
||||
color_target.texture = offscreen_texture_;
|
||||
color_target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
color_target.cycle = false;
|
||||
|
||||
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
|
||||
if (render_pass_ == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] SDL_BeginGPURenderPass (offscreen): "
|
||||
<< SDL_GetError() << '\n';
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
// Sin SetGPUViewport: el offscreen se llena entero a tamaño lógico.
|
||||
|
||||
vertices_.clear();
|
||||
indices_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::setViewport(float x, float y, float w, float h) {
|
||||
viewport_x_ = x;
|
||||
viewport_y_ = y;
|
||||
viewport_w_ = w;
|
||||
viewport_h_ = h;
|
||||
// El viewport solo se aplica en el pase final (composite). Si estamos
|
||||
// ya dentro del composite, lo aplicaríamos inmediatamente, pero la API
|
||||
// está pensada para llamarse antes de endFrame/al cambiar de ventana.
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::setVSync(bool enabled) {
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
if (dev == nullptr || device_.window() == nullptr) {
|
||||
return;
|
||||
}
|
||||
const SDL_GPUPresentMode MODE = enabled
|
||||
? SDL_GPU_PRESENTMODE_VSYNC
|
||||
: SDL_GPU_PRESENTMODE_IMMEDIATE;
|
||||
if (!SDL_SetGPUSwapchainParameters(dev, device_.window(),
|
||||
SDL_GPU_SWAPCHAINCOMPOSITION_SDR, MODE)) {
|
||||
std::cerr << "[GpuFrameRenderer] SDL_SetGPUSwapchainParameters: " << SDL_GetError() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::applyFinalViewport() {
|
||||
if (render_pass_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (viewport_w_ <= 0.0F || viewport_h_ <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
SDL_GPUViewport vp{};
|
||||
vp.x = viewport_x_;
|
||||
vp.y = viewport_y_;
|
||||
vp.w = viewport_w_;
|
||||
vp.h = viewport_h_;
|
||||
vp.min_depth = 0.0F;
|
||||
vp.max_depth = 1.0F;
|
||||
SDL_SetGPUViewport(render_pass_, &vp);
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness,
|
||||
float r, float g, float b, float a) {
|
||||
const float DX = x2 - x1;
|
||||
const float DY = y2 - y1;
|
||||
const float LEN = std::sqrt((DX * DX) + (DY * DY));
|
||||
if (LEN < 1e-6F) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float HALF = thickness * 0.5F;
|
||||
const float NX = -DY / LEN * HALF;
|
||||
const float NY = DX / LEN * HALF;
|
||||
|
||||
const auto BASE_INDEX = static_cast<uint16_t>(vertices_.size());
|
||||
|
||||
vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a});
|
||||
vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a});
|
||||
vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a});
|
||||
vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a});
|
||||
|
||||
indices_.push_back(BASE_INDEX + 0);
|
||||
indices_.push_back(BASE_INDEX + 1);
|
||||
indices_.push_back(BASE_INDEX + 2);
|
||||
indices_.push_back(BASE_INDEX + 1);
|
||||
indices_.push_back(BASE_INDEX + 3);
|
||||
indices_.push_back(BASE_INDEX + 2);
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::flushBatch() {
|
||||
if (vertices_.empty() || indices_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
|
||||
const uint32_t VBO_SIZE = static_cast<uint32_t>(vertices_.size() * sizeof(LineVertex));
|
||||
const uint32_t IBO_SIZE = static_cast<uint32_t>(indices_.size() * sizeof(uint16_t));
|
||||
|
||||
SDL_GPUBufferCreateInfo vbo_info{};
|
||||
vbo_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
|
||||
vbo_info.size = VBO_SIZE;
|
||||
SDL_GPUBuffer* vbo = SDL_CreateGPUBuffer(dev, &vbo_info);
|
||||
|
||||
SDL_GPUBufferCreateInfo ibo_info{};
|
||||
ibo_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
|
||||
ibo_info.size = IBO_SIZE;
|
||||
SDL_GPUBuffer* ibo = SDL_CreateGPUBuffer(dev, &ibo_info);
|
||||
|
||||
SDL_GPUTransferBufferCreateInfo tbo_info{};
|
||||
tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tbo_info.size = VBO_SIZE + IBO_SIZE;
|
||||
SDL_GPUTransferBuffer* tbo = SDL_CreateGPUTransferBuffer(dev, &tbo_info);
|
||||
|
||||
auto* mapped = static_cast<uint8_t*>(SDL_MapGPUTransferBuffer(dev, tbo, false));
|
||||
std::memcpy(mapped, vertices_.data(), VBO_SIZE);
|
||||
std::memcpy(mapped + VBO_SIZE, indices_.data(), IBO_SIZE);
|
||||
SDL_UnmapGPUTransferBuffer(dev, tbo);
|
||||
|
||||
// Copy pass FUERA del render pass.
|
||||
SDL_EndGPURenderPass(render_pass_);
|
||||
render_pass_ = nullptr;
|
||||
|
||||
SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_);
|
||||
SDL_GPUTransferBufferLocation vbo_src{.transfer_buffer = tbo, .offset = 0};
|
||||
SDL_GPUBufferRegion vbo_dst{.buffer = vbo, .offset = 0, .size = VBO_SIZE};
|
||||
SDL_UploadToGPUBuffer(copy_pass, &vbo_src, &vbo_dst, false);
|
||||
SDL_GPUTransferBufferLocation ibo_src{.transfer_buffer = tbo, .offset = VBO_SIZE};
|
||||
SDL_GPUBufferRegion ibo_dst{.buffer = ibo, .offset = 0, .size = IBO_SIZE};
|
||||
SDL_UploadToGPUBuffer(copy_pass, &ibo_src, &ibo_dst, false);
|
||||
SDL_EndGPUCopyPass(copy_pass);
|
||||
|
||||
// Reabrir render pass sobre OFFSCREEN (load_op=LOAD para preservar el clear).
|
||||
SDL_GPUColorTargetInfo color_target{};
|
||||
color_target.texture = offscreen_texture_;
|
||||
color_target.load_op = SDL_GPU_LOADOP_LOAD;
|
||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
|
||||
|
||||
SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());
|
||||
|
||||
// UBO de líneas usa el tamaño lógico (también del offscreen).
|
||||
LineUniforms ubo{.viewport_width = logical_w_,
|
||||
.viewport_height = logical_h_,
|
||||
.padding_0 = 0.0F,
|
||||
.padding_1 = 0.0F};
|
||||
SDL_PushGPUVertexUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));
|
||||
|
||||
SDL_GPUBufferBinding vbo_binding{.buffer = vbo, .offset = 0};
|
||||
SDL_BindGPUVertexBuffers(render_pass_, 0, &vbo_binding, 1);
|
||||
|
||||
SDL_GPUBufferBinding ibo_binding{.buffer = ibo, .offset = 0};
|
||||
SDL_BindGPUIndexBuffer(render_pass_, &ibo_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);
|
||||
|
||||
SDL_DrawGPUIndexedPrimitives(render_pass_,
|
||||
static_cast<uint32_t>(indices_.size()),
|
||||
1, 0, 0, 0);
|
||||
|
||||
SDL_ReleaseGPUBuffer(dev, vbo);
|
||||
SDL_ReleaseGPUBuffer(dev, ibo);
|
||||
SDL_ReleaseGPUTransferBuffer(dev, tbo);
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::compositePass() {
|
||||
// Cierra el render pass actual (sobre offscreen).
|
||||
if (render_pass_ != nullptr) {
|
||||
SDL_EndGPURenderPass(render_pass_);
|
||||
render_pass_ = nullptr;
|
||||
}
|
||||
|
||||
// Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el
|
||||
// letterbox del viewport físico).
|
||||
SDL_GPUColorTargetInfo target{};
|
||||
target.texture = swapchain_texture_;
|
||||
target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
target.cycle = false;
|
||||
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr);
|
||||
if (render_pass_ == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] BeginRenderPass (composite): "
|
||||
<< SDL_GetError() << '\n';
|
||||
return;
|
||||
}
|
||||
applyFinalViewport();
|
||||
|
||||
SDL_BindGPUGraphicsPipeline(render_pass_, postfx_pipeline_.get());
|
||||
|
||||
// Bind del sampler (escena offscreen) en slot 0 del fragment shader.
|
||||
SDL_GPUTextureSamplerBinding sampler_binding{};
|
||||
sampler_binding.texture = offscreen_texture_;
|
||||
sampler_binding.sampler = linear_sampler_;
|
||||
SDL_BindGPUFragmentSamplers(render_pass_, 0, &sampler_binding, 1);
|
||||
|
||||
// Uniforms del postpro. Si una sección está desactivada, anulamos sus
|
||||
// contribuciones (intensidad / amplitud / max=min) en lugar de tener
|
||||
// un branch en el shader.
|
||||
const float BLOOM_INTENSITY = postfx_params_.bloom_enabled
|
||||
? postfx_params_.bloom_intensity : 0.0F;
|
||||
const float FLICKER_AMPLITUDE = postfx_params_.flicker_enabled
|
||||
? postfx_params_.flicker_amplitude : 0.0F;
|
||||
const float BG_MIN_R = postfx_params_.background_enabled ? postfx_params_.background_min_r : 0.0F;
|
||||
const float BG_MIN_G = postfx_params_.background_enabled ? postfx_params_.background_min_g : 0.0F;
|
||||
const float BG_MIN_B = postfx_params_.background_enabled ? postfx_params_.background_min_b : 0.0F;
|
||||
const float BG_MAX_R = postfx_params_.background_enabled ? postfx_params_.background_max_r : 0.0F;
|
||||
const float BG_MAX_G = postfx_params_.background_enabled ? postfx_params_.background_max_g : 0.0F;
|
||||
const float BG_MAX_B = postfx_params_.background_enabled ? postfx_params_.background_max_b : 0.0F;
|
||||
|
||||
// Tiempo en segundos desde el inicio de SDL (wall-clock real, robusto a FPS variables).
|
||||
const float TIME_SECONDS = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||
|
||||
PostFxUniforms ubo{};
|
||||
ubo.time = TIME_SECONDS;
|
||||
ubo.bloom_intensity = BLOOM_INTENSITY;
|
||||
ubo.bloom_threshold = postfx_params_.bloom_threshold;
|
||||
ubo.bloom_radius_px = postfx_params_.bloom_radius_px;
|
||||
ubo.flicker_amplitude = FLICKER_AMPLITUDE;
|
||||
ubo.flicker_frequency_hz = postfx_params_.flicker_frequency_hz;
|
||||
ubo.background_pulse_freq_hz = postfx_params_.background_pulse_freq_hz;
|
||||
ubo.pad_a_ = 0.0F;
|
||||
ubo.background_min_r = BG_MIN_R;
|
||||
ubo.background_min_g = BG_MIN_G;
|
||||
ubo.background_min_b = BG_MIN_B;
|
||||
ubo.background_min_a = 1.0F;
|
||||
ubo.background_max_r = BG_MAX_R;
|
||||
ubo.background_max_g = BG_MAX_G;
|
||||
ubo.background_max_b = BG_MAX_B;
|
||||
ubo.background_max_a = 1.0F;
|
||||
ubo.texel_size_x = 1.0F / logical_w_;
|
||||
ubo.texel_size_y = 1.0F / logical_h_;
|
||||
ubo.pad_b_ = 0.0F;
|
||||
ubo.pad_c_ = 0.0F;
|
||||
|
||||
SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));
|
||||
|
||||
// Fullscreen triangle: 3 vértices generados en el shader, sin VBO.
|
||||
SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::endFrame() {
|
||||
if (cmd_buffer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
flushBatch();
|
||||
compositePass();
|
||||
if (render_pass_ != nullptr) {
|
||||
SDL_EndGPURenderPass(render_pass_);
|
||||
render_pass_ = nullptr;
|
||||
}
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
swapchain_texture_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,139 @@
|
||||
// gpu_frame_renderer.hpp - Renderer de alto nivel basado en SDL_GPU
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Flujo por frame:
|
||||
// 1. beginFrame(clear_color)
|
||||
// → acquire swapchain + begin render pass sobre la textura OFFSCREEN
|
||||
// (clear a black; la swapchain se pinta después con el postpro).
|
||||
// 2. pushLine(...) — encola líneas (extrusión en CPU).
|
||||
// 3. endFrame()
|
||||
// → flush del batch en offscreen + pase de postpro (sample offscreen
|
||||
// → swapchain con bloom/flicker/background) + submit + presenta.
|
||||
//
|
||||
// La oscilación de brillo y el fondo pulsante viven ahora en el shader de
|
||||
// postpro; el CPU solo le pasa los uniformes del struct PostFxParams.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
|
||||
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
// Parámetros del postpro que el caller (SDLManager) pasa cada frame.
|
||||
// Equivalente al lado "humano" del PostFxUniforms (sin paddings).
|
||||
struct PostFxParams {
|
||||
bool bloom_enabled{true};
|
||||
float bloom_intensity{0.6F};
|
||||
float bloom_threshold{0.4F};
|
||||
float bloom_radius_px{2.0F};
|
||||
|
||||
bool flicker_enabled{true};
|
||||
float flicker_amplitude{0.10F};
|
||||
float flicker_frequency_hz{6.0F};
|
||||
|
||||
bool background_enabled{true};
|
||||
float background_min_r{0.0F};
|
||||
float background_min_g{0.02F};
|
||||
float background_min_b{0.0F};
|
||||
float background_max_r{0.0F};
|
||||
float background_max_g{0.06F};
|
||||
float background_max_b{0.0F};
|
||||
float background_pulse_freq_hz{6.0F};
|
||||
};
|
||||
|
||||
class GpuFrameRenderer {
|
||||
public:
|
||||
GpuFrameRenderer() = default;
|
||||
~GpuFrameRenderer();
|
||||
|
||||
GpuFrameRenderer(const GpuFrameRenderer&) = delete;
|
||||
auto operator=(const GpuFrameRenderer&) -> GpuFrameRenderer& = delete;
|
||||
GpuFrameRenderer(GpuFrameRenderer&&) = delete;
|
||||
auto operator=(GpuFrameRenderer&&) -> GpuFrameRenderer& = delete;
|
||||
|
||||
// Crea device + pipeline + offscreen + sampler. logical_w/h = tamaño
|
||||
// en píxeles lógicos del juego (1280×720), usado como base del
|
||||
// offscreen y de la transformación a NDC del shader de líneas.
|
||||
[[nodiscard]] auto init(SDL_Window* window, float logical_w, float logical_h) -> bool;
|
||||
void destroy();
|
||||
|
||||
// beginFrame: adquiere swapchain, abre render pass sobre offscreen
|
||||
// con clear a negro. Devuelve false si no hay textura disponible.
|
||||
// Los argumentos clear_r/g/b se ignoran (compatibilidad de API: el
|
||||
// fondo lo dibuja el postpro).
|
||||
[[nodiscard]] auto beginFrame(float clear_r, float clear_g, float clear_b) -> bool;
|
||||
|
||||
// Encola una línea con grosor configurable (px). Color RGBA en [0..1].
|
||||
void pushLine(float x1, float y1, float x2, float y2, float thickness,
|
||||
float r, float g, float b, float a);
|
||||
|
||||
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
||||
void endFrame();
|
||||
|
||||
// Viewport del PASE FINAL (postpro → swapchain) en píxeles físicos.
|
||||
// Implementa el letterbox: cualquier área fuera del viewport queda
|
||||
// como el clear color del pase de composite (negro). Si w<=0 o h<=0,
|
||||
// se usa el tamaño completo de la ventana.
|
||||
void setViewport(float x, float y, float w, float h);
|
||||
|
||||
// Activa/desactiva VSync. true = SDL_GPU_PRESENTMODE_VSYNC, false = IMMEDIATE.
|
||||
void setVSync(bool enabled);
|
||||
|
||||
// Parámetros del postpro que se aplican en endFrame. Por defecto =
|
||||
// valores de Defaults (bloom moderado, flicker suave, fondo verde tenue).
|
||||
void setPostFx(const PostFxParams& params) { postfx_params_ = params; }
|
||||
[[nodiscard]] auto postfx() const -> const PostFxParams& { return postfx_params_; }
|
||||
|
||||
// Acceso a internals.
|
||||
[[nodiscard]] auto device() -> GpuDevice& { return device_; }
|
||||
[[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; }
|
||||
|
||||
private:
|
||||
GpuDevice device_;
|
||||
GpuLinePipeline line_pipeline_;
|
||||
GpuPostFxPipeline postfx_pipeline_;
|
||||
|
||||
// Tamaño lógico del juego (= tamaño del offscreen).
|
||||
float logical_w_{1280.0F};
|
||||
float logical_h_{720.0F};
|
||||
|
||||
// Viewport del pase final en píxeles físicos. <0 = full window.
|
||||
float viewport_x_{0.0F};
|
||||
float viewport_y_{0.0F};
|
||||
float viewport_w_{-1.0F};
|
||||
float viewport_h_{-1.0F};
|
||||
|
||||
// Offscreen color target (formato fijo R8G8B8A8_UNORM para portabilidad).
|
||||
SDL_GPUTexture* offscreen_texture_{nullptr};
|
||||
SDL_GPUTextureFormat offscreen_format_{SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM};
|
||||
SDL_GPUSampler* linear_sampler_{nullptr};
|
||||
|
||||
// Batch del frame en curso.
|
||||
std::vector<LineVertex> vertices_;
|
||||
std::vector<uint16_t> indices_;
|
||||
|
||||
// Estado del frame en curso.
|
||||
SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
|
||||
SDL_GPUTexture* swapchain_texture_{nullptr};
|
||||
SDL_GPURenderPass* render_pass_{nullptr};
|
||||
|
||||
// Parámetros del postpro (configurables vía YAML).
|
||||
PostFxParams postfx_params_{};
|
||||
|
||||
// Helpers internos.
|
||||
[[nodiscard]] auto createOffscreen() -> bool;
|
||||
void destroyOffscreen();
|
||||
void flushBatch();
|
||||
void compositePass();
|
||||
void applyFinalViewport();
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,117 @@
|
||||
// gpu_line_pipeline.cpp - Implementación del pipeline de líneas
|
||||
|
||||
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
GpuLinePipeline::~GpuLinePipeline() { destroy(); }
|
||||
|
||||
auto GpuLinePipeline::init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool {
|
||||
owner_ = device.get();
|
||||
if (owner_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUShader* vert = device.loadShader("line.vert.spv",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX,
|
||||
/*num_uniform_buffers=*/1);
|
||||
SDL_GPUShader* frag = device.loadShader("line.frag.spv",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||
/*num_uniform_buffers=*/0);
|
||||
if ((vert == nullptr) || (frag == nullptr)) {
|
||||
if (vert != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
}
|
||||
if (frag != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
}
|
||||
std::cerr << "[GpuLinePipeline] Error cargando shaders\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vertex layout: pos (vec2) + color (vec4) → 6 floats por vertex.
|
||||
SDL_GPUVertexBufferDescription vbo_desc{};
|
||||
vbo_desc.slot = 0;
|
||||
vbo_desc.pitch = sizeof(LineVertex);
|
||||
vbo_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
|
||||
vbo_desc.instance_step_rate = 0;
|
||||
|
||||
SDL_GPUVertexAttribute attrs[2];
|
||||
attrs[0].location = 0; // in_position
|
||||
attrs[0].buffer_slot = 0;
|
||||
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||
attrs[0].offset = 0;
|
||||
attrs[1].location = 1; // in_color
|
||||
attrs[1].buffer_slot = 0;
|
||||
attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
|
||||
attrs[1].offset = sizeof(float) * 2;
|
||||
|
||||
SDL_GPUVertexInputState vertex_input{};
|
||||
vertex_input.vertex_buffer_descriptions = &vbo_desc;
|
||||
vertex_input.num_vertex_buffers = 1;
|
||||
vertex_input.vertex_attributes = attrs;
|
||||
vertex_input.num_vertex_attributes = 2;
|
||||
|
||||
// Color target = formato pasado por el caller (offscreen u otro).
|
||||
// Blending alpha estándar.
|
||||
SDL_GPUColorTargetDescription color_target{};
|
||||
color_target.format = target_format;
|
||||
color_target.blend_state.enable_blend = true;
|
||||
color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
|
||||
color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
color_target.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||
color_target.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
|
||||
color_target.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
color_target.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||
color_target.blend_state.color_write_mask =
|
||||
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
|
||||
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
|
||||
|
||||
SDL_GPUGraphicsPipelineTargetInfo target_info{};
|
||||
target_info.color_target_descriptions = &color_target;
|
||||
target_info.num_color_targets = 1;
|
||||
target_info.has_depth_stencil_target = false;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo info{};
|
||||
info.vertex_shader = vert;
|
||||
info.fragment_shader = frag;
|
||||
info.vertex_input_state = vertex_input;
|
||||
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
|
||||
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; // No backface culling para 2D
|
||||
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
|
||||
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
info.depth_stencil_state = {};
|
||||
info.target_info = target_info;
|
||||
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
|
||||
|
||||
// Los shaders se pueden liberar tras crear el pipeline (SDL los retiene
|
||||
// internamente mientras el pipeline esté vivo).
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
|
||||
if (pipeline_ == nullptr) {
|
||||
std::cerr << "[GpuLinePipeline] SDL_CreateGPUGraphicsPipeline: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuLinePipeline::destroy() {
|
||||
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
|
||||
}
|
||||
pipeline_ = nullptr;
|
||||
owner_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,60 @@
|
||||
// gpu_line_pipeline.hpp - Pipeline gráfico para dibujar líneas vectoriales
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Las líneas se renderizan como quads (2 triángulos = 6 índices) ya extrudidos
|
||||
// en CPU según el grosor pedido por línea. Vertex layout: position (vec2) + color (vec4).
|
||||
// Primitive type: TRIANGLELIST. Sin depth test (juego 2D), blending alpha estándar.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
class GpuDevice;
|
||||
|
||||
// Vertex layout (debe coincidir con shaders/line.vert.glsl).
|
||||
struct LineVertex {
|
||||
float x;
|
||||
float y;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float a;
|
||||
};
|
||||
|
||||
// Uniform buffer del vertex shader (debe coincidir con UBO en line.vert.glsl).
|
||||
struct LineUniforms {
|
||||
float viewport_width;
|
||||
float viewport_height;
|
||||
float padding_0;
|
||||
float padding_1; // Alineamiento a 16 bytes
|
||||
};
|
||||
|
||||
class GpuLinePipeline {
|
||||
public:
|
||||
GpuLinePipeline() = default;
|
||||
~GpuLinePipeline();
|
||||
|
||||
GpuLinePipeline(const GpuLinePipeline&) = delete;
|
||||
auto operator=(const GpuLinePipeline&) -> GpuLinePipeline& = delete;
|
||||
GpuLinePipeline(GpuLinePipeline&&) = delete;
|
||||
auto operator=(GpuLinePipeline&&) -> GpuLinePipeline& = delete;
|
||||
|
||||
// target_format: formato del color target sobre el que renderizamos
|
||||
// (swapchain o offscreen). Por defecto coincide con el del swapchain
|
||||
// del device.
|
||||
[[nodiscard]] auto init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool;
|
||||
void destroy();
|
||||
|
||||
[[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; }
|
||||
|
||||
private:
|
||||
SDL_GPUDevice* owner_{nullptr}; // No-owning; el GpuDevice es el dueño
|
||||
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,98 @@
|
||||
// gpu_postfx_pipeline.cpp - Implementación del pipeline de postprocesado.
|
||||
|
||||
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); }
|
||||
|
||||
auto GpuPostFxPipeline::init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool {
|
||||
owner_ = device.get();
|
||||
if (owner_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// El vertex shader no usa UBO (emite tres vértices hardcodeados).
|
||||
// El fragment shader usa 1 sampler (escena) y 1 UBO (parámetros postpro).
|
||||
SDL_GPUShader* vert = device.loadShader("postfx.vert.spv",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX,
|
||||
/*num_uniform_buffers=*/0,
|
||||
/*num_samplers=*/0);
|
||||
SDL_GPUShader* frag = device.loadShader("postfx.frag.spv",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||
/*num_uniform_buffers=*/1,
|
||||
/*num_samplers=*/1);
|
||||
if ((vert == nullptr) || (frag == nullptr)) {
|
||||
if (vert != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
}
|
||||
if (frag != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
}
|
||||
std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sin vertex input: los tres vértices del triángulo se generan en el shader.
|
||||
SDL_GPUVertexInputState vertex_input{};
|
||||
vertex_input.vertex_buffer_descriptions = nullptr;
|
||||
vertex_input.num_vertex_buffers = 0;
|
||||
vertex_input.vertex_attributes = nullptr;
|
||||
vertex_input.num_vertex_attributes = 0;
|
||||
|
||||
// Color target del postpro = swapchain. Sin blending: el postpro reescribe
|
||||
// píxeles directamente (la mezcla con la escena ya se hizo dentro del shader).
|
||||
SDL_GPUColorTargetDescription color_target{};
|
||||
color_target.format = target_format;
|
||||
color_target.blend_state.enable_blend = false;
|
||||
color_target.blend_state.color_write_mask =
|
||||
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
|
||||
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
|
||||
|
||||
SDL_GPUGraphicsPipelineTargetInfo target_info{};
|
||||
target_info.color_target_descriptions = &color_target;
|
||||
target_info.num_color_targets = 1;
|
||||
target_info.has_depth_stencil_target = false;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo info{};
|
||||
info.vertex_shader = vert;
|
||||
info.fragment_shader = frag;
|
||||
info.vertex_input_state = vertex_input;
|
||||
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
|
||||
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
|
||||
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
|
||||
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
info.depth_stencil_state = {};
|
||||
info.target_info = target_info;
|
||||
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
|
||||
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
|
||||
if (pipeline_ == nullptr) {
|
||||
std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: "
|
||||
<< SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuPostFxPipeline::destroy() {
|
||||
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
|
||||
}
|
||||
pipeline_ = nullptr;
|
||||
owner_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,72 @@
|
||||
// gpu_postfx_pipeline.hpp - Pipeline de postprocesado (fullscreen triangle)
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Pase final del frame: muestrea la escena renderizada en offscreen y aplica
|
||||
// bloom + flicker + background pulse en el fragment shader. El vertex shader
|
||||
// emite un único triángulo que cubre toda la pantalla, así que el draw no
|
||||
// necesita vertex buffer (DrawPrimitives con vertex_count=3).
|
||||
//
|
||||
// Recursos del shader (SDL_gpu set bindings):
|
||||
// fragment set=2, binding=0 → sampler2D (escena offscreen)
|
||||
// fragment set=3, binding=0 → uniform buffer (parámetros del postpro)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
class GpuDevice;
|
||||
|
||||
// Uniform buffer del postpro. Debe coincidir EXACTAMENTE con
|
||||
// shaders/postfx.frag.glsl (layout std140 con vec4 alineadas a 16 bytes).
|
||||
struct PostFxUniforms {
|
||||
float time; // Tiempo acumulado en segundos
|
||||
float bloom_intensity; // Mezcla bloom (0..2)
|
||||
float bloom_threshold; // Luminancia mínima high-pass (0..1)
|
||||
float bloom_radius_px; // Radio del kernel en píxeles lógicos
|
||||
|
||||
float flicker_amplitude; // Profundidad del flicker (0..1)
|
||||
float flicker_frequency_hz; // Hz
|
||||
float background_pulse_freq_hz; // Hz
|
||||
float pad_a_;
|
||||
|
||||
float background_min_r; // Color min RGB en [0..1], A=1
|
||||
float background_min_g;
|
||||
float background_min_b;
|
||||
float background_min_a;
|
||||
|
||||
float background_max_r;
|
||||
float background_max_g;
|
||||
float background_max_b;
|
||||
float background_max_a;
|
||||
|
||||
float texel_size_x; // 1.0 / texture_width
|
||||
float texel_size_y;
|
||||
float pad_b_;
|
||||
float pad_c_;
|
||||
};
|
||||
|
||||
class GpuPostFxPipeline {
|
||||
public:
|
||||
GpuPostFxPipeline() = default;
|
||||
~GpuPostFxPipeline();
|
||||
|
||||
GpuPostFxPipeline(const GpuPostFxPipeline&) = delete;
|
||||
auto operator=(const GpuPostFxPipeline&) -> GpuPostFxPipeline& = delete;
|
||||
GpuPostFxPipeline(GpuPostFxPipeline&&) = delete;
|
||||
auto operator=(GpuPostFxPipeline&&) -> GpuPostFxPipeline& = delete;
|
||||
|
||||
// target_format: formato del color target del pase final (swapchain).
|
||||
[[nodiscard]] auto init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool;
|
||||
void destroy();
|
||||
|
||||
[[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; }
|
||||
|
||||
private:
|
||||
SDL_GPUDevice* owner_{nullptr};
|
||||
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -1,102 +1,55 @@
|
||||
// line_renderer.cpp - Implementació de renderitzat de línies
|
||||
// © 1999 Visente i Sergi (versió Pascal)
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// line_renderer.cpp - Implementación de renderizado de líneas (SDL3 GPU)
|
||||
// © 1999 Visente i Sergi (versión Pascal)
|
||||
// © 2025 Port a C++20 con 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
|
||||
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
|
||||
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
|
||||
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
|
||||
SDL_Color g_current_line_color = {100, 255, 100, 255};
|
||||
|
||||
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
|
||||
// Grosor global por defecto. Configurable via setLineThickness.
|
||||
// 1.5 da una línea visible y crujiente; 1.0 se ve demasiado fino en pantallas grandes.
|
||||
float g_current_line_thickness = 1.5F;
|
||||
|
||||
// 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));
|
||||
void linea(Renderer* renderer,
|
||||
int x1, int y1, int x2, int y2,
|
||||
float brightness,
|
||||
float thickness,
|
||||
SDL_Color color) {
|
||||
if (renderer == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
*/
|
||||
// Coords lógicas (1280×720). El shader hace el mapeo a NDC; el viewport
|
||||
// del SDLManager hace el letterbox a píxeles físicos.
|
||||
const float FX1 = static_cast<float>(x1);
|
||||
const float FY1 = static_cast<float>(y1);
|
||||
const float FX2 = static_cast<float>(x2);
|
||||
const float FY2 = static_cast<float>(y2);
|
||||
|
||||
return colisio;
|
||||
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
|
||||
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
|
||||
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
|
||||
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
|
||||
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
|
||||
|
||||
const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness;
|
||||
|
||||
renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, 1.0F);
|
||||
}
|
||||
|
||||
// [NUEVO] Establir el color global de les línies
|
||||
void setLineColor(SDL_Color color) { g_current_line_color = color; }
|
||||
|
||||
void setLineThickness(float thickness) {
|
||||
if (thickness > 0.0F) {
|
||||
g_current_line_thickness = thickness;
|
||||
}
|
||||
}
|
||||
|
||||
auto getLineThickness() -> float { return g_current_line_thickness; }
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
// line_renderer.hpp - Renderitzat de línies
|
||||
// © 1999 Visente i Sergi (versió Pascal)
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// line_renderer.hpp - Renderizado de líneas vectoriales (SDL3 GPU)
|
||||
// © 1999 Visente i Sergi (versión Pascal)
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
//
|
||||
// El dibujo de líneas pasa por el pipeline GPU. Las coordenadas (x1,y1,x2,y2)
|
||||
// son lógicas (1280×720); el shader las mapea a NDC y el viewport del SDLManager
|
||||
// hace el letterbox a píxeles físicos. El brillo modula el color global de
|
||||
// línea (lo gestiona ColorOscillator). El grosor es configurable por línea
|
||||
// (parámetro thickness>0) o global (g_current_line_thickness vía setLineThickness).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#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ó)
|
||||
// Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720).
|
||||
// brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo).
|
||||
// thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness.
|
||||
// color: si alpha==0, se usa el color global del oscilador; si alpha>0 se
|
||||
// usa este color directo (paleta semántica por entidad).
|
||||
void linea(Renderer* renderer,
|
||||
int x1, int y1, int x2, int y2,
|
||||
float brightness = 1.0F,
|
||||
float thickness = 0.0F,
|
||||
SDL_Color color = {0, 0, 0, 0});
|
||||
|
||||
// Color global de las líneas (lo actualiza ColorOscillator vía SDLManager).
|
||||
void setLineColor(SDL_Color color);
|
||||
|
||||
// Grosor global por defecto (en píxeles lógicos). Default: 1.5.
|
||||
void setLineThickness(float thickness);
|
||||
[[nodiscard]] auto getLineThickness() -> float;
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// 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
|
||||
@@ -1,22 +0,0 @@
|
||||
// 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
|
||||
@@ -1,66 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,22 @@
|
||||
// render_context.hpp - Alias del contexto de rendering del juego
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Punto único de indireción entre el resto del código y el backend de
|
||||
// rendering. El juego habla con un `Rendering::Renderer*` opaco; el alias
|
||||
// apunta a la implementación concreta (SDL3 GPU vía GpuFrameRenderer).
|
||||
//
|
||||
// Antes (BETA 3.0): se propagaba un `SDL_Renderer*` por todas las firmas.
|
||||
// Ahora: `Rendering::Renderer*`. El cambio de backend (Vulkan→Metal vía
|
||||
// SDL_gpu) no toca el código del juego.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
// Tipo único del contexto de dibujo que se pasa a las entidades, scenes,
|
||||
// effects, etc. Las llamadas concretas viven en GpuFrameRenderer.
|
||||
using Renderer = GPU::GpuFrameRenderer;
|
||||
|
||||
} // namespace Rendering
|
||||
@@ -1,12 +1,15 @@
|
||||
// sdl_manager.cpp - Implementació del gestor SDL3
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "sdl_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <iostream>
|
||||
|
||||
#include "core/config/postfx_config.hpp"
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "core/rendering/coordinate_transform.hpp"
|
||||
@@ -14,12 +17,53 @@
|
||||
#include "game/options.hpp"
|
||||
#include "project.h"
|
||||
|
||||
namespace {
|
||||
auto initWindowAndGpu(SDL_Window** out_window,
|
||||
Rendering::Renderer& gpu_renderer,
|
||||
int width, int height, bool fullscreen) -> bool {
|
||||
// Título estático estilo CCAE. El FPS y el estado de VSync los muestra
|
||||
// el DebugOverlay (toggle F11), no la barra de título.
|
||||
const std::string TITLE = std::format("© 2026 {} — JailDesigner",
|
||||
Project::LONG_NAME);
|
||||
|
||||
SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE;
|
||||
if (fullscreen) {
|
||||
flags = static_cast<SDL_WindowFlags>(flags | SDL_WINDOW_FULLSCREEN);
|
||||
}
|
||||
|
||||
SDL_Window* window = SDL_CreateWindow(TITLE.c_str(), width, height, flags);
|
||||
if (window == nullptr) {
|
||||
std::cerr << "Error creant finestra: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fullscreen) {
|
||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
}
|
||||
|
||||
// Inicializar el FrameRenderer (claim del window + pipeline de líneas).
|
||||
if (!gpu_renderer.init(window,
|
||||
static_cast<float>(Defaults::Game::WIDTH),
|
||||
static_cast<float>(Defaults::Game::HEIGHT))) {
|
||||
std::cerr << "Error inicialitzant GpuFrameRenderer\n";
|
||||
SDL_DestroyWindow(window);
|
||||
return false;
|
||||
}
|
||||
|
||||
gpu_renderer.setVSync(Options::rendering.vsync != 0);
|
||||
|
||||
// Cargar parámetros del postpro desde el resource pack. Si el YAML falta
|
||||
// o falla, el loader devuelve los defaults built-in (bloom suave + flicker
|
||||
// sutil + background verde tenue).
|
||||
gpu_renderer.setPostFx(Config::PostFx::load("config/postfx.yaml"));
|
||||
|
||||
*out_window = window;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SDLManager::SDLManager()
|
||||
: finestra_(nullptr),
|
||||
renderer_(nullptr),
|
||||
fps_accumulator_(0.0F),
|
||||
fps_frame_count_(0),
|
||||
fps_display_(0),
|
||||
current_width_(Defaults::Window::WIDTH),
|
||||
current_height_(Defaults::Window::HEIGHT),
|
||||
is_fullscreen_(false),
|
||||
@@ -29,61 +73,27 @@ SDLManager::SDLManager()
|
||||
windowed_width_(Defaults::Window::WIDTH),
|
||||
windowed_height_(Defaults::Window::HEIGHT),
|
||||
max_zoom_(1.0F) {
|
||||
// Inicialitzar SDL3
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// Calcular mida màxima des del display
|
||||
calculateMaxWindowSize();
|
||||
|
||||
// Construir títol dinàmic
|
||||
std::string window_title = std::format("{} v{} ({})", Project::LONG_NAME, Project::VERSION, Project::COPYRIGHT);
|
||||
|
||||
// Crear finestra CENTRADA (SDL ho fa automàticament amb CENTERED)
|
||||
finestra_ =
|
||||
SDL_CreateWindow(window_title.c_str(), current_width_, current_height_,
|
||||
SDL_WINDOW_RESIZABLE // Permetre resize manual també
|
||||
);
|
||||
|
||||
if (finestra_ == nullptr) {
|
||||
std::cerr << "Error creant finestra: " << SDL_GetError() << '\n';
|
||||
if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, false)) {
|
||||
SDL_Quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// IMPORTANT: Centrar explícitament la finestra
|
||||
SDL_SetWindowPosition(finestra_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
|
||||
// Crear renderer amb acceleració
|
||||
renderer_ = SDL_CreateRenderer(finestra_, nullptr);
|
||||
|
||||
if (renderer_ == nullptr) {
|
||||
std::cerr << "Error creant renderer: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyWindow(finestra_);
|
||||
SDL_Quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Aplicar configuració de V-Sync
|
||||
SDL_SetRenderVSync(renderer_, Options::rendering.vsync);
|
||||
|
||||
// CRÍTIC: Configurar viewport scaling
|
||||
updateLogicalPresentation();
|
||||
updateViewport();
|
||||
|
||||
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
|
||||
<< " (logic: " << Defaults::Game::WIDTH << "x"
|
||||
<< Defaults::Game::HEIGHT << ")" << '\n';
|
||||
}
|
||||
|
||||
// Constructor amb configuració
|
||||
SDLManager::SDLManager(int width, int height, bool fullscreen)
|
||||
: finestra_(nullptr),
|
||||
renderer_(nullptr),
|
||||
fps_accumulator_(0.0F),
|
||||
fps_frame_count_(0),
|
||||
fps_display_(0),
|
||||
current_width_(width),
|
||||
current_height_(height),
|
||||
is_fullscreen_(fullscreen),
|
||||
@@ -93,56 +103,21 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
|
||||
windowed_width_(width),
|
||||
windowed_height_(height),
|
||||
max_zoom_(1.0F) {
|
||||
// Inicialitzar SDL3
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// Calcular mida màxima des del display
|
||||
calculateMaxWindowSize();
|
||||
|
||||
// Construir títol dinàmic
|
||||
std::string window_title = std::format("{} v{} ({})", Project::LONG_NAME, Project::VERSION, Project::COPYRIGHT);
|
||||
|
||||
// Configurar flags de la finestra
|
||||
SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE;
|
||||
if (is_fullscreen_) {
|
||||
flags = static_cast<SDL_WindowFlags>(flags | SDL_WINDOW_FULLSCREEN);
|
||||
}
|
||||
|
||||
// Crear finestra
|
||||
finestra_ = SDL_CreateWindow(window_title.c_str(), current_width_, current_height_, flags);
|
||||
|
||||
if (finestra_ == nullptr) {
|
||||
std::cerr << "Error creant finestra: " << SDL_GetError() << '\n';
|
||||
if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, is_fullscreen_)) {
|
||||
SDL_Quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Centrar explícitament la finestra (si no és fullscreen)
|
||||
if (!is_fullscreen_) {
|
||||
SDL_SetWindowPosition(finestra_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
}
|
||||
updateViewport();
|
||||
|
||||
// Crear renderer amb acceleració
|
||||
renderer_ = SDL_CreateRenderer(finestra_, nullptr);
|
||||
|
||||
if (renderer_ == nullptr) {
|
||||
std::cerr << "Error creant renderer: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyWindow(finestra_);
|
||||
SDL_Quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Aplicar configuració de V-Sync
|
||||
SDL_SetRenderVSync(renderer_, Options::rendering.vsync);
|
||||
|
||||
// Configurar viewport scaling
|
||||
updateLogicalPresentation();
|
||||
|
||||
// Inicialitzar sistema de cursor
|
||||
// En fullscreen: forzar ocultació permanent
|
||||
// En fullscreen: forzar ocultació permanent del cursor.
|
||||
if (is_fullscreen_) {
|
||||
Mouse::setForceHidden(true);
|
||||
}
|
||||
@@ -157,10 +132,7 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
|
||||
}
|
||||
|
||||
SDLManager::~SDLManager() {
|
||||
if (renderer_ != nullptr) {
|
||||
SDL_DestroyRenderer(renderer_);
|
||||
renderer_ = nullptr;
|
||||
}
|
||||
gpu_renderer_.destroy();
|
||||
|
||||
if (finestra_ != nullptr) {
|
||||
SDL_DestroyWindow(finestra_);
|
||||
@@ -176,36 +148,27 @@ void SDLManager::calculateMaxWindowSize() {
|
||||
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
|
||||
|
||||
if (mode != nullptr) {
|
||||
// Deixar marge de 100px per a decoracions de l'OS
|
||||
// Deixar marge de 100px para decoracions de l'OS
|
||||
max_width_ = mode->w - 100;
|
||||
max_height_ = mode->h - 100;
|
||||
std::cout << "Display detectat: " << mode->w << "x" << mode->h
|
||||
<< " (max finestra: " << max_width_ << "x" << max_height_ << ")"
|
||||
<< '\n';
|
||||
} else {
|
||||
// Fallback conservador
|
||||
max_width_ = 1920;
|
||||
max_height_ = 1080;
|
||||
std::cerr << "No s'ha pogut detectar el display, usant fallback: "
|
||||
<< 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: "
|
||||
@@ -213,37 +176,25 @@ void SDLManager::calculateMaxZoom() {
|
||||
}
|
||||
|
||||
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_));
|
||||
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_;
|
||||
@@ -252,30 +203,23 @@ void SDLManager::applyZoom(float new_zoom) {
|
||||
<< new_width << "x" << new_height << ")" << '\n';
|
||||
}
|
||||
|
||||
void SDLManager::updateLogicalPresentation() {
|
||||
// CANVIAT: Ja no usem SDL_SetRenderLogicalPresentation
|
||||
// Ara renderitzem directament a resolució física per evitar pixelació irregular
|
||||
// El viewport amb letterbox es configura a updateViewport()
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
void SDLManager::updateViewport() {
|
||||
// Calcular dimensions físiques basades en el zoom
|
||||
// Cálculo de letterbox: el juego se renderiza a 1280×720 lógicos, pero
|
||||
// la swapchain tiene el tamaño físico de la ventana. Aplicamos un viewport
|
||||
// centrado con la proporción 16:9 para preservar aspect ratio.
|
||||
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);
|
||||
gpu_renderer_.setViewport(static_cast<float>(offset_x),
|
||||
static_cast<float>(offset_y),
|
||||
static_cast<float>(scaled_width),
|
||||
static_cast<float>(scaled_height));
|
||||
|
||||
std::cout << "Viewport: " << scaled_width << "x" << scaled_height
|
||||
<< " @ (" << offset_x << "," << offset_y << ") [scale=" << scale << "]"
|
||||
@@ -283,7 +227,6 @@ void SDLManager::updateViewport() {
|
||||
}
|
||||
|
||||
void SDLManager::updateRenderingContext() const {
|
||||
// Actualitzar el factor d'escala global per a totes les funcions de renderitzat
|
||||
Rendering::g_current_scale_factor = zoom_factor_;
|
||||
}
|
||||
|
||||
@@ -291,10 +234,8 @@ void SDLManager::increaseWindowSize() {
|
||||
if (is_fullscreen_) {
|
||||
return;
|
||||
}
|
||||
|
||||
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
|
||||
applyZoom(new_zoom);
|
||||
|
||||
std::cout << "F2: Zoom aumentat a " << zoom_factor_ << "x" << '\n';
|
||||
}
|
||||
|
||||
@@ -302,15 +243,12 @@ void SDLManager::decreaseWindowSize() {
|
||||
if (is_fullscreen_) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
// Obtenir posició actual ABANS del resize
|
||||
int old_x;
|
||||
int old_y;
|
||||
SDL_GetWindowPosition(finestra_, &old_x, &old_y);
|
||||
@@ -318,76 +256,56 @@ void SDLManager::applyWindowSize(int new_width, int new_height) {
|
||||
int old_width = current_width_;
|
||||
int old_height = current_height_;
|
||||
|
||||
// Actualitzar mida
|
||||
SDL_SetWindowSize(finestra_, new_width, new_height);
|
||||
current_width_ = new_width;
|
||||
current_height_ = new_height;
|
||||
|
||||
// CENTRADO INTEL·LIGENT (algoritme de pollo)
|
||||
// Calcular nova posició per mantenir la finestra centrada sobre si mateixa
|
||||
int delta_width = old_width - new_width;
|
||||
int delta_height = old_height - new_height;
|
||||
|
||||
int new_x = old_x + (delta_width / 2);
|
||||
int new_y = old_y + (delta_height / 2);
|
||||
|
||||
// Evitar que la finestra surti de la pantalla
|
||||
constexpr int TITLEBAR_HEIGHT = 35; // Alçada aproximada de la barra de títol
|
||||
constexpr int TITLEBAR_HEIGHT = 35;
|
||||
new_x = std::max(new_x, 0);
|
||||
new_y = std::max(new_y, TITLEBAR_HEIGHT);
|
||||
|
||||
SDL_SetWindowPosition(finestra_, new_x, new_y);
|
||||
|
||||
// Actualitzar viewport després del resize
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
void SDLManager::toggleFullscreen() {
|
||||
if (!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';
|
||||
}
|
||||
|
||||
Options::window.fullscreen = is_fullscreen_;
|
||||
|
||||
// Notificar al mòdul Mouse: Fullscreen requereix ocultació permanent del cursor.
|
||||
// Quan es surt de fullscreen, restaurar el comportament normal d'auto-ocultació.
|
||||
Mouse::setForceHidden(is_fullscreen_);
|
||||
}
|
||||
|
||||
bool SDLManager::handleWindowEvent(const SDL_Event& event) {
|
||||
auto SDLManager::handleWindowEvent(const SDL_Event& event) -> bool {
|
||||
if (event.type == SDL_EVENT_WINDOW_RESIZED) {
|
||||
SDL_GetWindowSize(finestra_, ¤t_width_, ¤t_height_);
|
||||
|
||||
// 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_
|
||||
@@ -398,84 +316,22 @@ bool SDLManager::handleWindowEvent(const SDL_Event& event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void SDLManager::neteja(uint8_t r, uint8_t g, uint8_t b) {
|
||||
if (renderer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// [MODIFICAT] Usar color oscil·lat del fons en lloc dels paràmetres
|
||||
void SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) {
|
||||
// El fondo lo dibuja ahora el shader de postpro (background pulse). El
|
||||
// offscreen se limpia en negro dentro de beginFrame. Los argumentos r/g/b
|
||||
// se mantienen por compatibilidad de API.
|
||||
(void)r;
|
||||
(void)g;
|
||||
(void)b; // Suprimir warnings
|
||||
SDL_Color bg = color_oscillator_.getCurrentBackgroundColor();
|
||||
SDL_SetRenderDrawColor(renderer_, bg.r, bg.g, bg.b, 255);
|
||||
SDL_RenderClear(renderer_);
|
||||
(void)b;
|
||||
gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F);
|
||||
}
|
||||
|
||||
void SDLManager::presenta() {
|
||||
if (renderer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_RenderPresent(renderer_);
|
||||
void SDLManager::present() {
|
||||
gpu_renderer_.endFrame();
|
||||
}
|
||||
|
||||
// [NUEVO] Actualitzar colors amb oscil·lació
|
||||
void SDLManager::updateColors(float delta_time) {
|
||||
color_oscillator_.update(delta_time);
|
||||
|
||||
// Actualitzar color global de línies
|
||||
Rendering::setLineColor(color_oscillator_.getCurrentLineColor());
|
||||
}
|
||||
|
||||
// [NUEVO] Actualitzar comptador de FPS
|
||||
void SDLManager::updateFPS(float delta_time) {
|
||||
// Acumular temps i frames
|
||||
fps_accumulator_ += delta_time;
|
||||
fps_frame_count_++;
|
||||
|
||||
// Actualitzar display cada 0.5 segons
|
||||
if (fps_accumulator_ >= 0.5F) {
|
||||
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_);
|
||||
fps_frame_count_ = 0;
|
||||
fps_accumulator_ = 0.0F;
|
||||
|
||||
// Actualitzar títol de la finestra
|
||||
std::string vsync_state = (Options::rendering.vsync == 1) ? "ON" : "OFF";
|
||||
std::string title = std::format("{} v{} ({}) - {} FPS - VSync: {}",
|
||||
Project::LONG_NAME,
|
||||
Project::VERSION,
|
||||
Project::COPYRIGHT,
|
||||
fps_display_,
|
||||
vsync_state);
|
||||
|
||||
if (finestra_ != nullptr) {
|
||||
SDL_SetWindowTitle(finestra_, title.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [NUEVO] Actualitzar títol de la finestra
|
||||
void SDLManager::setWindowTitle(const std::string& title) {
|
||||
if (finestra_ != nullptr) {
|
||||
SDL_SetWindowTitle(finestra_, title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// [NUEVO] Toggle V-Sync (F4)
|
||||
void SDLManager::toggleVSync() {
|
||||
// Toggle: 1 → 0 → 1
|
||||
Options::rendering.vsync = (Options::rendering.vsync == 1) ? 0 : 1;
|
||||
|
||||
// Aplicar a SDL
|
||||
if (renderer_ != nullptr) {
|
||||
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ó
|
||||
gpu_renderer_.setVSync(Options::rendering.vsync != 0);
|
||||
Options::saveToFile();
|
||||
}
|
||||
|
||||
@@ -1,61 +1,50 @@
|
||||
// sdl_manager.hpp - Gestor d'inicialització de SDL3
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// sdl_manager.hpp - Gestor de inicialización de SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
//
|
||||
// Tras la Fase 7 de la migración, el rendering ya no usa SDL_Renderer:
|
||||
// SDLManager posee un GpuFrameRenderer (SDL3 GPU) que es el contexto único
|
||||
// de dibujo del juego. El resto del código accede vía getRenderer() →
|
||||
// Rendering::Renderer* (alias del GpuFrameRenderer).
|
||||
|
||||
#ifndef SDL_MANAGER_HPP
|
||||
#define SDL_MANAGER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "core/rendering/color_oscillator.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
class SDLManager {
|
||||
public:
|
||||
SDLManager(); // Constructor per defecte (usa Defaults::)
|
||||
SDLManager(int width, int height, bool fullscreen); // Constructor amb configuració
|
||||
SDLManager(int width, int height, bool fullscreen); // Constructor con configuración
|
||||
~SDLManager();
|
||||
|
||||
// No permetre còpia ni assignació
|
||||
SDLManager(const SDLManager&) = delete;
|
||||
SDLManager& operator=(const SDLManager&) = delete;
|
||||
auto operator=(const SDLManager&) -> SDLManager& = delete;
|
||||
|
||||
// [NUEVO] Gestió de finestra dinàmica
|
||||
void increaseWindowSize(); // F2: +100px
|
||||
void decreaseWindowSize(); // F1: -100px
|
||||
void toggleFullscreen(); // F3
|
||||
void toggleVSync(); // F4
|
||||
bool handleWindowEvent(const SDL_Event& event); // Per a SDL_EVENT_WINDOW_RESIZED
|
||||
auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED
|
||||
|
||||
// Funcions principals (renderitzat)
|
||||
void neteja(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);
|
||||
void presenta();
|
||||
|
||||
// [NUEVO] Actualització de colors (oscil·lació)
|
||||
void updateColors(float delta_time);
|
||||
|
||||
// [NUEVO] Actualitzar comptador de FPS
|
||||
void updateFPS(float delta_time);
|
||||
// Funciones principals (renderizado)
|
||||
void clear(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);
|
||||
void present();
|
||||
|
||||
// Getters
|
||||
SDL_Renderer* obte_renderer() { return renderer_; }
|
||||
[[nodiscard]] float getScaleFactor() const { return zoom_factor_; }
|
||||
auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
|
||||
[[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; }
|
||||
|
||||
// [NUEVO] Actualitzar títol de la finestra
|
||||
void setWindowTitle(const std::string& title);
|
||||
|
||||
// [NUEVO] Actualitzar context de renderitzat (factor d'escala global)
|
||||
// [NUEVO] Actualitzar context de renderizado (factor de scale global)
|
||||
void updateRenderingContext() const;
|
||||
|
||||
private:
|
||||
SDL_Window* finestra_;
|
||||
SDL_Renderer* renderer_;
|
||||
|
||||
// [NUEVO] Variables FPS
|
||||
float fps_accumulator_;
|
||||
int fps_frame_count_;
|
||||
int fps_display_;
|
||||
Rendering::Renderer gpu_renderer_; // GpuFrameRenderer (SDL3 GPU)
|
||||
|
||||
// [NUEVO] Estat de la finestra
|
||||
int current_width_; // Mida física actual
|
||||
@@ -70,16 +59,11 @@ class SDLManager {
|
||||
int windowed_height_; // Saved size before fullscreen
|
||||
float max_zoom_; // Maximum zoom (calculated from display)
|
||||
|
||||
// [NUEVO] Funcions internes
|
||||
// [NUEVO] Funciones internes
|
||||
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 updateLogicalPresentation(); // Actualitzar viewport
|
||||
void updateViewport(); // Configurar viewport amb letterbox
|
||||
void updateViewport(); // Configurar viewport con letterbox
|
||||
|
||||
// [NUEVO] Oscil·lador de colors
|
||||
Rendering::ColorOscillator color_oscillator_;
|
||||
};
|
||||
|
||||
#endif // SDL_MANAGER_HPP
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// shape_renderer.cpp - Implementació del renderitzat de formes
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// shape_renderer.cpp - Implementació del renderizado de formes
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
|
||||
@@ -10,108 +10,100 @@
|
||||
|
||||
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
|
||||
// Helper: aplicar rotación 3D a un point 2D (assumeix Z=0)
|
||||
static Vec2 apply_3d_rotation(float x, float y, const Rotation3D& rot) {
|
||||
float z = 0.0F; // Todos los points 2D comencen a Z=0
|
||||
|
||||
// Pitch (rotació eix X): cabeceo arriba/baix
|
||||
// Pitch (rotación 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
|
||||
// Yaw (rotación eix Y): guiñada izquierda/derecha
|
||||
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
|
||||
// Roll (rotación 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
|
||||
// Naves quieren hacia el point de fuga (320, 240) a "infinit" (Z → +∞)
|
||||
// Z més grande = més lluny = més pequeño 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
|
||||
// Helper: transformar un point con rotación, scale i traslación
|
||||
static Vec2 transform_point(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale, const Rotation3D* rotation_3d) {
|
||||
// 1. Centrar el point respecte al centro de la shape
|
||||
float centered_x = point.x - shape_centre.x;
|
||||
float centered_y = point.y - shape_centre.y;
|
||||
|
||||
// 2. Aplicar rotació 3D (si es proporciona)
|
||||
// 2. Aplicar rotación 3D (si es proporciona)
|
||||
if ((rotation_3d != nullptr) && rotation_3d->has_rotation()) {
|
||||
Punt rotated_3d = apply_3d_rotation(centered_x, centered_y, *rotation_3d);
|
||||
Vec2 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;
|
||||
// 3. Aplicar scale al point (después de rotación 3D)
|
||||
float scaled_x = centered_x * scale;
|
||||
float scaled_y = centered_y * scale;
|
||||
|
||||
// 4. Aplicar rotació 2D (Z-axis, tradicional)
|
||||
// IMPORTANT: En el sistema original, angle=0 apunta AMUNT (no dreta)
|
||||
// 4. Aplicar rotación 2D (Z-axis, tradicional)
|
||||
// IMPORTANT: En el sistema original, angle=0 apunta AMUNT (no derecha)
|
||||
// Per això usem (angle - PI/2) per compensar
|
||||
// Però aquí angle ja ve en el sistema correcte del joc
|
||||
// Pero aquí angle ya ve en el sistema correcte del juego
|
||||
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};
|
||||
// 5. Aplicar traslación a posición mundial
|
||||
return {.x = rotated_x + position.x, .y = rotated_y + position.y};
|
||||
}
|
||||
|
||||
void render_shape(SDL_Renderer* renderer,
|
||||
void render_shape(Rendering::Renderer* renderer,
|
||||
const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Punt& posicio,
|
||||
const Vec2& position,
|
||||
float angle,
|
||||
float escala,
|
||||
bool dibuixar,
|
||||
float scale,
|
||||
float progress,
|
||||
float brightness,
|
||||
const Rotation3D* rotation_3d) {
|
||||
// Verificar que la forma és vàlida
|
||||
if (!shape || !shape->es_valida()) {
|
||||
const Rotation3D* rotation_3d,
|
||||
SDL_Color color) {
|
||||
if (!shape || !shape->isValid()) {
|
||||
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();
|
||||
const Vec2& SHAPE_CENTRE = shape->getCenter();
|
||||
|
||||
// Iterar sobre totes les primitives
|
||||
for (const auto& primitive : shape->get_primitives()) {
|
||||
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
|
||||
// POLYLINE: connectar punts consecutius
|
||||
// POLYLINE: conectar puntos consecutivos.
|
||||
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);
|
||||
const Vec2 P1 = transform_point(primitive.points[i], SHAPE_CENTRE, position, angle, scale, rotation_3d);
|
||||
const Vec2 P2 = transform_point(primitive.points[i + 1], SHAPE_CENTRE, position, angle, scale, rotation_3d);
|
||||
linea(renderer, static_cast<int>(P1.x), static_cast<int>(P1.y),
|
||||
static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, 0.0F, color);
|
||||
}
|
||||
} else if (primitive.points.size() >= 2) { // LINE
|
||||
const Vec2 P1 = transform_point(primitive.points[0], SHAPE_CENTRE, position, angle, scale, rotation_3d);
|
||||
const Vec2 P2 = transform_point(primitive.points[1], SHAPE_CENTRE, position, angle, scale, rotation_3d);
|
||||
linea(renderer, static_cast<int>(P1.x), static_cast<int>(P1.y),
|
||||
static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, 0.0F, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// shape_renderer.hpp - Renderitzat de formes vectorials
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// shape_renderer.hpp - Renderizado de formes vectorials
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
@@ -14,9 +16,9 @@ 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)
|
||||
float pitch; // Rotación eix X (cabeceo arriba/baix)
|
||||
float yaw; // Rotación eix Y (guiñada izquierda/derecha)
|
||||
float roll; // Rotación eix Z (alabeo lateral)
|
||||
|
||||
Rotation3D()
|
||||
: pitch(0.0F),
|
||||
@@ -32,23 +34,22 @@ struct Rotation3D {
|
||||
}
|
||||
};
|
||||
|
||||
// Renderitzar forma amb transformacions
|
||||
// Renderizar shape con 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,
|
||||
// - shape: shape vectorial a draw
|
||||
// - position: posición del centro en coordenades mundials
|
||||
// - angle: rotación en radians (0 = amunt, sentit horari)
|
||||
// - scale: factor de scale (1.0 = mida original)
|
||||
// - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible)
|
||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||
void render_shape(Rendering::Renderer* renderer,
|
||||
const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Punt& posicio,
|
||||
const Vec2& position,
|
||||
float angle,
|
||||
float escala = 1.0F,
|
||||
bool dibuixar = true,
|
||||
float scale = 1.0F,
|
||||
float progress = 1.0F,
|
||||
float brightness = 1.0F,
|
||||
const Rotation3D* rotation_3d = nullptr);
|
||||
const Rotation3D* rotation_3d = nullptr,
|
||||
SDL_Color color = {0, 0, 0, 0}); // alpha==0 → usa global oscilador
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// resource_helper.cpp - Implementació de funcions d'ajuda
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// resource_helper.cpp - Implementació de funciones de ajuda
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "resource_helper.hpp"
|
||||
|
||||
@@ -15,7 +15,7 @@ bool initializeResourceSystem(const std::string& pack_file, bool fallback) {
|
||||
return Loader::get().initialize(pack_file, fallback);
|
||||
}
|
||||
|
||||
// Carregar un fitxer
|
||||
// Carregar un file
|
||||
std::vector<uint8_t> loadFile(const std::string& filepath) {
|
||||
// Normalitzar la ruta
|
||||
std::string normalized = normalizePath(filepath);
|
||||
@@ -24,7 +24,7 @@ std::vector<uint8_t> loadFile(const std::string& filepath) {
|
||||
return Loader::get().loadResource(normalized);
|
||||
}
|
||||
|
||||
// Comprovar si existeix un fitxer
|
||||
// Comprovar si existeix un file
|
||||
bool fileExists(const std::string& filepath) {
|
||||
std::string normalized = normalizePath(filepath);
|
||||
return Loader::get().resourceExists(normalized);
|
||||
@@ -37,7 +37,7 @@ std::string getPackPath(const std::string& asset_path) {
|
||||
|
||||
// Eliminar rutes absolutes (detectar / o C:\ al principi)
|
||||
if (!path.empty() && path[0] == '/') {
|
||||
// Buscar "data/" i agafar el que ve després
|
||||
// Buscar "data/" i agafar el que ve después
|
||||
size_t data_pos = path.find("/data/");
|
||||
if (data_pos != std::string::npos) {
|
||||
path = path.substr(data_pos + 6); // Saltar "/data/"
|
||||
@@ -73,7 +73,7 @@ std::string normalizePath(const std::string& path) {
|
||||
return getPackPath(path);
|
||||
}
|
||||
|
||||
// Comprovar si hi ha paquet carregat
|
||||
// Comprovar si hay paquet carregat
|
||||
bool isPackLoaded() {
|
||||
return Loader::get().isPackLoaded();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// resource_helper.hpp - Funcions d'ajuda per gestió de recursos
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// resource_helper.hpp - Funciones de ajuda per gestió de recursos
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
// API simplificada i normalització de rutes
|
||||
|
||||
#pragma once
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
namespace Resource::Helper {
|
||||
|
||||
// Inicialització del sistema
|
||||
// Inicialización del sistema
|
||||
bool initializeResourceSystem(const std::string& pack_file, bool fallback);
|
||||
|
||||
// Càrrega de fitxers
|
||||
// Càrrega de archivos
|
||||
std::vector<uint8_t> loadFile(const std::string& filepath);
|
||||
bool fileExists(const std::string& filepath);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// resource_loader.cpp - Implementació del carregador de recursos
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "resource_loader.hpp"
|
||||
|
||||
@@ -19,18 +19,18 @@ Loader& Loader::get() {
|
||||
bool Loader::initialize(const std::string& pack_file, bool enable_fallback) {
|
||||
fallback_enabled_ = enable_fallback;
|
||||
|
||||
// Intentar carregar el paquet
|
||||
// Intentar load 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";
|
||||
std::cerr << "[ResourceLoader] ERROR FATAL: No es pot load " << pack_file
|
||||
<< " y 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
|
||||
std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de archivos\n";
|
||||
pack_.reset(); // No hay paquet
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ bool Loader::initialize(const std::string& pack_file, bool enable_fallback) {
|
||||
|
||||
// Carregar un recurs
|
||||
std::vector<uint8_t> Loader::loadResource(const std::string& filename) {
|
||||
// Intentar carregar del paquet primer
|
||||
// Intentar load del paquet primer
|
||||
if (pack_) {
|
||||
if (pack_->hasResource(filename)) {
|
||||
auto data = pack_->getResource(filename);
|
||||
@@ -51,7 +51,7 @@ std::vector<uint8_t> Loader::loadResource(const std::string& filename) {
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
// Si no està al paquet i no hi ha fallback, falla
|
||||
// Si no está al paquet y no hay fallback, falla
|
||||
if (!fallback_enabled_) {
|
||||
std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: "
|
||||
<< filename << "\n";
|
||||
@@ -59,7 +59,7 @@ std::vector<uint8_t> Loader::loadResource(const std::string& filename) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback al sistema de fitxers
|
||||
// Fallback al sistema de archivos
|
||||
if (fallback_enabled_) {
|
||||
return loadFromFilesystem(filename);
|
||||
}
|
||||
@@ -74,7 +74,7 @@ bool Loader::resourceExists(const std::string& filename) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Comprovar al sistema de fitxers si està activat el fallback
|
||||
// Comprovar al sistema de archivos 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);
|
||||
@@ -86,14 +86,14 @@ bool Loader::resourceExists(const std::string& filename) {
|
||||
// Validar el paquet
|
||||
bool Loader::validatePack() {
|
||||
if (!pack_) {
|
||||
std::cerr << "[ResourceLoader] Advertència: no hi ha paquet carregat per validar\n";
|
||||
std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
return pack_->validatePack();
|
||||
}
|
||||
|
||||
// Comprovar si hi ha paquet carregat
|
||||
// Comprovar si hay paquet carregat
|
||||
bool Loader::isPackLoaded() const {
|
||||
return pack_ != nullptr;
|
||||
}
|
||||
@@ -109,7 +109,7 @@ std::string Loader::getBasePath() const {
|
||||
return base_path_;
|
||||
}
|
||||
|
||||
// Carregar des del sistema de fitxers (fallback)
|
||||
// Carregar des del sistema de archivos (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/"
|
||||
@@ -136,7 +136,7 @@ std::vector<uint8_t> Loader::loadFromFilesystem(const std::string& filename) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::cout << "[ResourceLoader] Carregat des del sistema de fitxers: " << fullpath << "\n";
|
||||
std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n";
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
// Coordina càrrega des del paquet i/o sistema de archivos
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -18,7 +18,7 @@ class Loader {
|
||||
// Singleton
|
||||
static Loader& get();
|
||||
|
||||
// Inicialització
|
||||
// Inicialización
|
||||
bool initialize(const std::string& pack_file, bool enable_fallback);
|
||||
|
||||
// Càrrega de recursos
|
||||
@@ -46,7 +46,7 @@ class Loader {
|
||||
bool fallback_enabled_ = false;
|
||||
std::string base_path_;
|
||||
|
||||
// Funcions auxiliars
|
||||
// Funciones auxiliars
|
||||
std::vector<uint8_t> loadFromFilesystem(const std::string& filename);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// resource_pack.cpp - Implementació del sistema d'empaquetament
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// resource_pack.cpp - Implementació del sistema de empaquetament
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "resource_pack.hpp"
|
||||
|
||||
@@ -30,11 +30,11 @@ void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
}
|
||||
|
||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
// XOR és simètric
|
||||
// XOR es simètric
|
||||
encryptData(data, key);
|
||||
}
|
||||
|
||||
// Llegir fitxer complet a memòria
|
||||
// Llegir file 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) {
|
||||
@@ -54,7 +54,7 @@ std::vector<uint8_t> Pack::readFile(const std::string& filepath) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Afegir un fitxer individual al paquet
|
||||
// Añadir un file individual al paquet
|
||||
bool Pack::addFile(const std::string& filepath, const std::string& pack_name) {
|
||||
auto file_data = readFile(filepath);
|
||||
if (file_data.empty()) {
|
||||
@@ -67,17 +67,17 @@ bool Pack::addFile(const std::string& filepath, const std::string& pack_name) {
|
||||
.size = file_data.size(),
|
||||
.checksum = calculateChecksum(file_data)};
|
||||
|
||||
// Afegir dades al bloc de dades
|
||||
// Añadir 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()
|
||||
std::cout << "[ResourcePack] Añadido: " << pack_name << " (" << file_data.size()
|
||||
<< " bytes)\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Afegir tots els fitxers d'un directori recursivament
|
||||
// Añadir todos los archivos de un directori recursivament
|
||||
bool Pack::addDirectory(const std::string& dir_path,
|
||||
const std::string& base_path) {
|
||||
namespace fs = std::filesystem;
|
||||
@@ -100,7 +100,7 @@ bool Pack::addDirectory(const std::string& dir_path,
|
||||
// Convertir barres invertides a normals (Windows)
|
||||
std::ranges::replace(relative_path, '\\', '/');
|
||||
|
||||
// Saltar fitxers de desenvolupament
|
||||
// Saltar archivos 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 ||
|
||||
@@ -134,7 +134,7 @@ bool Pack::savePack(const std::string& pack_file) {
|
||||
|
||||
// Escriure metadades de recursos
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
// Nom del fitxer
|
||||
// Nom del file
|
||||
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);
|
||||
@@ -149,7 +149,7 @@ bool Pack::savePack(const std::string& pack_file) {
|
||||
std::vector<uint8_t> encrypted_data = data_;
|
||||
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
|
||||
|
||||
// Escriure mida de dades i dades encriptades
|
||||
// Escriure mida de dades y 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());
|
||||
@@ -160,7 +160,7 @@ bool Pack::savePack(const std::string& pack_file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Carregar paquet des de disc
|
||||
// Carregar paquet desde disc
|
||||
bool Pack::loadPack(const std::string& pack_file) {
|
||||
std::ifstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
@@ -180,7 +180,7 @@ bool Pack::loadPack(const std::string& pack_file) {
|
||||
uint32_t version;
|
||||
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||
if (version != VERSION) {
|
||||
std::cerr << "[ResourcePack] Error: versió incompatible (esperava " << VERSION
|
||||
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
|
||||
<< ", trobat " << version << ")\n";
|
||||
return false;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ bool Pack::loadPack(const std::string& pack_file) {
|
||||
// Llegir metadades de recursos
|
||||
resources_.clear();
|
||||
for (uint32_t i = 0; i < resource_count; ++i) {
|
||||
// Nom del fitxer
|
||||
// Nom del file
|
||||
uint32_t name_len;
|
||||
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
|
||||
|
||||
@@ -250,7 +250,7 @@ std::vector<uint8_t> Pack::getResource(const std::string& filename) {
|
||||
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
|
||||
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
|
||||
<< ")\n";
|
||||
// No falla, però adverteix
|
||||
// No falla, pero adverteix
|
||||
}
|
||||
|
||||
return resource_data;
|
||||
@@ -261,7 +261,7 @@ bool Pack::hasResource(const std::string& filename) const {
|
||||
return resources_.contains(filename);
|
||||
}
|
||||
|
||||
// Obtenir llista de tots els recursos
|
||||
// Obtenir list de todos los recursos
|
||||
std::vector<std::string> Pack::getResourceList() const {
|
||||
std::vector<std::string> list;
|
||||
list.reserve(resources_.size());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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
|
||||
// resource_pack.hpp - Sistema de empaquetament de recursos
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
// Basat en el sistema de "pollo" con adaptacions per Orni Attack
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -11,31 +11,31 @@
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Capçalera del fitxer de paquet
|
||||
// Capçalera del file de paquet
|
||||
struct PackHeader {
|
||||
char magic[4]; // "ORNI"
|
||||
uint32_t version; // Versió del format (1)
|
||||
uint32_t version; // Versión 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
|
||||
std::string filename; // Nom del recurs (con barres normals)
|
||||
uint64_t offset; // Posición 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
|
||||
// Clase principal per gestionar paquets de recursos
|
||||
class Pack {
|
||||
public:
|
||||
Pack() = default;
|
||||
~Pack() = default;
|
||||
|
||||
// Afegir fitxers al paquet
|
||||
// Añadir archivos 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
|
||||
// Guardar i load paquets
|
||||
bool savePack(const std::string& pack_file);
|
||||
bool loadPack(const std::string& pack_file);
|
||||
|
||||
@@ -57,7 +57,7 @@ class Pack {
|
||||
std::unordered_map<std::string, ResourceEntry> resources_;
|
||||
std::vector<uint8_t> data_;
|
||||
|
||||
// Funcions auxiliars
|
||||
// Funciones 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);
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// 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
|
||||
@@ -0,0 +1,64 @@
|
||||
// debug_overlay.cpp - Implementación del overlay de debug.
|
||||
|
||||
#include "core/system/debug_overlay.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core/types.hpp"
|
||||
#include "game/options.hpp"
|
||||
|
||||
namespace System {
|
||||
|
||||
namespace {
|
||||
// Posición y tamaño del overlay en coordenadas lógicas (1280×720).
|
||||
constexpr float OVERLAY_X = 12.0F;
|
||||
constexpr float OVERLAY_Y_FPS = 12.0F;
|
||||
constexpr float OVERLAY_LINE_HEIGHT = 18.0F; // separación entre líneas (scale 0.4 → ~16 px alto)
|
||||
constexpr float OVERLAY_SCALE = 0.4F;
|
||||
constexpr float OVERLAY_SPACING = 2.0F;
|
||||
constexpr float OVERLAY_BRIGHTNESS = 1.0F;
|
||||
|
||||
// Cadencia de actualización del valor de FPS mostrado.
|
||||
constexpr float FPS_UPDATE_INTERVAL = 0.5F;
|
||||
} // namespace
|
||||
|
||||
DebugOverlay::DebugOverlay(Rendering::Renderer* renderer)
|
||||
: text_(renderer),
|
||||
#ifdef _DEBUG
|
||||
visible_(true),
|
||||
#else
|
||||
visible_(false),
|
||||
#endif
|
||||
fps_accumulator_(0.0F),
|
||||
fps_frame_count_(0),
|
||||
fps_display_(0) {}
|
||||
|
||||
void DebugOverlay::update(float delta_time) {
|
||||
fps_accumulator_ += delta_time;
|
||||
fps_frame_count_++;
|
||||
|
||||
if (fps_accumulator_ >= FPS_UPDATE_INTERVAL) {
|
||||
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_);
|
||||
fps_frame_count_ = 0;
|
||||
fps_accumulator_ = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::draw() const {
|
||||
if (!visible_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_);
|
||||
const std::string VSYNC_TEXT = std::string("VSYNC: ")
|
||||
+ (Options::rendering.vsync == 1 ? "ON" : "OFF");
|
||||
|
||||
text_.render(FPS_TEXT,
|
||||
Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS},
|
||||
OVERLAY_SCALE, OVERLAY_SPACING, OVERLAY_BRIGHTNESS);
|
||||
text_.render(VSYNC_TEXT,
|
||||
Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS + OVERLAY_LINE_HEIGHT},
|
||||
OVERLAY_SCALE, OVERLAY_SPACING, OVERLAY_BRIGHTNESS);
|
||||
}
|
||||
|
||||
} // namespace System
|
||||
@@ -0,0 +1,38 @@
|
||||
// debug_overlay.hpp - Overlay de debug (FPS + VSync) toggleable con F11
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Sistema global propiedad del Director. Se actualiza y dibuja cada frame
|
||||
// después de la escena (queda on top). En builds debug arranca visible,
|
||||
// en release oculto. F11 alterna visibilidad.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
namespace System {
|
||||
|
||||
class DebugOverlay {
|
||||
public:
|
||||
explicit DebugOverlay(Rendering::Renderer* renderer);
|
||||
|
||||
// Acumula FPS. Llamar una vez por frame con el delta del Director.
|
||||
void update(float delta_time);
|
||||
|
||||
// Pinta el overlay si está visible. Posición esquina sup-izq.
|
||||
void draw() const;
|
||||
|
||||
void toggle() { visible_ = !visible_; }
|
||||
[[nodiscard]] auto isVisible() const -> bool { return visible_; }
|
||||
|
||||
private:
|
||||
Graphics::VectorText text_;
|
||||
bool visible_;
|
||||
|
||||
// FPS counter — se actualiza cada FPS_UPDATE_INTERVAL segundos.
|
||||
float fps_accumulator_;
|
||||
int fps_frame_count_;
|
||||
int fps_display_;
|
||||
};
|
||||
|
||||
} // namespace System
|
||||
+119
-67
@@ -3,13 +3,18 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
#include "context_escenes.hpp"
|
||||
#include "debug_overlay.hpp"
|
||||
#include "scene.hpp"
|
||||
#include "scene_context.hpp"
|
||||
#include "global_events.hpp"
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/audio/audio_cache.hpp"
|
||||
#include "core/audio/audio_adapter.hpp"
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
@@ -17,9 +22,9 @@
|
||||
#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/scenes/game_scene.hpp"
|
||||
#include "game/scenes/logo_scene.hpp"
|
||||
#include "game/scenes/title_scene.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "project.h"
|
||||
|
||||
@@ -29,14 +34,14 @@
|
||||
#endif
|
||||
|
||||
// Using declarations per simplificar el codi
|
||||
using GestorEscenes::ContextEscenes;
|
||||
using Escena = ContextEscenes::Escena;
|
||||
using SceneManager::SceneContext;
|
||||
using SceneType = SceneContext::SceneType;
|
||||
|
||||
// Constructor
|
||||
Director::Director(std::vector<std::string> const& args) {
|
||||
std::cout << "Orni Attack - Inici\n";
|
||||
|
||||
// Inicialitzar opcions amb valors per defecte
|
||||
// Inicialitzar opciones con valors per defecte
|
||||
Options::init();
|
||||
|
||||
// Comprovar arguments del programa
|
||||
@@ -50,28 +55,28 @@ Director::Director(std::vector<std::string> const& args) {
|
||||
|
||||
// Inicialitzar sistema de recursos
|
||||
#ifdef RELEASE_BUILD
|
||||
// Mode release: paquet obligatori, sense fallback
|
||||
// Mode release: paquet obligatori, sin 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::cerr << "ERROR FATAL: No es pot load " << pack_path << "\n";
|
||||
std::cerr << "El juego no pot continuar sin los 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::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/
|
||||
// Mode desenvolupament: intentar paquet con 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";
|
||||
std::cout << "Sistema de recursos inicialitzat (mode dev con paquet)\n";
|
||||
} else {
|
||||
std::cout << "Sistema de recursos inicialitzat (mode dev, fallback a data/)\n";
|
||||
}
|
||||
@@ -84,21 +89,21 @@ Director::Director(std::vector<std::string> const& args) {
|
||||
createSystemFolder("jailgames");
|
||||
createSystemFolder(std::string("jailgames/") + Project::NAME);
|
||||
|
||||
// Establir ruta del fitxer de configuració
|
||||
// Establir ruta del file de configuración
|
||||
Options::setConfigFile(system_folder_ + "/config.yaml");
|
||||
|
||||
// Carregar o crear configuració
|
||||
// Carregar o crear configuración
|
||||
Options::loadFromFile();
|
||||
|
||||
// Inicialitzar sistema d'input
|
||||
// Inicialitzar sistema de input
|
||||
Input::init("data/gamecontrollerdb.txt");
|
||||
|
||||
// Aplicar configuració de controls dels jugadors
|
||||
// Aplicar configuración de controls dels jugadors
|
||||
Input::get()->applyPlayer1BindingsFromOptions();
|
||||
Input::get()->applyPlayer2BindingsFromOptions();
|
||||
|
||||
if (Options::console) {
|
||||
std::cout << "Configuració carregada\n";
|
||||
std::cout << "Configuración carregada\n";
|
||||
std::cout << " Finestra: " << Options::window.width << "×"
|
||||
<< Options::window.height << '\n';
|
||||
std::cout << " Física: rotation=" << Options::physics.rotation_speed
|
||||
@@ -111,7 +116,7 @@ Director::Director(std::vector<std::string> const& args) {
|
||||
}
|
||||
|
||||
Director::~Director() {
|
||||
// Guardar opcions
|
||||
// Guardar opciones
|
||||
Options::saveToFile();
|
||||
|
||||
// Cleanup input
|
||||
@@ -138,7 +143,7 @@ auto Director::checkProgramArguments(std::vector<std::string> const& args)
|
||||
} else if (argument == "--reset-config") {
|
||||
Options::init();
|
||||
Options::saveToFile();
|
||||
std::cout << "Configuració restablida als valors per defecte\n";
|
||||
std::cout << "Configuración restablida als valors per defecte\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +193,7 @@ void Director::createSystemFolder(const std::string& folder) {
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
case EEXIST:
|
||||
// La carpeta ja existeix (race condition), continuar
|
||||
// La carpeta ya existeix (race condition), continuar
|
||||
break;
|
||||
|
||||
case ENAMETOOLONG:
|
||||
@@ -207,7 +212,7 @@ void Director::createSystemFolder(const std::string& folder) {
|
||||
}
|
||||
}
|
||||
|
||||
// Bucle principal del joc
|
||||
// Bucle principal del juego
|
||||
auto Director::run() -> int {
|
||||
// Calculate initial size from saved zoom_factor
|
||||
int initial_width = static_cast<int>(std::round(
|
||||
@@ -215,68 +220,115 @@ auto Director::run() -> int {
|
||||
int initial_height = static_cast<int>(std::round(
|
||||
Defaults::Window::HEIGHT * Options::window.zoom_factor));
|
||||
|
||||
// Crear gestor SDL amb configuració de Options
|
||||
// Crear gestor SDL con configuración 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
|
||||
// CRÍTIC: Forçar ocultació del cursor DESPRÉS de toda la inicialización SDL
|
||||
// Això evita que SDL mostre el cursor automàticament durante 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);
|
||||
// Inicializar sistema de audio (config inyectada desde Defaults)
|
||||
const Audio::Config AUDIO_CONFIG{
|
||||
.enabled = Defaults::Audio::ENABLED,
|
||||
.volume = Defaults::Audio::VOLUME,
|
||||
.music_enabled = Defaults::Audio::MUSIC_ENABLED,
|
||||
.music_volume = Defaults::Audio::MUSIC_VOLUME,
|
||||
.sound_enabled = Defaults::Audio::SOUND_ENABLED,
|
||||
.sound_volume = Defaults::Audio::SOUND_VOLUME,
|
||||
};
|
||||
Audio::init(AUDIO_CONFIG);
|
||||
Audio::get()->applySettings(AUDIO_CONFIG); // Aplicar volúmenes iniciales al motor
|
||||
|
||||
// Precachejar música per evitar lag al començar
|
||||
AudioCache::getMusic("title.ogg");
|
||||
AudioCache::getMusic("game.ogg");
|
||||
// Precachear música para evitar lag al empezar
|
||||
AudioResource::getMusic("title.ogg");
|
||||
AudioResource::getMusic("game.ogg");
|
||||
if (Options::console) {
|
||||
std::cout << "Música precachejada: "
|
||||
<< AudioCache::getMusicCacheSize() << " fitxers\n";
|
||||
std::cout << "Música precacheada\n";
|
||||
}
|
||||
|
||||
// Crear context d'escenes
|
||||
ContextEscenes context;
|
||||
// Crear context de escenes
|
||||
SceneContext context;
|
||||
#ifdef _DEBUG
|
||||
context.canviar_escena(Escena::TITOL);
|
||||
context.setNextScene(SceneType::TITLE);
|
||||
#else
|
||||
context.canviar_escena(Escena::LOGO);
|
||||
context.setNextScene(SceneType::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();
|
||||
// Overlay de debug (FPS + VSync). Vive en el Director porque es global
|
||||
// a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG).
|
||||
System::DebugOverlay debug_overlay(sdl.getRenderer());
|
||||
|
||||
switch (context.escena_desti()) {
|
||||
case Escena::LOGO: {
|
||||
EscenaLogo logo(sdl, context);
|
||||
logo.executar();
|
||||
// Bucle principal: construir escena → frame loop → destruir → siguiente.
|
||||
while (context.nextScene() != SceneType::EXIT) {
|
||||
SceneManager::actual = context.nextScene();
|
||||
std::unique_ptr<Scene> scene = buildScene(context.nextScene(), sdl, context);
|
||||
if (!scene) {
|
||||
break;
|
||||
}
|
||||
|
||||
case Escena::TITOL: {
|
||||
EscenaTitol titol(sdl, context);
|
||||
titol.executar();
|
||||
break;
|
||||
runFrameLoop(*scene, sdl, context, debug_overlay);
|
||||
}
|
||||
|
||||
case Escena::JOC: {
|
||||
EscenaJoc joc(sdl, context);
|
||||
joc.executar();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Sincronitzar final amb GestorEscenes::actual
|
||||
GestorEscenes::actual = Escena::EIXIR;
|
||||
|
||||
SceneManager::actual = SceneType::EXIT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context)
|
||||
-> std::unique_ptr<Scene> {
|
||||
switch (type) {
|
||||
case SceneType::LOGO:
|
||||
return std::make_unique<LogoScene>(sdl, context);
|
||||
case SceneType::TITLE:
|
||||
return std::make_unique<TitleScene>(sdl, context);
|
||||
case SceneType::GAME:
|
||||
return std::make_unique<GameScene>(sdl, context);
|
||||
case SceneType::EXIT:
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context,
|
||||
System::DebugOverlay& debug_overlay) {
|
||||
SDL_Event event;
|
||||
Uint64 last_time = SDL_GetTicks();
|
||||
|
||||
while (!scene.isFinished()) {
|
||||
// Delta time real, capeado a 50ms para evitar grandes saltos.
|
||||
const Uint64 NOW = SDL_GetTicks();
|
||||
float delta_time = static_cast<float>(NOW - last_time) / 1000.0F;
|
||||
last_time = NOW;
|
||||
delta_time = std::min(delta_time, 0.05F);
|
||||
|
||||
Mouse::updateCursorVisibility();
|
||||
Input::get()->update();
|
||||
|
||||
// Event loop: primero ventana, después globales, después F11
|
||||
// (toggle del overlay), después escena.
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (sdl.handleWindowEvent(event)) {
|
||||
continue;
|
||||
}
|
||||
if (GlobalEvents::handle(event, sdl, context)) {
|
||||
continue;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN
|
||||
&& event.key.scancode == SDL_SCANCODE_F11) {
|
||||
debug_overlay.toggle();
|
||||
continue;
|
||||
}
|
||||
scene.handleEvent(event);
|
||||
}
|
||||
|
||||
scene.update(delta_time);
|
||||
debug_overlay.update(delta_time);
|
||||
Audio::update();
|
||||
|
||||
sdl.clear(0, 0, 0);
|
||||
sdl.updateRenderingContext();
|
||||
scene.draw();
|
||||
debug_overlay.draw(); // siempre on top de la escena
|
||||
sdl.present();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "scene_context.hpp"
|
||||
|
||||
class Scene;
|
||||
class SDLManager;
|
||||
namespace System { class DebugOverlay; }
|
||||
|
||||
class Director {
|
||||
public:
|
||||
explicit Director(std::vector<std::string> const& args);
|
||||
@@ -17,4 +24,18 @@ class Director {
|
||||
static auto checkProgramArguments(std::vector<std::string> const& args)
|
||||
-> std::string;
|
||||
void createSystemFolder(const std::string& folder);
|
||||
|
||||
// Construye la escena correspondiente al tipo solicitado. Retorna
|
||||
// nullptr para EXIT u otros valores no constructibles.
|
||||
static auto buildScene(SceneManager::SceneContext::SceneType type,
|
||||
SDLManager& sdl,
|
||||
SceneManager::SceneContext& context)
|
||||
-> std::unique_ptr<Scene>;
|
||||
|
||||
// Ejecuta el bucle de frames de UNA escena hasta que scene.isFinished()
|
||||
// sea true. Maneja delta_time, eventos (globales + escena), update y draw.
|
||||
// El debug_overlay es global a todas las escenas; el Director lo posee.
|
||||
static void runFrameLoop(Scene& scene, SDLManager& sdl,
|
||||
SceneManager::SceneContext& context,
|
||||
System::DebugOverlay& debug_overlay);
|
||||
};
|
||||
|
||||
@@ -4,43 +4,43 @@
|
||||
|
||||
namespace GameConfig {
|
||||
|
||||
// Mode de joc
|
||||
// Mode de juego
|
||||
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
|
||||
// Configuración de una match
|
||||
struct MatchConfig {
|
||||
bool jugador1_actiu{false}; // Es active el player 1?
|
||||
bool jugador2_actiu{false}; // Es active el player 2?
|
||||
Mode mode{Mode::NORMAL}; // Mode de juego
|
||||
|
||||
// Mètodes auxiliars
|
||||
// Métodos auxiliars
|
||||
|
||||
// Retorna true si només hi ha un jugador actiu
|
||||
// Retorna true si solo hay un player active
|
||||
[[nodiscard]] bool es_un_jugador() const {
|
||||
return (jugador1_actiu && !jugador2_actiu) ||
|
||||
(!jugador1_actiu && jugador2_actiu);
|
||||
}
|
||||
|
||||
// Retorna true si hi ha dos jugadors actius
|
||||
// Retorna true si hay dos jugadors active
|
||||
[[nodiscard]] bool son_dos_jugadors() const {
|
||||
return jugador1_actiu && jugador2_actiu;
|
||||
}
|
||||
|
||||
// Retorna true si no hi ha cap jugador actiu
|
||||
// Retorna true si no hay sin player active
|
||||
[[nodiscard]] bool cap_jugador() const {
|
||||
return !jugador1_actiu && !jugador2_actiu;
|
||||
}
|
||||
|
||||
// Compte de jugadors actius (0, 1 o 2)
|
||||
// Compte de jugadors active (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
|
||||
// Retorna l'ID de l'únic player active (0 o 1)
|
||||
// Solo vàlid si es_un_jugador() retorna true
|
||||
[[nodiscard]] uint8_t id_unic_jugador() const {
|
||||
if (jugador1_actiu && !jugador2_actiu) {
|
||||
return 0;
|
||||
@@ -48,7 +48,7 @@ struct ConfigPartida {
|
||||
if (!jugador1_actiu && jugador2_actiu) {
|
||||
return 1;
|
||||
}
|
||||
return 0; // Fallback (cal comprovar es_un_jugador() primer)
|
||||
return 0; // Fallback (necesario comprovar es_un_jugador() primer)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "context_escenes.hpp"
|
||||
#include "scene_context.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;
|
||||
using SceneManager::SceneContext;
|
||||
using SceneType = SceneContext::SceneType;
|
||||
|
||||
namespace GlobalEvents {
|
||||
|
||||
bool handle(const SDL_Event& event, SDLManager& sdl, ContextEscenes& context) {
|
||||
bool handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) {
|
||||
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
|
||||
auto event_msg = Input::get()->handleEvent(event);
|
||||
if (!event_msg.empty()) {
|
||||
@@ -25,8 +25,8 @@ bool handle(const SDL_Event& event, SDLManager& sdl, ContextEscenes& context) {
|
||||
|
||||
// 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;
|
||||
context.setNextScene(SceneType::EXIT);
|
||||
SceneManager::actual = SceneType::EXIT;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ bool handle(const SDL_Event& event, SDLManager& sdl, ContextEscenes& context) {
|
||||
return true;
|
||||
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
context.canviar_escena(Escena::EIXIR);
|
||||
GestorEscenes::actual = Escena::EIXIR;
|
||||
context.setNextScene(SceneType::EXIT);
|
||||
SceneManager::actual = SceneType::EXIT;
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// global_events.hpp - Events globals del joc
|
||||
// global_events.hpp - Events globals del juego
|
||||
// Basat en el patró del projecte "pollo"
|
||||
// © 2025 Port a C++20
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
// Forward declarations
|
||||
class SDLManager;
|
||||
namespace GestorEscenes {
|
||||
class ContextEscenes;
|
||||
namespace SceneManager {
|
||||
class SceneContext;
|
||||
}
|
||||
|
||||
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);
|
||||
// Retorna true si l'event ha state processat y no necesario seguir processant-lo
|
||||
bool handle(const SDL_Event& event, SDLManager& sdl, SceneManager::SceneContext& context);
|
||||
} // namespace GlobalEvents
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// scene.hpp - Interfaz base para escenas del juego
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// El frame loop vive en Director, no en cada escena. Cada escena implementa
|
||||
// estos cuatro métodos y el Director los llama en orden por frame:
|
||||
// handleEvent(ev) por cada evento SDL (tras los eventos globales)
|
||||
// update(dt) lógica
|
||||
// draw() pintado (entre clear y present del Director)
|
||||
// isFinished() consultar transición pendiente
|
||||
//
|
||||
// Cuando una escena pide transición (vía context_.setNextScene(...)),
|
||||
// isFinished() debe pasar a true y el Director destruye la escena para
|
||||
// construir la siguiente.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
class Scene {
|
||||
public:
|
||||
Scene() = default;
|
||||
virtual ~Scene() = default;
|
||||
|
||||
Scene(const Scene&) = delete;
|
||||
auto operator=(const Scene&) -> Scene& = delete;
|
||||
Scene(Scene&&) = delete;
|
||||
auto operator=(Scene&&) -> Scene& = delete;
|
||||
|
||||
// Eventos específicos de la escena. Los globales (window resize,
|
||||
// F1-F4, ESC, QUIT) los procesa GlobalEvents::handle antes.
|
||||
virtual void handleEvent(const SDL_Event& event) = 0;
|
||||
|
||||
// Lógica de la escena. delta_time en segundos, ya capeado por el Director.
|
||||
virtual void update(float delta_time) = 0;
|
||||
|
||||
// Pintar la escena (entre clear y present del Director).
|
||||
virtual void draw() = 0;
|
||||
|
||||
// True cuando la escena ha pedido transición.
|
||||
[[nodiscard]] virtual auto isFinished() const -> bool = 0;
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
// scene_context.hpp - Sistema de gestió de escenes i context de transiciones
|
||||
// © 2025 Port a C++20
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/system/game_config.hpp"
|
||||
|
||||
namespace SceneManager {
|
||||
|
||||
// Context de transición entre escenes
|
||||
// Conté l'escena destinació i opciones específiques per aquella escena
|
||||
class SceneContext {
|
||||
public:
|
||||
// Tipo de escena del juego
|
||||
enum class SceneType {
|
||||
LOGO, // Pantalla de start (logo JAILGAMES)
|
||||
TITLE, // Pantalla de título con menú
|
||||
GAME, // Juego principal (Asteroids)
|
||||
EXIT // Salir del programa
|
||||
};
|
||||
|
||||
// Opciones específiques para cada escena
|
||||
enum class Option {
|
||||
NONE, // Sin opciones especials (comportament per defecte)
|
||||
JUMP_TO_TITLE_MAIN, // TITLE: Saltar directament a MAIN (starfield instantani)
|
||||
// MODE_DEMO, // GAME: Mode demostració con IA (futur)
|
||||
};
|
||||
|
||||
// Constructor inicial con escena LOGO i sin opciones
|
||||
SceneContext() = default;
|
||||
|
||||
// Canviar escena con opción específica
|
||||
void setNextScene(SceneType next_scene, Option option = Option::NONE) {
|
||||
next_scene_ = next_scene;
|
||||
option_ = option;
|
||||
}
|
||||
|
||||
// Consultar escena destinació
|
||||
[[nodiscard]] auto nextScene() const -> SceneType {
|
||||
return next_scene_;
|
||||
}
|
||||
|
||||
// Consultar opción actual
|
||||
[[nodiscard]] auto option() const -> Option {
|
||||
return option_;
|
||||
}
|
||||
|
||||
// Consumir opción (retorna value i reseteja a NONE)
|
||||
// Utilitzar cuando l'escena processa l'opción
|
||||
[[nodiscard]] auto consumeOption() -> Option {
|
||||
Option value = option_;
|
||||
option_ = Option::NONE;
|
||||
return value;
|
||||
}
|
||||
|
||||
// Reset opción a NONE (sin retornar value)
|
||||
void resetOption() {
|
||||
option_ = Option::NONE;
|
||||
}
|
||||
|
||||
// Configurar match antes de transicionar a GAME
|
||||
void setMatchConfig(const GameConfig::MatchConfig& config) {
|
||||
match_config_ = config;
|
||||
}
|
||||
|
||||
// Obtenir configuración de match (consumit per GameScene)
|
||||
[[nodiscard]] const GameConfig::MatchConfig& getMatchConfig() const {
|
||||
return match_config_;
|
||||
}
|
||||
|
||||
private:
|
||||
SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar
|
||||
Option option_{Option::NONE}; // Opción específica per l'escena
|
||||
GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode)
|
||||
};
|
||||
|
||||
// Variable global inline per gestionar l'escena actual (backward compatibility)
|
||||
// Sincronitzada con context.nextScene() por el Director
|
||||
inline SceneContext::SceneType actual = SceneContext::SceneType::LOGO;
|
||||
|
||||
} // namespace SceneManager
|
||||
+66
-37
@@ -1,43 +1,72 @@
|
||||
// types.hpp - Tipos básicos compartidos
|
||||
// © 2025 Orni Attack
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
// Vector 2D cartesiano - única estructura de coordenadas del juego.
|
||||
// Aggregate (sin constructores definidos) para soportar designated initializers:
|
||||
// Vec2{.x = 1.0F, .y = 2.0F}
|
||||
// y aggregate initialization clásica:
|
||||
// Vec2{1.0F, 2.0F}
|
||||
struct Vec2 {
|
||||
float x{0.0F};
|
||||
float y{0.0F};
|
||||
|
||||
// Punt polar (coordenades polars)
|
||||
struct IPunt {
|
||||
float r; // Radi
|
||||
float angle; // Angle en radians
|
||||
constexpr auto operator+=(const Vec2& o) -> Vec2& {
|
||||
x += o.x;
|
||||
y += o.y;
|
||||
return *this;
|
||||
}
|
||||
constexpr auto operator-=(const Vec2& o) -> Vec2& {
|
||||
x -= o.x;
|
||||
y -= o.y;
|
||||
return *this;
|
||||
}
|
||||
constexpr auto operator*=(float s) -> Vec2& {
|
||||
x *= s;
|
||||
y *= s;
|
||||
return *this;
|
||||
}
|
||||
constexpr auto operator/=(float s) -> Vec2& {
|
||||
x /= s;
|
||||
y /= s;
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto lengthSquared() const -> float { return (x * x) + (y * y); }
|
||||
[[nodiscard]] auto length() const -> float { return std::sqrt(lengthSquared()); }
|
||||
[[nodiscard]] auto dot(const Vec2& o) const -> float { return (x * o.x) + (y * o.y); }
|
||||
|
||||
// Devuelve el vector normalizado; si la magnitud es 0 devuelve {0,0}.
|
||||
[[nodiscard]] auto normalized() const -> Vec2 {
|
||||
const float L = length();
|
||||
return L > 0.0F ? Vec2{.x = x / L, .y = y / L} : Vec2{};
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
constexpr auto operator+(Vec2 a, const Vec2& b) -> Vec2 {
|
||||
a += b;
|
||||
return a;
|
||||
}
|
||||
constexpr auto operator-(Vec2 a, const Vec2& b) -> Vec2 {
|
||||
a -= b;
|
||||
return a;
|
||||
}
|
||||
constexpr auto operator*(Vec2 v, float s) -> Vec2 {
|
||||
v *= s;
|
||||
return v;
|
||||
}
|
||||
constexpr auto operator*(float s, Vec2 v) -> Vec2 {
|
||||
v *= s;
|
||||
return v;
|
||||
}
|
||||
constexpr auto operator/(Vec2 v, float s) -> Vec2 {
|
||||
v /= s;
|
||||
return v;
|
||||
}
|
||||
constexpr auto operator-(const Vec2& v) -> Vec2 { return {.x = -v.x, .y = -v.y}; }
|
||||
constexpr auto operator==(const Vec2& a, const Vec2& b) -> bool {
|
||||
return a.x == b.x && a.y == b.y;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// path_utils.cpp - Implementació de utilitats de rutes
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "path_utils.hpp"
|
||||
|
||||
@@ -13,10 +13,10 @@ namespace Utils {
|
||||
static std::string executable_path_;
|
||||
static std::string executable_directory_;
|
||||
|
||||
// Inicialitzar el sistema de rutes amb argv[0]
|
||||
// Inicialitzar el sistema de rutes con argv[0]
|
||||
void initializePathSystem(const char* argv0) {
|
||||
if (argv0 == nullptr) {
|
||||
std::cerr << "[PathUtils] ADVERTÈNCIA: argv[0] és nullptr\n";
|
||||
std::cerr << "[PathUtils] ADVERTÈNCIA: argv[0] es nullptr\n";
|
||||
executable_path_ = "";
|
||||
executable_directory_ = ".";
|
||||
return;
|
||||
@@ -50,7 +50,7 @@ bool isMacOSBundle() {
|
||||
#ifdef MACOS_BUNDLE
|
||||
return true;
|
||||
#else
|
||||
// Detecció en temps d'execució
|
||||
// Detecció en time de 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;
|
||||
@@ -62,10 +62,10 @@ std::string getResourceBasePath() {
|
||||
std::string exe_dir = getExecutableDirectory();
|
||||
|
||||
if (isMacOSBundle()) {
|
||||
// Bundle de macOS: recursos a ../Resources des de MacOS/
|
||||
// Bundle de macOS: recursos a ../Resources desde MacOS/
|
||||
std::cout << "[PathUtils] Detectat bundle de macOS\n";
|
||||
return exe_dir + "/../Resources";
|
||||
} // Executable normal: recursos al mateix directori
|
||||
} // Executable normal: recursos al mismo directori
|
||||
return exe_dir;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ std::string normalizePath(const std::string& path) {
|
||||
// Convertir barres invertides a normals
|
||||
std::ranges::replace(normalized, '\\', '/');
|
||||
|
||||
// Simplificar rutes amb filesystem
|
||||
// Simplificar rutes con filesystem
|
||||
try {
|
||||
std::filesystem::path fs_path(normalized);
|
||||
normalized = fs_path.lexically_normal().string();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// path_utils.hpp - Utilitats de gestió de rutes
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
// Detecció de directoris i bundles multiplataforma
|
||||
|
||||
#pragma once
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace Utils {
|
||||
|
||||
// Inicialització amb argv[0]
|
||||
// Inicialización con argv[0]
|
||||
void initializePathSystem(const char* argv0);
|
||||
|
||||
// Obtenció de rutes
|
||||
|
||||
Vendored
+116
-116
@@ -2,9 +2,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_NODE_HPP
|
||||
@@ -25,9 +25,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_MACROS_DEFINE_MACROS_HPP
|
||||
@@ -38,9 +38,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Check version definitions if already defined.
|
||||
@@ -94,9 +94,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_MACROS_CPP_CONFIG_MACROS_HPP
|
||||
@@ -109,7 +109,7 @@
|
||||
// With the MSVC compilers, the value of __cplusplus is by default always
|
||||
// "199611L"(C++98). To avoid that, the library instead references _MSVC_LANG
|
||||
// which is always set a correct value. See
|
||||
// https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/
|
||||
// https://devblogs.microsoft.como/cppblog/msvc-now-correctly-reports-__cplusplus/
|
||||
// for more details.
|
||||
#if defined(_MSVC_LANG) && !defined(__clang__)
|
||||
#define FK_YAML_CPLUSPLUS _MSVC_LANG
|
||||
@@ -265,9 +265,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_ASSERT_HPP
|
||||
@@ -290,9 +290,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_DOCUMENT_METAINFO_HPP
|
||||
@@ -308,9 +308,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_META_NODE_TRAITS_HPP
|
||||
@@ -323,9 +323,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_META_DETECT_HPP
|
||||
@@ -341,9 +341,9 @@
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_META_STL_SUPPLEMENT_HPP
|
||||
@@ -373,13 +373,13 @@ FK_YAML_DETAIL_NAMESPACE_BEGIN
|
||||
|
||||
/// @brief An alias template for std::add_pointer::type with C++11.
|
||||
/// @note std::add_pointer_t is available since C++14.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/add_pointer
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/add_pointer
|
||||
/// @tparam T A type to be added a pointer.
|
||||
template <typename T> using add_pointer_t = typename std::add_pointer<T>::type;
|
||||
|
||||
/// @brief An alias template for std::enable_if::type with C++11.
|
||||
/// @note std::enable_if_t is available since C++14.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/enable_if
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/enable_if
|
||||
/// @tparam Condition A condition tested at compile time.
|
||||
/// @tparam T The type defined only if Condition is true.
|
||||
template <bool Condition, typename T = void>
|
||||
@@ -387,7 +387,7 @@ using enable_if_t = typename std::enable_if<Condition, T>::type;
|
||||
|
||||
/// @brief A simple implementation to use std::is_null_pointer with C++11.
|
||||
/// @note std::is_null_pointer is available since C++14.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/is_null_pointer
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/is_null_pointer
|
||||
/// @tparam T The type to be checked if it's equal to std::nullptr_t.
|
||||
template <typename T>
|
||||
struct is_null_pointer
|
||||
@@ -395,20 +395,20 @@ struct is_null_pointer
|
||||
|
||||
/// @brief An alias template for std::remove_cv::type with C++11.
|
||||
/// @note std::remove_cv_t is available since C++14.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/remove_cv
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/remove_cv
|
||||
/// @tparam T A type from which const-volatile qualifiers are removed.
|
||||
template <typename T> using remove_cv_t = typename std::remove_cv<T>::type;
|
||||
|
||||
/// @brief An alias template for std::remove_pointer::type with C++11.
|
||||
/// @note std::remove_pointer_t is available since C++14.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/remove_pointer
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/remove_pointer
|
||||
/// @tparam T A type from which a pointer is removed.
|
||||
template <typename T>
|
||||
using remove_pointer_t = typename std::remove_pointer<T>::type;
|
||||
|
||||
/// @brief An alias template for std::remove_reference::type with C++11.
|
||||
/// @note std::remove_reference_t is available since C++14.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/remove_reference
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/remove_reference
|
||||
/// @tparam T A type from which a reference is removed.
|
||||
template <typename T>
|
||||
using remove_reference_t = typename std::remove_reference<T>::type;
|
||||
@@ -498,7 +498,7 @@ template <bool Val> using bool_constant = std::integral_constant<bool, Val>;
|
||||
/// @note
|
||||
/// std::conjunction is available since C++17.
|
||||
/// This is applied when no traits are specified as inputs.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/conjunction
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/conjunction
|
||||
/// @tparam Traits Type traits to be checked if their ::value are all true.
|
||||
template <typename... Traits> struct conjunction : std::true_type {};
|
||||
|
||||
@@ -519,7 +519,7 @@ struct conjunction<First, Rest...>
|
||||
/// @note
|
||||
/// std::disjunction is available since C++17.
|
||||
/// This is applied when no traits are specified as inputs.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/disjunction
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/disjunction
|
||||
/// @tparam Traits Type traits to be checked if at least one of their ::value is
|
||||
/// true.
|
||||
template <typename... Traits> struct disjunction : std::false_type {};
|
||||
@@ -539,7 +539,7 @@ struct disjunction<First, Rest...>
|
||||
|
||||
/// @brief A simple implementation to use std::negation with C++11/C++14.
|
||||
/// @note std::negation is available since C++17.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/negation
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/negation
|
||||
/// @tparam Trait Type trait whose ::value is negated.
|
||||
template <typename Trait>
|
||||
struct negation : std::integral_constant<bool, !Trait::value> {};
|
||||
@@ -552,7 +552,7 @@ template <typename... Types> struct make_void {
|
||||
|
||||
/// @brief A simple implementation to use std::void_t with C++11/C++14.
|
||||
/// @note std::void_t is available since C++17.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/void_t
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/void_t
|
||||
/// @tparam Types Any types to be transformed to void type.
|
||||
template <typename... Types> using void_t = typename make_void<Types...>::type;
|
||||
|
||||
@@ -571,7 +571,7 @@ using std::void_t;
|
||||
/// @brief A simple implementation to use std::remove_cvref_t with
|
||||
/// C++11/C++14/C++17.
|
||||
/// @note std::remove_cvref & std::remove_cvref_t are available since C++20.
|
||||
/// @sa https://en.cppreference.com/w/cpp/types/remove_cvref
|
||||
/// @sa https://en.cppreference.como/w/cpp/types/remove_cvref
|
||||
/// @tparam T A type from which cv-qualifiers and reference are removed.
|
||||
template <typename T>
|
||||
using remove_cvref_t =
|
||||
@@ -790,9 +790,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_META_TYPE_TRAITS_HPP
|
||||
@@ -890,9 +890,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_FKYAML_FWD_HPP
|
||||
@@ -1106,9 +1106,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_YAML_VERSION_TYPE_HPP
|
||||
@@ -1171,9 +1171,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_EXCEPTION_SAFE_ALLOCATION_HPP
|
||||
@@ -1278,9 +1278,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_DESERIALIZER_HPP
|
||||
@@ -1299,9 +1299,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_LEXICAL_ANALYZER_HPP
|
||||
@@ -1320,9 +1320,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_ENCODINGS_URI_ENCODING_HPP
|
||||
@@ -1453,9 +1453,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODINGS_HPP
|
||||
@@ -1471,9 +1471,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_EXCEPTION_HPP
|
||||
@@ -1491,9 +1491,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_STRING_FORMATTER_HPP
|
||||
@@ -1541,9 +1541,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_TYPES_NODE_T_HPP
|
||||
@@ -1558,9 +1558,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_NODE_TYPE_HPP
|
||||
@@ -2155,9 +2155,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_BLOCK_SCALAR_HEADER_HPP
|
||||
@@ -2196,9 +2196,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_POSITION_TRACKER_HPP
|
||||
@@ -2213,9 +2213,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_STR_VIEW_HPP
|
||||
@@ -3346,9 +3346,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_TYPES_LEXICAL_TOKEN_T_HPP
|
||||
@@ -4396,7 +4396,7 @@ private:
|
||||
if FK_YAML_UNLIKELY (cur_itr == m_end_itr) {
|
||||
// Without the following iterator update, lexer cannot reach the end of
|
||||
// input buffer and causes infinite loops from the next loop.
|
||||
// (https://github.com/fktn-k/fkYAML/pull/410)
|
||||
// (https://github.como/fktn-k/fkYAML/pull/410)
|
||||
m_cur_itr = m_end_itr;
|
||||
|
||||
// If there's no non-empty line, the content indentation level is equal to
|
||||
@@ -4726,9 +4726,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_SCALAR_PARSER_HPP
|
||||
@@ -4743,9 +4743,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// **NOTE FOR LIBRARY DEVELOPERS**:
|
||||
@@ -5644,9 +5644,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_ENCODINGS_YAML_ESCAPER_HPP
|
||||
@@ -6005,9 +6005,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_SCALAR_SCANNER_HPP
|
||||
@@ -6377,9 +6377,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_TAG_T_HPP
|
||||
@@ -6948,7 +6948,7 @@ private:
|
||||
|
||||
// conversion error from a scalar which is not tagged with !!int is
|
||||
// recovered by treating it as a string scalar. See
|
||||
// https://github.com/fktn-k/fkYAML/issues/428.
|
||||
// https://github.como/fktn-k/fkYAML/issues/428.
|
||||
return basic_node_type(string_type(token.begin(), token.end()));
|
||||
}
|
||||
case node_type::FLOAT: {
|
||||
@@ -6966,7 +6966,7 @@ private:
|
||||
|
||||
// conversion error from a scalar which is not tagged with !!float is
|
||||
// recovered by treating it as a string scalar. See
|
||||
// https://github.com/fktn-k/fkYAML/issues/428.
|
||||
// https://github.como/fktn-k/fkYAML/issues/428.
|
||||
return basic_node_type(string_type(token.begin(), token.end()));
|
||||
}
|
||||
case node_type::STRING:
|
||||
@@ -7000,9 +7000,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_TAG_RESOLVER_HPP
|
||||
@@ -7208,9 +7208,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_META_INPUT_ADAPTER_TRAITS_HPP
|
||||
@@ -7274,9 +7274,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_NODE_ATTRS_HPP
|
||||
@@ -7417,9 +7417,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_NODE_PROPERTY_HPP
|
||||
@@ -8019,7 +8019,7 @@ private:
|
||||
// (i.e., the properties are for a container node), the
|
||||
// application and the line advancement must happen here.
|
||||
// Otherwise, a false indent error will be emitted. See
|
||||
// https://github.com/fktn-k/fkYAML/issues/368 for more details.
|
||||
// https://github.como/fktn-k/fkYAML/issues/368 for more details.
|
||||
line = line_after_props;
|
||||
indent = lexer.get_last_token_begin_pos();
|
||||
*mp_current_node = basic_node_type::mapping();
|
||||
@@ -8910,9 +8910,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_INPUT_INPUT_ADAPTER_HPP
|
||||
@@ -8935,9 +8935,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODE_DETECTOR_HPP
|
||||
@@ -8953,9 +8953,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODE_T_HPP
|
||||
@@ -10496,9 +10496,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_ITERATOR_HPP
|
||||
@@ -10914,7 +10914,7 @@ namespace std {
|
||||
#ifdef __clang__
|
||||
// clang emits warnings against mixed usage of class/struct for
|
||||
// tuple_size/tuple_element. see also:
|
||||
// https://groups.google.com/a/isocpp.org/g/std-discussion/c/QC-AMb5oO1w
|
||||
// https://groups.google.como/a/isocpp.org/g/std-discussion/c/QC-AMb5oO1w
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wmismatched-tags"
|
||||
#endif
|
||||
@@ -10949,9 +10949,9 @@ struct tuple_element<I, ::fkyaml::detail::iterator<ValueType>> {
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_MAP_RANGE_PROXY_HPP
|
||||
@@ -11139,9 +11139,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_NODE_REF_STORAGE_HPP
|
||||
@@ -11235,9 +11235,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_OUTPUT_SERIALIZER_HPP
|
||||
@@ -11255,9 +11255,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_CONVERSIONS_TO_STRING_HPP
|
||||
@@ -11337,7 +11337,7 @@ to_string(FloatType v, std::string &s) noexcept {
|
||||
|
||||
// If `v` is actually an integer and no scientific notation is used for
|
||||
// serialization, ".0" must be appended. The result would cause a roundtrip
|
||||
// issue otherwise. https://github.com/fktn-k/fkYAML/issues/405
|
||||
// issue otherwise. https://github.como/fktn-k/fkYAML/issues/405
|
||||
const std::size_t pos = s.find_first_of(".e");
|
||||
if (pos == std::string::npos) {
|
||||
s += ".0";
|
||||
@@ -11744,9 +11744,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_REVERSE_ITERATOR_HPP
|
||||
@@ -12016,9 +12016,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_TYPES_YAML_VERSION_T_HPP
|
||||
@@ -12073,9 +12073,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_NODE_VALUE_CONVERTER_HPP
|
||||
@@ -12090,9 +12090,9 @@ FK_YAML_DETAIL_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_CONVERSIONS_FROM_NODE_HPP
|
||||
@@ -12875,9 +12875,9 @@ FK_YAML_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_DETAIL_CONVERSIONS_TO_NODE_HPP
|
||||
@@ -13262,9 +13262,9 @@ FK_YAML_NAMESPACE_END
|
||||
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
|
||||
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2
|
||||
// |__| |_| \__| |_| |_| |_|___||___|______|
|
||||
// https://github.com/fktn-k/fkYAML
|
||||
// https://github.como/fktn-k/fkYAML
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Kensuke Fukutani <fktn.dev@gmail.como>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FK_YAML_ORDERED_MAP_HPP
|
||||
@@ -13875,7 +13875,7 @@ public:
|
||||
/// iterators into a basic_node object.
|
||||
/// @note
|
||||
/// Iterators must satisfy the LegacyInputIterator requirements.
|
||||
/// See https://en.cppreference.com/w/cpp/named_req/InputIterator.
|
||||
/// See https://en.cppreference.como/w/cpp/named_req/InputIterator.
|
||||
/// @tparam ItrType Type of a compatible iterator
|
||||
/// @param[in] begin An iterator to the first element of an input sequence.
|
||||
/// @param[in] end An iterator to the past-the-last element of an input
|
||||
@@ -15517,7 +15517,7 @@ inline namespace yaml_literals {
|
||||
|
||||
// Whitespace before the literal operator identifier is deprecated in C++23 or
|
||||
// better but required in C++11. Ignore the warning as a workaround.
|
||||
// https://github.com/fktn-k/fkYAML/pull/417
|
||||
// https://github.como/fktn-k/fkYAML/pull/417
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
|
||||
+67
-48
@@ -1,4 +1,4 @@
|
||||
// Ogg Vorbis audio decoder - v1.20 - public domain
|
||||
// Ogg Vorbis audio decoder - v1.22 - public domain
|
||||
// http://nothings.org/stb_vorbis/
|
||||
//
|
||||
// Original version written by Sean Barrett in 2007.
|
||||
@@ -29,12 +29,15 @@
|
||||
// Bernhard Wodo Evan Balster github:alxprd
|
||||
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
|
||||
// Phillip Bennefall Rohit Thiago Goulart
|
||||
// github:manxorist saga musix github:infatum
|
||||
// github:manxorist Saga Musix github:infatum
|
||||
// Timur Gagiev Maxwell Koo Peter Waller
|
||||
// github:audinowho Dougall Johnson David Reid
|
||||
// github:Clownacy Pedro J. Estebanez Remi Verschelde
|
||||
// AnthoFoxo github:morlat Gabriel Ravier
|
||||
//
|
||||
// Partial history:
|
||||
// 1.22 - 2021-07-11 - various small fixes
|
||||
// 1.21 - 2021-07-02 - fix bug for files with no comments
|
||||
// 1.20 - 2020-07-11 - several small fixes
|
||||
// 1.19 - 2020-02-05 - warnings
|
||||
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
|
||||
@@ -220,6 +223,12 @@ extern int stb_vorbis_decode_frame_pushdata(
|
||||
// channel. In other words, (*output)[0][0] contains the first sample from
|
||||
// the first channel, and (*output)[1][0] contains the first sample from
|
||||
// the second channel.
|
||||
//
|
||||
// *output points into stb_vorbis's internal output buffer storage; these
|
||||
// buffers are owned by stb_vorbis and application code should not free
|
||||
// them or modify their contents. They are transient and will be overwritten
|
||||
// once you ask for more data to get decoded, so be sure to grab any data
|
||||
// you need before then.
|
||||
|
||||
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
|
||||
// inform stb_vorbis that your next datablock will not be contiguous with
|
||||
@@ -579,7 +588,7 @@ enum STBVorbisError
|
||||
#if defined(_MSC_VER) || defined(__MINGW32__)
|
||||
#include <malloc.h>
|
||||
#endif
|
||||
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
|
||||
#if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
#else // STB_VORBIS_NO_CRT
|
||||
@@ -646,6 +655,12 @@ typedef signed int int32;
|
||||
|
||||
typedef float codetype;
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define STBV_NOTUSED(v) (void)(v)
|
||||
#else
|
||||
#define STBV_NOTUSED(v) (void)sizeof(v)
|
||||
#endif
|
||||
|
||||
// @NOTE
|
||||
//
|
||||
// Some arrays below are tagged "//varies", which means it's actually
|
||||
@@ -1046,7 +1061,7 @@ static float float32_unpack(uint32 x)
|
||||
uint32 sign = x & 0x80000000;
|
||||
uint32 exp = (x & 0x7fe00000) >> 21;
|
||||
double res = sign ? -(double)mantissa : (double)mantissa;
|
||||
return (float) ldexp((float)res, exp-788);
|
||||
return (float) ldexp((float)res, (int)exp-788);
|
||||
}
|
||||
|
||||
|
||||
@@ -1077,6 +1092,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
||||
// find the first entry
|
||||
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
|
||||
if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
|
||||
assert(len[k] < 32); // no error return required, code reading lens checks this
|
||||
// add to the list
|
||||
add_entry(c, 0, k, m++, len[k], values);
|
||||
// add all available leaves
|
||||
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
||||
uint32 res;
|
||||
int z = len[i], y;
|
||||
if (z == NO_CODE) continue;
|
||||
assert(z < 32); // no error return required, code reading lens checks this
|
||||
// find lowest available leaf (should always be earliest,
|
||||
// which is what the specification calls for)
|
||||
// note that this property, and the fact we can never have
|
||||
@@ -1099,12 +1116,10 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
||||
while (z > 0 && !available[z]) --z;
|
||||
if (z == 0) { return FALSE; }
|
||||
res = available[z];
|
||||
assert(z >= 0 && z < 32);
|
||||
available[z] = 0;
|
||||
add_entry(c, bit_reverse(res), i, m++, len[i], values);
|
||||
// propagate availability up the tree
|
||||
if (z != len[i]) {
|
||||
assert(len[i] >= 0 && len[i] < 32);
|
||||
for (y=len[i]; y > z; --y) {
|
||||
assert(available[y] == 0);
|
||||
available[y] = res + (1 << (32-y));
|
||||
@@ -2577,34 +2592,33 @@ static void imdct_step3_inner_s_loop_ld654(int n, float *e, int i_off, float *A,
|
||||
|
||||
while (z > base) {
|
||||
float k00,k11;
|
||||
float l00,l11;
|
||||
|
||||
k00 = z[-0] - z[-8];
|
||||
k11 = z[-1] - z[-9];
|
||||
z[-0] = z[-0] + z[-8];
|
||||
z[-1] = z[-1] + z[-9];
|
||||
z[-8] = k00;
|
||||
z[-9] = k11 ;
|
||||
k00 = z[-0] - z[ -8];
|
||||
k11 = z[-1] - z[ -9];
|
||||
l00 = z[-2] - z[-10];
|
||||
l11 = z[-3] - z[-11];
|
||||
z[ -0] = z[-0] + z[ -8];
|
||||
z[ -1] = z[-1] + z[ -9];
|
||||
z[ -2] = z[-2] + z[-10];
|
||||
z[ -3] = z[-3] + z[-11];
|
||||
z[ -8] = k00;
|
||||
z[ -9] = k11;
|
||||
z[-10] = (l00+l11) * A2;
|
||||
z[-11] = (l11-l00) * A2;
|
||||
|
||||
k00 = z[ -2] - z[-10];
|
||||
k11 = z[ -3] - z[-11];
|
||||
z[ -2] = z[ -2] + z[-10];
|
||||
z[ -3] = z[ -3] + z[-11];
|
||||
z[-10] = (k00+k11) * A2;
|
||||
z[-11] = (k11-k00) * A2;
|
||||
|
||||
k00 = z[-12] - z[ -4]; // reverse to avoid a unary negation
|
||||
k00 = z[ -4] - z[-12];
|
||||
k11 = z[ -5] - z[-13];
|
||||
l00 = z[ -6] - z[-14];
|
||||
l11 = z[ -7] - z[-15];
|
||||
z[ -4] = z[ -4] + z[-12];
|
||||
z[ -5] = z[ -5] + z[-13];
|
||||
z[-12] = k11;
|
||||
z[-13] = k00;
|
||||
|
||||
k00 = z[-14] - z[ -6]; // reverse to avoid a unary negation
|
||||
k11 = z[ -7] - z[-15];
|
||||
z[ -6] = z[ -6] + z[-14];
|
||||
z[ -7] = z[ -7] + z[-15];
|
||||
z[-14] = (k00+k11) * A2;
|
||||
z[-15] = (k00-k11) * A2;
|
||||
z[-12] = k11;
|
||||
z[-13] = -k00;
|
||||
z[-14] = (l11-l00) * A2;
|
||||
z[-15] = (l00+l11) * -A2;
|
||||
|
||||
iter_54(z);
|
||||
iter_54(z-8);
|
||||
@@ -3069,6 +3083,7 @@ static int do_floor(vorb *f, Mapping *map, int i, int n, float *target, YTYPE *f
|
||||
for (q=1; q < g->values; ++q) {
|
||||
j = g->sorted_order[q];
|
||||
#ifndef STB_VORBIS_NO_DEFER_FLOOR
|
||||
STBV_NOTUSED(step2_flag);
|
||||
if (finalY[j] >= 0)
|
||||
#else
|
||||
if (step2_flag[j])
|
||||
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
|
||||
|
||||
// WINDOWING
|
||||
|
||||
STBV_NOTUSED(left_end);
|
||||
n = f->blocksize[m->blockflag];
|
||||
map = &f->mapping[m->mapping];
|
||||
|
||||
@@ -3368,7 +3384,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
|
||||
// this isn't to spec, but spec would require us to read ahead
|
||||
// and decode the size of all current frames--could be done,
|
||||
// but presumably it's not a commonly used feature
|
||||
f->current_loc = -n2; // start of first frame is positioned for discard
|
||||
f->current_loc = 0u - n2; // start of first frame is positioned for discard (NB this is an intentional unsigned overflow/wrap-around)
|
||||
// we might have to discard samples "from" the next frame too,
|
||||
// if we're lapping a large block then a small at the start?
|
||||
f->discard_samples_deferred = n - right_end;
|
||||
@@ -3642,8 +3658,10 @@ static int start_decoder(vorb *f)
|
||||
f->vendor[len] = (char)'\0';
|
||||
//user comments
|
||||
f->comment_list_length = get32_packet(f);
|
||||
if (f->comment_list_length > 0) {
|
||||
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length));
|
||||
f->comment_list = NULL;
|
||||
if (f->comment_list_length > 0)
|
||||
{
|
||||
f->comment_list = (char**) setup_malloc(f, sizeof(char*) * (f->comment_list_length));
|
||||
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
|
||||
}
|
||||
|
||||
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
|
||||
unsigned int div=1;
|
||||
for (k=0; k < c->dimensions; ++k) {
|
||||
int off = (z / div) % c->lookup_values;
|
||||
float val = mults[off];
|
||||
val = mults[off]*c->delta_value + c->minimum_value + last;
|
||||
float val = mults[off]*c->delta_value + c->minimum_value + last;
|
||||
c->multiplicands[j*c->dimensions + k] = val;
|
||||
if (c->sequence_p)
|
||||
last = val;
|
||||
@@ -3951,7 +3968,7 @@ static int start_decoder(vorb *f)
|
||||
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
||||
}
|
||||
for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
|
||||
g->subclass_books[j][k] = get_bits(f,8)-1;
|
||||
g->subclass_books[j][k] = (int16)get_bits(f,8)-1;
|
||||
if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
||||
}
|
||||
}
|
||||
@@ -4509,6 +4526,7 @@ stb_vorbis *stb_vorbis_open_pushdata(
|
||||
*error = VORBIS_need_more_data;
|
||||
else
|
||||
*error = p.error;
|
||||
vorbis_deinit(&p);
|
||||
return NULL;
|
||||
}
|
||||
f = vorbis_alloc(&p);
|
||||
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
|
||||
header[i] = get8(f);
|
||||
if (f->eof) return 0;
|
||||
if (header[4] != 0) goto invalid;
|
||||
goal = header[22] + (header[23] << 8) + (header[24]<<16) + (header[25]<<24);
|
||||
goal = header[22] + (header[23] << 8) + (header[24]<<16) + ((uint32)header[25]<<24);
|
||||
for (i=22; i < 26; ++i)
|
||||
header[i] = 0;
|
||||
crc = 0;
|
||||
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
|
||||
// set. whoops!
|
||||
break;
|
||||
}
|
||||
previous_safe = last_page_loc+1;
|
||||
//previous_safe = last_page_loc+1; // NOTE: not used after this point, but note for debugging
|
||||
last_page_loc = stb_vorbis_get_file_offset(f);
|
||||
}
|
||||
|
||||
@@ -5081,7 +5099,10 @@ stb_vorbis * stb_vorbis_open_filename(const char *filename, int *error, const st
|
||||
stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
|
||||
{
|
||||
stb_vorbis *f, p;
|
||||
if (data == NULL) return NULL;
|
||||
if (!data) {
|
||||
if (error) *error = VORBIS_unexpected_eof;
|
||||
return NULL;
|
||||
}
|
||||
vorbis_init(&p, alloc);
|
||||
p.stream = (uint8 *) data;
|
||||
p.stream_end = (uint8 *) data + len;
|
||||
@@ -5156,11 +5177,11 @@ static void copy_samples(short *dest, float *src, int len)
|
||||
|
||||
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
|
||||
{
|
||||
#define BUFFER_SIZE 32
|
||||
float buffer[BUFFER_SIZE];
|
||||
int i,j,o,n = BUFFER_SIZE;
|
||||
#define STB_BUFFER_SIZE 32
|
||||
float buffer[STB_BUFFER_SIZE];
|
||||
int i,j,o,n = STB_BUFFER_SIZE;
|
||||
check_endianness();
|
||||
for (o = 0; o < len; o += BUFFER_SIZE) {
|
||||
for (o = 0; o < len; o += STB_BUFFER_SIZE) {
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
if (o + n > len) n = len - o;
|
||||
for (j=0; j < num_c; ++j) {
|
||||
@@ -5177,16 +5198,17 @@ static void compute_samples(int mask, short *output, int num_c, float **data, in
|
||||
output[o+i] = v;
|
||||
}
|
||||
}
|
||||
#undef STB_BUFFER_SIZE
|
||||
}
|
||||
|
||||
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
|
||||
{
|
||||
#define BUFFER_SIZE 32
|
||||
float buffer[BUFFER_SIZE];
|
||||
int i,j,o,n = BUFFER_SIZE >> 1;
|
||||
#define STB_BUFFER_SIZE 32
|
||||
float buffer[STB_BUFFER_SIZE];
|
||||
int i,j,o,n = STB_BUFFER_SIZE >> 1;
|
||||
// o is the offset in the source data
|
||||
check_endianness();
|
||||
for (o = 0; o < len; o += BUFFER_SIZE >> 1) {
|
||||
for (o = 0; o < len; o += STB_BUFFER_SIZE >> 1) {
|
||||
// o2 is the offset in the output data
|
||||
int o2 = o << 1;
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
@@ -5216,6 +5238,7 @@ static void compute_stereo_samples(short *output, int num_c, float **data, int d
|
||||
output[o2+i] = v;
|
||||
}
|
||||
}
|
||||
#undef STB_BUFFER_SIZE
|
||||
}
|
||||
|
||||
static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples)
|
||||
@@ -5288,8 +5311,6 @@ int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short
|
||||
float **outputs;
|
||||
int len = num_shorts / channels;
|
||||
int n=0;
|
||||
int z = f->channels;
|
||||
if (z > channels) z = channels;
|
||||
while (n < len) {
|
||||
int k = f->channel_buffer_end - f->channel_buffer_start;
|
||||
if (n+k >= len) k = len - n;
|
||||
@@ -5308,8 +5329,6 @@ int stb_vorbis_get_samples_short(stb_vorbis *f, int channels, short **buffer, in
|
||||
{
|
||||
float **outputs;
|
||||
int n=0;
|
||||
int z = f->channels;
|
||||
if (z > channels) z = channels;
|
||||
while (n < len) {
|
||||
int k = f->channel_buffer_end - f->channel_buffer_start;
|
||||
if (n+k >= len) k = len - n;
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
// Unitat de compilació aïllada per a la implementació de stb_vorbis.
|
||||
// Viu dins de source/external/ perquè el `.clang-tidy` d'aquesta carpeta
|
||||
// desactiva tots els checks (com fa per stb_image_write_impl.cpp) i el
|
||||
// pre-commit hook ja filtra aquesta ruta de clang-format / clang-tidy.
|
||||
// Així els fals positius de clang-analyzer-* dins de codi C de tercers
|
||||
// no afecten el nostre codi, que continua tenint tots els checks actius.
|
||||
//
|
||||
// jail_audio.cpp defineix STB_VORBIS_HEADER_ONLY abans d'incloure el .c,
|
||||
// així només en veu les declaracions; les definicions les aporta aquest
|
||||
// TU i l'enllaçador les resol.
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
||||
#include "external/stb_vorbis.c"
|
||||
+12
-13
@@ -2,20 +2,19 @@
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
|
||||
// Aliases per a backward compatibility amb codi existent
|
||||
// Permet usar Constants::MARGE_ESQ en lloc de Defaults::Game::MARGIN_LEFT
|
||||
// Aliases para backward compatibility con codi existent
|
||||
// Permet usar Constants::MARGIN_LEFT en lloc de Defaults::Game::MARGIN_LEFT
|
||||
|
||||
namespace Constants {
|
||||
// Marges de l'àrea de joc (derivats de Defaults::Zones::GAME)
|
||||
constexpr int MARGE_ESQ = static_cast<int>(Defaults::Zones::PLAYAREA.x);
|
||||
constexpr int MARGE_DRET =
|
||||
// Márgenes de l'àrea de juego (derivats de Defaults::Zones::GAME)
|
||||
constexpr int MARGIN_LEFT = static_cast<int>(Defaults::Zones::PLAYAREA.x);
|
||||
constexpr int MARGIN_RIGHT =
|
||||
static_cast<int>(Defaults::Zones::PLAYAREA.x + Defaults::Zones::PLAYAREA.w);
|
||||
constexpr int MARGE_DALT = static_cast<int>(Defaults::Zones::PLAYAREA.y);
|
||||
constexpr int MARGE_BAIX =
|
||||
constexpr int MARGIN_TOP = static_cast<int>(Defaults::Zones::PLAYAREA.y);
|
||||
constexpr int MARGIN_BOTTOM =
|
||||
static_cast<int>(Defaults::Zones::PLAYAREA.y + Defaults::Zones::PLAYAREA.h);
|
||||
|
||||
// Límits de polígons i objectes
|
||||
constexpr int MAX_IPUNTS = Defaults::Entities::MAX_IPUNTS;
|
||||
// Límits de objectes
|
||||
constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS;
|
||||
constexpr int MAX_BALES = Defaults::Entities::MAX_BALES;
|
||||
|
||||
@@ -28,8 +27,8 @@ constexpr float PI = Defaults::Math::PI;
|
||||
|
||||
// Helpers per comprovar límits de zona
|
||||
inline bool dins_zona_joc(float x, float y) {
|
||||
const SDL_FPoint punt = {x, y};
|
||||
return SDL_PointInRectFloat(&punt, &Defaults::Zones::PLAYAREA);
|
||||
const SDL_FPoint point = {x, y};
|
||||
return SDL_PointInRectFloat(&point, &Defaults::Zones::PLAYAREA);
|
||||
}
|
||||
|
||||
inline void obtenir_limits_zona(float& min_x, float& max_x, float& min_y, float& max_y) {
|
||||
@@ -40,7 +39,7 @@ inline void obtenir_limits_zona(float& min_x, float& max_x, float& min_y, float&
|
||||
max_y = zona.y + zona.h;
|
||||
}
|
||||
|
||||
// Obtenir límits segurs (compensant radi de l'entitat)
|
||||
// Obtenir límits segurs (compensant radi de l'entidad)
|
||||
inline void obtenir_limits_zona_segurs(float radi, float& min_x, float& max_x, float& min_y, float& max_y) {
|
||||
const auto& zona = Defaults::Zones::PLAYAREA;
|
||||
constexpr float MARGE_SEGURETAT = 10.0F; // Safety margin
|
||||
@@ -51,7 +50,7 @@ inline void obtenir_limits_zona_segurs(float radi, float& min_x, float& max_x, f
|
||||
max_y = zona.y + zona.h - radi - MARGE_SEGURETAT;
|
||||
}
|
||||
|
||||
// Obtenir centre de l'àrea de joc
|
||||
// Obtenir centro de l'àrea de juego
|
||||
inline void obtenir_centre_zona(float& centre_x, float& centre_y) {
|
||||
const auto& zona = Defaults::Zones::PLAYAREA;
|
||||
centre_x = zona.x + (zona.w / 2.0F);
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
// debris.hpp - Fragment de línia volant (explosió de formes)
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// debris.hpp - Fragment de línia volant (explosión de formes)
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Debris: un segment de línia que vola perpendicular a sí mateix
|
||||
// Representa un fragment d'una forma destruïda (nau, enemic, bala)
|
||||
// Debris: un segment de línia que vola perpendicular a sí mismo
|
||||
// Representa un fragment de una shape destruïda (ship, enemy, bullet)
|
||||
struct Debris {
|
||||
// Geometria del segment (2 punts en coordenades mundials)
|
||||
Punt p1; // Punt inicial del segment
|
||||
Punt p2; // Punt final del segment
|
||||
// Geometria del segment (2 points en coordenades mundials)
|
||||
Vec2 p1; // Vec2 inicial del segment
|
||||
Vec2 p2; // Vec2 final del segment
|
||||
|
||||
// Física
|
||||
Punt velocitat; // Velocitat en px/s (components x, y)
|
||||
float acceleracio; // Acceleració negativa (fricció) en px/s²
|
||||
Vec2 velocity; // Velocidad en px/s (components x, y)
|
||||
float acceleration; // Aceleración negativa (fricció) en px/s²
|
||||
|
||||
// Rotació
|
||||
float angle_rotacio; // Angle de rotació acumulat (radians)
|
||||
float velocitat_rot; // Velocitat de rotació de TRAYECTORIA (rad/s)
|
||||
float velocitat_rot_visual; // Velocitat de rotació VISUAL del segment (rad/s)
|
||||
// Rotación
|
||||
float angle_rotacio; // Angle de rotación acumulat (radians)
|
||||
float velocitat_rot; // Velocidad de rotación de TRAYECTORIA (rad/s)
|
||||
float velocitat_rot_visual; // Velocidad de rotación VISUAL del segment (rad/s)
|
||||
|
||||
// Estat de vida
|
||||
float temps_vida; // Temps transcorregut (segons)
|
||||
float temps_max; // Temps de vida màxim (segons)
|
||||
bool actiu; // Està actiu?
|
||||
bool active; // Está active?
|
||||
|
||||
// Shrinking (reducció de distància entre punts)
|
||||
// Shrinking (reducció de distancia entre points)
|
||||
float factor_shrink; // Factor de reducció per segon (0.0-1.0)
|
||||
|
||||
// Rendering
|
||||
float brightness; // Factor de brillantor (0.0-1.0, heretat de l'objecte original)
|
||||
float brightness; // Factor de brightness (0.0-1.0, heretat de l'objecte original)
|
||||
SDL_Color color{}; // Color heredado del padre. alpha==0 → usa global oscilador
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// debris_manager.cpp - Implementació del gestor de fragments
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "debris_manager.hpp"
|
||||
|
||||
@@ -14,60 +14,61 @@
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Helper: transformar punt amb rotació, escala i trasllació
|
||||
// Helper: transformar point con rotación, scale i traslación
|
||||
// (Copiat de shape_renderer.cpp:12-34)
|
||||
static Punt transform_point(const Punt& point, const Punt& shape_centre, const Punt& posicio, float angle, float escala) {
|
||||
// 1. Centrar el punt respecte al centre de la forma
|
||||
static Vec2 transform_point(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) {
|
||||
// 1. Centrar el point respecte al centro de la shape
|
||||
float centered_x = point.x - shape_centre.x;
|
||||
float centered_y = point.y - shape_centre.y;
|
||||
|
||||
// 2. Aplicar escala al punt centrat
|
||||
float scaled_x = centered_x * escala;
|
||||
float scaled_y = centered_y * escala;
|
||||
// 2. Aplicar scale al point centrat
|
||||
float scaled_x = centered_x * scale;
|
||||
float scaled_y = centered_y * scale;
|
||||
|
||||
// 3. Aplicar rotació
|
||||
// 3. Aplicar rotación
|
||||
float cos_a = std::cos(angle);
|
||||
float sin_a = std::sin(angle);
|
||||
|
||||
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a);
|
||||
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a);
|
||||
|
||||
// 4. Aplicar trasllació a posició mundial
|
||||
return {.x = rotated_x + posicio.x, .y = rotated_y + posicio.y};
|
||||
// 4. Aplicar traslación a posición mundial
|
||||
return {.x = rotated_x + position.x, .y = rotated_y + position.y};
|
||||
}
|
||||
|
||||
DebrisManager::DebrisManager(SDL_Renderer* renderer)
|
||||
DebrisManager::DebrisManager(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {
|
||||
// Inicialitzar tots els debris com inactius
|
||||
// Inicialitzar todos los debris como inactius
|
||||
for (auto& debris : debris_pool_) {
|
||||
debris.actiu = false;
|
||||
debris.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Punt& centre,
|
||||
void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Vec2& centro,
|
||||
float angle,
|
||||
float escala,
|
||||
float scale,
|
||||
float velocitat_base,
|
||||
float brightness,
|
||||
const Punt& velocitat_objecte,
|
||||
const Vec2& velocitat_objecte,
|
||||
float velocitat_angular,
|
||||
float factor_herencia_visual,
|
||||
const std::string& sound) {
|
||||
if (!shape || !shape->es_valida()) {
|
||||
const std::string& sound,
|
||||
SDL_Color color) {
|
||||
if (!shape || !shape->isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reproducir sonido de explosión
|
||||
Audio::get()->playSound(sound, Audio::Group::GAME);
|
||||
|
||||
// Obtenir centre de la forma per a transformacions
|
||||
const Punt& shape_centre = shape->get_centre();
|
||||
// Obtenir centro de la shape para transformacions
|
||||
const Vec2& shape_centre = shape->getCenter();
|
||||
|
||||
// Iterar sobre totes les primitives de la forma
|
||||
// Iterar sobre todas las primitives de la shape
|
||||
for (const auto& primitive : shape->get_primitives()) {
|
||||
// Processar cada segment de línia
|
||||
std::vector<std::pair<Punt, Punt>> segments;
|
||||
std::vector<std::pair<Vec2, Vec2>> segments;
|
||||
|
||||
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
|
||||
// Polyline: extreure segments consecutius
|
||||
@@ -81,16 +82,16 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
}
|
||||
}
|
||||
|
||||
// Crear debris per a cada segment
|
||||
// Crear debris para cada segment
|
||||
for (const auto& [local_p1, local_p2] : segments) {
|
||||
// 1. Transformar punts locals → coordenades mundials
|
||||
Punt world_p1 =
|
||||
transform_point(local_p1, shape_centre, centre, angle, escala);
|
||||
Punt world_p2 =
|
||||
transform_point(local_p2, shape_centre, centre, angle, escala);
|
||||
// 1. Transformar points locals → coordenades mundials
|
||||
Vec2 world_p1 =
|
||||
transform_point(local_p1, shape_centre, centro, angle, scale);
|
||||
Vec2 world_p2 =
|
||||
transform_point(local_p2, shape_centre, centro, angle, scale);
|
||||
|
||||
// 2. Trobar slot lliure
|
||||
Debris* debris = trobar_slot_lliure();
|
||||
Debris* debris = findFreeSlot();
|
||||
if (debris == nullptr) {
|
||||
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
|
||||
return; // Pool ple
|
||||
@@ -100,25 +101,25 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
debris->p1 = world_p1;
|
||||
debris->p2 = world_p2;
|
||||
|
||||
// 4. Calcular direcció d'explosió (radial, des del centre cap a fora)
|
||||
Punt direccio = calcular_direccio_explosio(world_p1, world_p2, centre);
|
||||
// 4. Calcular direcció de explosión (radial, des del centro hacia fuera)
|
||||
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro);
|
||||
|
||||
// 5. Velocitat inicial (base ± variació aleatòria + velocitat heretada)
|
||||
// 5. Velocidad inicial (base ± variació aleatòria + velocity heretada)
|
||||
float speed =
|
||||
velocitat_base +
|
||||
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
||||
Defaults::Physics::Debris::VARIACIO_VELOCITAT);
|
||||
|
||||
// Heredar velocitat de l'objecte original (suma vectorial)
|
||||
debris->velocitat.x = (direccio.x * speed) + velocitat_objecte.x;
|
||||
debris->velocitat.y = (direccio.y * speed) + velocitat_objecte.y;
|
||||
debris->acceleracio = Defaults::Physics::Debris::ACCELERACIO;
|
||||
// Heredar velocity de l'objecte original (suma vectorial)
|
||||
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
|
||||
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
|
||||
debris->acceleration = Defaults::Physics::Debris::ACCELERACIO;
|
||||
|
||||
// 6. Herència de velocitat angular amb cap + conversió d'excés
|
||||
// 6. Herència de velocity angular con sin + conversió de excés
|
||||
|
||||
// 6a. Rotació de TRAYECTORIA amb cap + conversió tangencial
|
||||
// 6a. Rotación de TRAYECTORIA con sin + conversió tangencial
|
||||
if (std::abs(velocitat_angular) > 0.01F) {
|
||||
// FASE 1: Aplicar herència i variació (igual que abans)
|
||||
// FASE 1: Aplicar herència i variació (igual que antes)
|
||||
float factor_herencia =
|
||||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||||
@@ -131,19 +132,19 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
|
||||
velocitat_ang_heretada *= (1.0F + variacio);
|
||||
|
||||
// FASE 2: Aplicar cap i calcular excés
|
||||
// FASE 2: Aplicar sin i calcular excés
|
||||
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
|
||||
float abs_ang = std::abs(velocitat_ang_heretada);
|
||||
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
|
||||
|
||||
if (abs_ang > CAP) {
|
||||
// Excés: convertir a velocitat tangencial
|
||||
// Excés: convertir a velocity tangencial
|
||||
float excess = abs_ang - CAP;
|
||||
|
||||
// Radi de la forma (enemics = 20 px)
|
||||
// Radi de la shape (enemigos = 20 px)
|
||||
float radius = 20.0F;
|
||||
|
||||
// Velocitat tangencial = ω_excés × radi
|
||||
// Velocidad tangencial = ω_excés × radi
|
||||
float v_tangential = excess * radius;
|
||||
|
||||
// Direcció tangencial: perpendicular a la radial (90° CCW)
|
||||
@@ -151,38 +152,38 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
float tangent_x = -direccio.y;
|
||||
float tangent_y = direccio.x;
|
||||
|
||||
// Afegir velocitat tangencial (suma vectorial)
|
||||
debris->velocitat.x += tangent_x * v_tangential;
|
||||
debris->velocitat.y += tangent_y * v_tangential;
|
||||
// Añadir velocity tangencial (suma vectorial)
|
||||
debris->velocity.x += tangent_x * v_tangential;
|
||||
debris->velocity.y += tangent_y * v_tangential;
|
||||
|
||||
// Aplicar cap a velocitat angular (preservar signe)
|
||||
// Aplicar hacia velocity angular (preservar signe)
|
||||
debris->velocitat_rot = sign_ang * CAP;
|
||||
} else {
|
||||
// Per sota del cap: comportament normal
|
||||
// Per sota del sin: comportament normal
|
||||
debris->velocitat_rot = velocitat_ang_heretada;
|
||||
}
|
||||
} else {
|
||||
debris->velocitat_rot = 0.0F; // Nave: sin curvas
|
||||
}
|
||||
|
||||
// 6b. Rotació VISUAL (proporcional según factor_herencia_visual)
|
||||
// 6b. Rotación VISUAL (proporcional según factor_herencia_visual)
|
||||
if (factor_herencia_visual > 0.01F && std::abs(velocitat_angular) > 0.01F) {
|
||||
// Heredar rotación visual con factor proporcional
|
||||
debris->velocitat_rot_visual = debris->velocitat_rot * factor_herencia_visual;
|
||||
|
||||
// Variació aleatòria petita (±5%) per naturalitat
|
||||
// Variació aleatòria pequeña (±5%) per naturalitat
|
||||
float variacio_visual =
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) * 0.1F) - 0.05F;
|
||||
debris->velocitat_rot_visual *= (1.0F + variacio_visual);
|
||||
} else {
|
||||
// Rotació visual aleatòria (factor = 0.0 o sin velocidad angular)
|
||||
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
|
||||
debris->velocitat_rot_visual =
|
||||
Defaults::Physics::Debris::ROTACIO_MIN +
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||||
(Defaults::Physics::Debris::ROTACIO_MAX -
|
||||
Defaults::Physics::Debris::ROTACIO_MIN));
|
||||
|
||||
// 50% probabilitat de rotació en sentit contrari
|
||||
// 50% probabilitat de rotación en sentit contrari
|
||||
if (std::rand() % 2 == 0) {
|
||||
debris->velocitat_rot_visual = -debris->velocitat_rot_visual;
|
||||
}
|
||||
@@ -195,66 +196,67 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
debris->temps_max = Defaults::Physics::Debris::TEMPS_VIDA;
|
||||
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
|
||||
|
||||
// 8. Heredar brightness
|
||||
// 8. Heredar brightness y color del padre
|
||||
debris->brightness = brightness;
|
||||
debris->color = color;
|
||||
|
||||
// 9. Activar
|
||||
debris->actiu = true;
|
||||
debris->active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DebrisManager::actualitzar(float delta_time) {
|
||||
void DebrisManager::update(float delta_time) {
|
||||
for (auto& debris : debris_pool_) {
|
||||
if (!debris.actiu) {
|
||||
if (!debris.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. Actualitzar temps de vida
|
||||
// 1. Actualitzar time de vida
|
||||
debris.temps_vida += delta_time;
|
||||
|
||||
// Desactivar si ha superat temps màxim
|
||||
// Desactivar si ha superat time màxim
|
||||
if (debris.temps_vida >= debris.temps_max) {
|
||||
debris.actiu = false;
|
||||
debris.active = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Actualitzar velocitat (desacceleració)
|
||||
// Aplicar fricció en la direcció del moviment
|
||||
float speed = std::sqrt((debris.velocitat.x * debris.velocitat.x) +
|
||||
(debris.velocitat.y * debris.velocitat.y));
|
||||
// 2. Actualitzar velocity (desacceleració)
|
||||
// Aplicar fricció en la direcció del movement
|
||||
float speed = std::sqrt((debris.velocity.x * debris.velocity.x) +
|
||||
(debris.velocity.y * debris.velocity.y));
|
||||
|
||||
if (speed > 1.0F) {
|
||||
// Calcular direcció normalitzada
|
||||
float dir_x = debris.velocitat.x / speed;
|
||||
float dir_y = debris.velocitat.y / speed;
|
||||
float dir_x = debris.velocity.x / speed;
|
||||
float dir_y = debris.velocity.y / speed;
|
||||
|
||||
// Aplicar acceleració negativa (fricció)
|
||||
float nova_speed = speed + (debris.acceleracio * delta_time);
|
||||
// Aplicar aceleración negativa (fricció)
|
||||
float nova_speed = speed + (debris.acceleration * delta_time);
|
||||
nova_speed = std::max(nova_speed, 0.0F);
|
||||
|
||||
debris.velocitat.x = dir_x * nova_speed;
|
||||
debris.velocitat.y = dir_y * nova_speed;
|
||||
debris.velocity.x = dir_x * nova_speed;
|
||||
debris.velocity.y = dir_y * nova_speed;
|
||||
} else {
|
||||
// Velocitat molt baixa, aturar
|
||||
debris.velocitat.x = 0.0F;
|
||||
debris.velocitat.y = 0.0F;
|
||||
// Velocidad mucho baixa, aturar
|
||||
debris.velocity.x = 0.0F;
|
||||
debris.velocity.y = 0.0F;
|
||||
}
|
||||
|
||||
// 2b. Rotar vector de velocitat (trayectoria curva)
|
||||
// 2b. Rotar vector de velocity (trayectoria curva)
|
||||
if (std::abs(debris.velocitat_rot) > 0.01F) {
|
||||
// Calcular angle de rotació aquest frame
|
||||
// Calcular angle de rotación este frame
|
||||
float dangle = debris.velocitat_rot * delta_time;
|
||||
|
||||
// Rotar vector de velocitat usant matriu de rotació 2D
|
||||
float vel_x_old = debris.velocitat.x;
|
||||
float vel_y_old = debris.velocitat.y;
|
||||
// Rotar vector de velocity usant matriu de rotación 2D
|
||||
float vel_x_old = debris.velocity.x;
|
||||
float vel_y_old = debris.velocity.y;
|
||||
|
||||
float cos_a = std::cos(dangle);
|
||||
float sin_a = std::sin(dangle);
|
||||
|
||||
debris.velocitat.x = (vel_x_old * cos_a) - (vel_y_old * sin_a);
|
||||
debris.velocitat.y = (vel_x_old * sin_a) + (vel_y_old * cos_a);
|
||||
debris.velocity.x = (vel_x_old * cos_a) - (vel_y_old * sin_a);
|
||||
debris.velocity.y = (vel_x_old * sin_a) + (vel_y_old * cos_a);
|
||||
}
|
||||
|
||||
// 2c. Aplicar fricció angular (desacceleració gradual)
|
||||
@@ -270,80 +272,79 @@ void DebrisManager::actualitzar(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Calcular centre del segment
|
||||
Punt centre = {.x = (debris.p1.x + debris.p2.x) / 2.0F,
|
||||
// 3. Calcular centro del segment
|
||||
Vec2 centro = {.x = (debris.p1.x + debris.p2.x) / 2.0F,
|
||||
.y = (debris.p1.y + debris.p2.y) / 2.0F};
|
||||
|
||||
// 4. Actualitzar posició del centre
|
||||
centre.x += debris.velocitat.x * delta_time;
|
||||
centre.y += debris.velocitat.y * delta_time;
|
||||
// 4. Actualitzar posición del centro
|
||||
centro.x += debris.velocity.x * delta_time;
|
||||
centro.y += debris.velocity.y * delta_time;
|
||||
|
||||
// 5. Actualitzar rotació VISUAL
|
||||
// 5. Actualitzar rotación VISUAL
|
||||
debris.angle_rotacio += debris.velocitat_rot_visual * delta_time;
|
||||
|
||||
// 6. Aplicar shrinking (reducció de distància entre punts)
|
||||
// 6. Aplicar shrinking (reducció de distancia entre points)
|
||||
float shrink_factor =
|
||||
1.0F - (debris.factor_shrink * debris.temps_vida / debris.temps_max);
|
||||
shrink_factor = std::max(0.0F, shrink_factor); // No negatiu
|
||||
|
||||
// Calcular distància original entre punts
|
||||
// Calcular distancia original entre points
|
||||
float dx = debris.p2.x - debris.p1.x;
|
||||
float dy = debris.p2.y - debris.p1.y;
|
||||
|
||||
// 7. Reconstruir segment amb nova mida i rotació
|
||||
// 7. Reconstruir segment con nueva mida i rotación
|
||||
float half_length = std::sqrt((dx * dx) + (dy * dy)) * shrink_factor / 2.0F;
|
||||
float original_angle = std::atan2(dy, dx);
|
||||
float new_angle = original_angle + debris.angle_rotacio;
|
||||
|
||||
debris.p1.x = centre.x - (half_length * std::cos(new_angle));
|
||||
debris.p1.y = centre.y - (half_length * std::sin(new_angle));
|
||||
debris.p2.x = centre.x + (half_length * std::cos(new_angle));
|
||||
debris.p2.y = centre.y + (half_length * std::sin(new_angle));
|
||||
debris.p1.x = centro.x - (half_length * std::cos(new_angle));
|
||||
debris.p1.y = centro.y - (half_length * std::sin(new_angle));
|
||||
debris.p2.x = centro.x + (half_length * std::cos(new_angle));
|
||||
debris.p2.y = centro.y + (half_length * std::sin(new_angle));
|
||||
}
|
||||
}
|
||||
|
||||
void DebrisManager::dibuixar() const {
|
||||
void DebrisManager::draw() const {
|
||||
for (const auto& debris : debris_pool_) {
|
||||
if (!debris.actiu) {
|
||||
if (!debris.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dibuixar segment de línia amb brightness heretat
|
||||
// Dibujar segmento con brightness y color heredados del padre.
|
||||
Rendering::linea(renderer_,
|
||||
static_cast<int>(debris.p1.x),
|
||||
static_cast<int>(debris.p1.y),
|
||||
static_cast<int>(debris.p2.x),
|
||||
static_cast<int>(debris.p2.y),
|
||||
true,
|
||||
debris.brightness);
|
||||
debris.brightness, 0.0F, debris.color);
|
||||
}
|
||||
}
|
||||
|
||||
Debris* DebrisManager::trobar_slot_lliure() {
|
||||
Debris* DebrisManager::findFreeSlot() {
|
||||
for (auto& debris : debris_pool_) {
|
||||
if (!debris.actiu) {
|
||||
if (!debris.active) {
|
||||
return &debris;
|
||||
}
|
||||
}
|
||||
return nullptr; // Pool ple
|
||||
}
|
||||
|
||||
Punt DebrisManager::calcular_direccio_explosio(const Punt& p1,
|
||||
const Punt& p2,
|
||||
const Punt& centre_objecte) const {
|
||||
// 1. Calcular centre del segment
|
||||
Vec2 DebrisManager::computeExplosionDirection(const Vec2& p1,
|
||||
const Vec2& p2,
|
||||
const Vec2& centre_objecte) const {
|
||||
// 1. Calcular centro del segment
|
||||
float centro_seg_x = (p1.x + p2.x) / 2.0F;
|
||||
float centro_seg_y = (p1.y + p2.y) / 2.0F;
|
||||
|
||||
// 2. Calcular vector des del centre de l'objecte cap al centre del segment
|
||||
// Això garanteix que la direcció sempre apunte cap a fora (direcció radial)
|
||||
// 2. Calcular vector des del centro de l'objecte hacia el centro del segment
|
||||
// Això garanteix que la direcció siempre apunte hacia fuera (direcció radial)
|
||||
float dx = centro_seg_x - centre_objecte.x;
|
||||
float dy = centro_seg_y - centre_objecte.y;
|
||||
|
||||
// 3. Normalitzar (obtenir vector unitari)
|
||||
float length = std::sqrt((dx * dx) + (dy * dy));
|
||||
if (length < 0.001F) {
|
||||
// Segment al centre (cas extrem molt improbable), retornar direcció aleatòria
|
||||
// Segment al centro (cas extrem mucho improbable), retornar direcció aleatòria
|
||||
float angle_rand =
|
||||
(std::rand() / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
||||
return {.x = std::cos(angle_rand), .y = std::sin(angle_rand)};
|
||||
@@ -352,7 +353,7 @@ Punt DebrisManager::calcular_direccio_explosio(const Punt& p1,
|
||||
dx /= length;
|
||||
dy /= length;
|
||||
|
||||
// 4. Afegir variació aleatòria petita (±15°) per varietat visual
|
||||
// 4. Añadir variació aleatòria pequeña (±15°) per varietat visual
|
||||
float angle_variacio =
|
||||
((std::rand() % 30) - 15) * Defaults::Math::PI / 180.0F;
|
||||
|
||||
@@ -365,16 +366,16 @@ Punt DebrisManager::calcular_direccio_explosio(const Punt& p1,
|
||||
return {.x = final_x, .y = final_y};
|
||||
}
|
||||
|
||||
void DebrisManager::reiniciar() {
|
||||
void DebrisManager::reset() {
|
||||
for (auto& debris : debris_pool_) {
|
||||
debris.actiu = false;
|
||||
debris.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
int DebrisManager::get_num_actius() const {
|
||||
int DebrisManager::getActiveCount() const {
|
||||
int count = 0;
|
||||
for (const auto& debris : debris_pool_) {
|
||||
if (debris.actiu) {
|
||||
if (debris.active) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,76 @@
|
||||
// debris_manager.hpp - Gestor de fragments d'explosions
|
||||
// © 2025 Port a C++20 amb SDL3
|
||||
// debris_manager.hpp - Gestor de fragments de explosions
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/graphics/shape.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "debris.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Gestor de fragments d'explosions
|
||||
// Manté un pool d'objectes Debris i gestiona el seu cicle de vida
|
||||
// Gestor de fragments de explosions
|
||||
// Manté un pool de objectes Debris i gestiona el seu cicle de vida
|
||||
class DebrisManager {
|
||||
public:
|
||||
explicit DebrisManager(SDL_Renderer* renderer);
|
||||
explicit DebrisManager(Rendering::Renderer* renderer);
|
||||
|
||||
// Crear explosió a partir d'una forma
|
||||
// - shape: forma vectorial a explotar
|
||||
// - centre: posició del centre de l'objecte
|
||||
// Crear explosión a partir de una shape
|
||||
// - shape: shape vectorial a explode
|
||||
// - centro: posición del centro de l'objecte
|
||||
// - angle: orientació de l'objecte (radians)
|
||||
// - escala: escala de l'objecte (1.0 = normal)
|
||||
// - velocitat_base: velocitat inicial dels fragments (px/s)
|
||||
// - brightness: factor de brillantor heretat (0.0-1.0, per defecte 1.0)
|
||||
// - velocitat_objecte: velocitat de l'objecte que explota (px/s, per defecte 0)
|
||||
// - velocitat_angular: velocitat angular heretada (rad/s, per defecte 0)
|
||||
// - factor_herencia_visual: factor de herència rotació visual (0.0-1.0, per defecte 0.0)
|
||||
void explotar(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Punt& centre,
|
||||
// - scale: scale de l'objecte (1.0 = normal)
|
||||
// - velocitat_base: velocity inicial dels fragments (px/s)
|
||||
// - brightness: factor de brightness heretat (0.0-1.0, per defecte 1.0)
|
||||
// - velocitat_objecte: velocity de l'objecte que explota (px/s, per defecte 0)
|
||||
// - velocitat_angular: velocity angular heretada (rad/s, per defecte 0)
|
||||
// - factor_herencia_visual: factor de herència rotación visual (0.0-1.0, per defecte 0.0)
|
||||
void explode(const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const Vec2& centro,
|
||||
float angle,
|
||||
float escala,
|
||||
float scale,
|
||||
float velocitat_base,
|
||||
float brightness = 1.0F,
|
||||
const Punt& velocitat_objecte = {.x = 0.0F, .y = 0.0F},
|
||||
const Vec2& velocitat_objecte = {.x = 0.0F, .y = 0.0F},
|
||||
float velocitat_angular = 0.0F,
|
||||
float factor_herencia_visual = 0.0F,
|
||||
const std::string& sound = Defaults::Sound::EXPLOSION);
|
||||
const std::string& sound = Defaults::Sound::EXPLOSION,
|
||||
SDL_Color color = {0, 0, 0, 0}); // alpha==0 → fragmentos usan oscilador global
|
||||
|
||||
// Actualitzar tots els fragments actius
|
||||
void actualitzar(float delta_time);
|
||||
// Actualitzar todos los fragments active
|
||||
void update(float delta_time);
|
||||
|
||||
// Dibuixar tots els fragments actius
|
||||
void dibuixar() const;
|
||||
// Dibuixar todos los fragments active
|
||||
void draw() const;
|
||||
|
||||
// Reiniciar tots els fragments (neteja)
|
||||
void reiniciar();
|
||||
// Reiniciar todos los fragments (clear)
|
||||
void reset();
|
||||
|
||||
// Obtenir número de fragments actius
|
||||
[[nodiscard]] int get_num_actius() const;
|
||||
// Obtenir número de fragments active
|
||||
[[nodiscard]] int getActiveCount() const;
|
||||
|
||||
private:
|
||||
SDL_Renderer* renderer_;
|
||||
Rendering::Renderer* renderer_;
|
||||
|
||||
// Pool de fragments (màxim concurrent)
|
||||
// Un pentàgon té 5 línies, 15 enemics = 75 línies
|
||||
// + nau (3 línies) + bales (5 línies * 3) = 93 línies màxim
|
||||
// Un pentágono té 5 línies, 15 enemigos = 75 línies
|
||||
// + ship (3 línies) + balas (5 línies * 3) = 93 línies màxim
|
||||
// Arrodonit a 100 per seguretat
|
||||
static constexpr int MAX_DEBRIS = 150;
|
||||
std::array<Debris, MAX_DEBRIS> debris_pool_;
|
||||
|
||||
// Trobar primer slot inactiu
|
||||
Debris* trobar_slot_lliure();
|
||||
Debris* findFreeSlot();
|
||||
|
||||
// Calcular direcció d'explosió (radial, des del centre cap al segment)
|
||||
[[nodiscard]] Punt calcular_direccio_explosio(const Punt& p1, const Punt& p2, const Punt& centre_objecte) const;
|
||||
// Calcular direcció de explosión (radial, des del centro hacia el segment)
|
||||
[[nodiscard]] Vec2 computeExplosionDirection(const Vec2& p1, const Vec2& p2, const Vec2& centre_objecte) const;
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// floating_score.hpp - Número de puntuación que apareix y desapareix
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// FloatingScore: text animat que muestra points guanyats
|
||||
// S'activa cuando es destrueix un enemy i s'esvaeix después de un time
|
||||
struct FloatingScore {
|
||||
// Text a mostrar (e.g., "100", "150", "200")
|
||||
std::string text;
|
||||
|
||||
// Posición actual (coordenades mundials)
|
||||
Vec2 position;
|
||||
|
||||
// Animación de movement
|
||||
Vec2 velocity; // px/s (normalment sin amunt: {0.0f, -30.0f})
|
||||
|
||||
// Animación de fade
|
||||
float temps_vida; // Temps transcorregut (segons)
|
||||
float temps_max; // Temps de vida màxim (segons)
|
||||
float brightness; // Brillantor calculada (0.0-1.0)
|
||||
|
||||
// Estat
|
||||
bool active;
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
@@ -0,0 +1,99 @@
|
||||
// floating_score_manager.cpp - Implementació del gestor de números flotantes
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#include "floating_score_manager.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Effects {
|
||||
|
||||
FloatingScoreManager::FloatingScoreManager(Rendering::Renderer* renderer)
|
||||
: text_(renderer) {
|
||||
// Inicialitzar todos los slots como inactius
|
||||
for (auto& pf : pool_) {
|
||||
pf.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void FloatingScoreManager::crear(int points, const Vec2& position) {
|
||||
// 1. Trobar slot lliure
|
||||
FloatingScore* pf = findFreeSlot();
|
||||
if (pf == nullptr) {
|
||||
return; // Pool ple (improbable)
|
||||
}
|
||||
|
||||
// 2. Inicialitzar puntuación flotante
|
||||
pf->text = std::to_string(points);
|
||||
pf->position = position;
|
||||
pf->velocity = {.x = Defaults::FloatingScore::VELOCITY_X,
|
||||
.y = Defaults::FloatingScore::VELOCITY_Y};
|
||||
pf->temps_vida = 0.0F;
|
||||
pf->temps_max = Defaults::FloatingScore::LIFETIME;
|
||||
pf->brightness = 1.0F;
|
||||
pf->active = true;
|
||||
}
|
||||
|
||||
void FloatingScoreManager::update(float delta_time) {
|
||||
for (auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. Actualitzar posición (deriva sin amunt)
|
||||
pf.position.x += pf.velocity.x * delta_time;
|
||||
pf.position.y += pf.velocity.y * delta_time;
|
||||
|
||||
// 2. Actualitzar time de vida
|
||||
pf.temps_vida += delta_time;
|
||||
|
||||
// 3. Calcular brightness (fade lineal)
|
||||
float progress = pf.temps_vida / pf.temps_max; // 0.0 → 1.0
|
||||
pf.brightness = 1.0F - progress; // 1.0 → 0.0
|
||||
|
||||
// 4. Desactivar cuando acaba el time
|
||||
if (pf.temps_vida >= pf.temps_max) {
|
||||
pf.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FloatingScoreManager::draw() {
|
||||
for (const auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Renderizar centrat con brightness (fade)
|
||||
constexpr float scale = Defaults::FloatingScore::SCALE;
|
||||
constexpr float spacing = Defaults::FloatingScore::SPACING;
|
||||
|
||||
text_.renderCentered(pf.text, pf.position, scale, spacing, pf.brightness);
|
||||
}
|
||||
}
|
||||
|
||||
void FloatingScoreManager::reset() {
|
||||
for (auto& pf : pool_) {
|
||||
pf.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
int FloatingScoreManager::getActiveCount() const {
|
||||
int count = 0;
|
||||
for (const auto& pf : pool_) {
|
||||
if (pf.active) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
FloatingScore* FloatingScoreManager::findFreeSlot() {
|
||||
for (auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
return &pf;
|
||||
}
|
||||
}
|
||||
return nullptr; // Pool ple
|
||||
}
|
||||
|
||||
} // namespace Effects
|
||||
@@ -0,0 +1,55 @@
|
||||
// floating_score_manager.hpp - Gestor de números de puntuación flotantes
|
||||
// © 2025 Port a C++20 con SDL3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "floating_score.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Gestor de números de puntuación flotantes
|
||||
// Manté un pool de FloatingScore i gestiona el seu cicle de vida
|
||||
class FloatingScoreManager {
|
||||
public:
|
||||
explicit FloatingScoreManager(Rendering::Renderer* renderer);
|
||||
|
||||
// Crear número flotante
|
||||
// - points: value numèric (100, 150, 200)
|
||||
// - position: on apareix (normalment centro de enemy destruït)
|
||||
void crear(int points, const Vec2& position);
|
||||
|
||||
// Actualitzar todos los números active
|
||||
void update(float delta_time);
|
||||
|
||||
// Dibuixar todos los números active
|
||||
void draw();
|
||||
|
||||
// Reiniciar tots (clear)
|
||||
void reset();
|
||||
|
||||
// Obtenir número active (debug)
|
||||
[[nodiscard]] int getActiveCount() const;
|
||||
|
||||
private:
|
||||
Graphics::VectorText text_; // Sistema de text vectorial
|
||||
|
||||
// Pool de números flotantes (màxim concurrent)
|
||||
// Màxim 15 enemigos simultanis = màxim 15 números
|
||||
static constexpr int MAX_PUNTUACIONS =
|
||||
Defaults::FloatingScore::MAX_CONCURRENT;
|
||||
std::array<FloatingScore, MAX_PUNTUACIONS> pool_;
|
||||
|
||||
// Trobar primer slot inactiu
|
||||
FloatingScore* findFreeSlot();
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user