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:
2026-05-20 09:36:22 +02:00
142 changed files with 9078 additions and 6676 deletions
+40
View File
@@ -135,6 +135,46 @@ add_custom_command(
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK}) add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} 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 --- # --- STATIC ANALYSIS / FORMAT TARGETS ---
find_program(CLANG_TIDY_EXE NAMES clang-tidy) find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format) find_program(CLANG_FORMAT_EXE NAMES clang-format)
+140
View File
@@ -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.
+34
View File
@@ -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
View File
@@ -1,23 +1,7 @@
# bullet.shp - Projectil (petit pentàgon) # bullet.shp - Projectil (octàgon, radi=3)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
name: bullet name: bullet
scale: 1.0 scale: 1.0
center: 0, 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 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 -15
View File
@@ -1,21 +1,7 @@
# enemy_pentagon.shp - ORNI enemic (pentàgon regular) # enemy_pentagon.shp - ORNI enemic (pentàgon regular, radi=20)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
name: enemy_pentagon name: enemy_pentagon
scale: 1.0 scale: 1.0
center: 0, 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 polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20
+1 -13
View File
@@ -1,19 +1,7 @@
# enemy_square.shp - ORNI enemic (quadrat regular) # enemy_square.shp - ORNI enemic (quadrat regular, radi=20)
# © 2025 Port a C++20 amb SDL3
name: enemy_square name: enemy_square
scale: 1.0 scale: 1.0
center: 0, 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 polyline: 0,-20 20,0 0,20 -20,0 0,-20
+2 -18
View File
@@ -1,24 +1,8 @@
# ship.shp - Nau del jugador 1 (triangle amb base còncava - punta de fletxa) # ship.shp - Nau del jugador 1
# © 1999 Visente i Sergi (versió Pascal) # Triangle amb base còncava (punta de fletxa)
# © 2025 Port a C++20 amb SDL3
name: ship name: ship
scale: 1.0 scale: 1.0
center: 0, 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 polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
+3 -22
View File
@@ -1,30 +1,11 @@
# ship2.shp - Nau del jugador 2 (triangle amb circulito central) # ship2.shp - Nau del jugador 2
# © 1999 Visente i Sergi (versió Pascal) # Triangle amb cercle central (distintiu visual)
# © 2025 Port a C++20 amb SDL3
name: ship2 name: ship2
scale: 1.0 scale: 1.0
center: 0, 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 polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
# Circulito central (octàgon r=2.5) # Octàgon central (radi=2.5)
# Distintiu visual del jugador 2
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5 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
View File
@@ -16,7 +16,7 @@ stages:
spawn_interval: 3.0 spawn_interval: 3.0
enemy_distribution: enemy_distribution:
pentagon: 100 pentagon: 100
quadrat: 0 cuadrado: 0
molinillo: 0 molinillo: 0
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 0.7 speed_multiplier: 0.7
@@ -32,7 +32,7 @@ stages:
spawn_interval: 2.5 spawn_interval: 2.5
enemy_distribution: enemy_distribution:
pentagon: 70 pentagon: 70
quadrat: 30 cuadrado: 30
molinillo: 0 molinillo: 0
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 0.85 speed_multiplier: 0.85
@@ -48,7 +48,7 @@ stages:
spawn_interval: 2.0 spawn_interval: 2.0
enemy_distribution: enemy_distribution:
pentagon: 50 pentagon: 50
quadrat: 30 cuadrado: 30
molinillo: 20 molinillo: 20
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 1.0 speed_multiplier: 1.0
@@ -64,7 +64,7 @@ stages:
spawn_interval: 1.8 spawn_interval: 1.8
enemy_distribution: enemy_distribution:
pentagon: 40 pentagon: 40
quadrat: 35 cuadrado: 35
molinillo: 25 molinillo: 25
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 1.1 speed_multiplier: 1.1
@@ -80,7 +80,7 @@ stages:
spawn_interval: 1.5 spawn_interval: 1.5
enemy_distribution: enemy_distribution:
pentagon: 35 pentagon: 35
quadrat: 35 cuadrado: 35
molinillo: 30 molinillo: 30
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 1.2 speed_multiplier: 1.2
@@ -96,7 +96,7 @@ stages:
spawn_interval: 1.3 spawn_interval: 1.3
enemy_distribution: enemy_distribution:
pentagon: 30 pentagon: 30
quadrat: 30 cuadrado: 30
molinillo: 40 molinillo: 40
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 1.3 speed_multiplier: 1.3
@@ -112,7 +112,7 @@ stages:
spawn_interval: 1.0 spawn_interval: 1.0
enemy_distribution: enemy_distribution:
pentagon: 25 pentagon: 25
quadrat: 30 cuadrado: 30
molinillo: 45 molinillo: 45
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 1.4 speed_multiplier: 1.4
@@ -128,7 +128,7 @@ stages:
spawn_interval: 0.8 spawn_interval: 0.8
enemy_distribution: enemy_distribution:
pentagon: 20 pentagon: 20
quadrat: 30 cuadrado: 30
molinillo: 50 molinillo: 50
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 1.5 speed_multiplier: 1.5
@@ -144,7 +144,7 @@ stages:
spawn_interval: 0.6 spawn_interval: 0.6
enemy_distribution: enemy_distribution:
pentagon: 15 pentagon: 15
quadrat: 25 cuadrado: 25
molinillo: 60 molinillo: 60
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 1.6 speed_multiplier: 1.6
@@ -160,7 +160,7 @@ stages:
spawn_interval: 0.5 spawn_interval: 0.5
enemy_distribution: enemy_distribution:
pentagon: 10 pentagon: 10
quadrat: 20 cuadrado: 20
molinillo: 70 molinillo: 70
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 1.8 speed_multiplier: 1.8
+12
View File
@@ -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;
}
+29
View File
@@ -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;
}
+82
View File
@@ -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);
}
+28
View File
@@ -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];
}
+218 -110
View File
@@ -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 <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) #include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
// clang-format off #include "core/audio/jail_audio.hpp" // Para Ja::* (motor jailgames)
#undef STB_VORBIS_HEADER_ONLY #include "core/audio/sound_effects_config.hpp" // Para SoundEffectsConfig
#include "external/stb_vorbis.h" #include "core/defaults.hpp" // Para Defaults::Audio::FREQUENCY
// clang-format on
#include "core/audio/audio_cache.hpp" // Para AudioCache // Invariant compile-time: tots los valors d'Audio::Group han de cabre als slots
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM... // de volum per grup que manté l'engine. Si s'afegeix una nueva entrada a Group
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions // 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 // Singleton
Audio* Audio::instance = nullptr; std::unique_ptr<Audio> Audio::instance;
// Inicializa la instancia única del singleton // Inicialitza la instància única del singleton con la configuración rebuda
void Audio::init() { Audio::instance = new Audio(); } void Audio::init(const Config& config) { Audio::instance = std::unique_ptr<Audio>(new Audio(config)); }
// Libera la instancia // Allibera la instància
void Audio::destroy() { delete Audio::instance; } void Audio::destroy() { Audio::instance.reset(); }
// Obtiene la instancia // Obté la instància
auto Audio::get() -> Audio* { return Audio::instance; } auto Audio::get() -> Audio* { return Audio::instance.get(); }
// Constructor // Constructor
Audio::Audio() { initSDLAudio(); } Audio::Audio(const Config& config)
: config_(config) { initSDLAudio(); }
// Destructor // Destructor: engine_ es std::unique_ptr, el seu dtor tanca el device SDL i
Audio::~Audio() { // desregistra Ja::Engine::active_. Cap crida explícita necessària.
JA_Quit(); 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() { void Audio::update() {
JA_Update(); if (instance && instance->engine_) { instance->engine_->update(); }
} }
// Reproduce la música // Reprodueix la música per nom (amb crossfade opcional)
void Audio::playMusic(const std::string& name, const int loop) { void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
bool new_loop = (loop != 0); const bool NEW_LOOP = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada // Si ya sona exactament la misma pista i mismo mode loop, no fem res
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) { if (getMusicState() == MusicState::PLAYING && music_.name == name && music_.loop == NEW_LOOP) {
return; return;
} }
// Intentar obtener recurso; si falla, no tocar estado if (!music_enabled_) { return; }
auto* resource = AudioCache::getMusic(name);
if (resource == nullptr) {
// manejo de error opcional
return;
}
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere) auto* resource = AudioResource::getMusic(name);
if (music_.state == MusicState::PLAYING) { if (resource == nullptr) { return; }
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
}
// Llamada al motor para reproducir la nueva pista playMusicInternal(resource, loop, crossfade_ms);
JA_PlayMusic(resource, loop);
// Actualizar estado y metadatos después de iniciar con éxito
music_.name = name; 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() { void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) { if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
JA_PauseMusic(); engine_->pauseMusic();
music_.state = MusicState::PAUSED;
} }
} }
// Continua la música pausada // Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
void Audio::resumeMusic() { void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) { if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
JA_ResumeMusic(); engine_->resumeMusic();
music_.state = MusicState::PLAYING;
} }
} }
// Detiene la música // Atura la música (l'estat el transiciona Engine::stopMusic)
void Audio::stopMusic() { void Audio::stopMusic() {
if (music_enabled_) { if (music_enabled_) {
JA_StopMusic(); engine_->stopMusic();
music_.state = MusicState::STOPPED;
} }
} }
// Reproduce un sonido por nombre void Audio::setMusicSpeed(float ratio) {
void Audio::playSound(const std::string& name, Group group) const { if (music_enabled_) {
engine_->setMusicSpeed(ratio);
}
}
// Reprodueix un so per nom
void Audio::playSound(const std::string& name, Group group) {
if (sound_enabled_) { 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 // Reprodueix un so per punter directe
void Audio::playSound(JA_Sound_t* sound, Group group) const { 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_) { if (sound_enabled_) {
JA_PlaySound(sound, 0, static_cast<int>(group)); engine_->stopChannel(-1);
} }
} }
// Detiene todos los sonidos // Fa una fosa de sortida de la música
void Audio::stopAllSounds() const { void Audio::fadeOutMusic(int milliseconds) {
if (sound_enabled_) { if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
JA_StopChannel(-1); engine_->fadeOutMusic(milliseconds);
} }
} }
// Realiza un fundido de salida de la música // Registra un callback que el motor dispararà cuando la pista actual acabi de
void Audio::fadeOutMusic(int milliseconds) const { // drenar (times == 0 + stream buit). S'executa al mismo thread que
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) { // Audio::update (render loop); los consumidors no poden fer I/O blocant.
JA_FadeOutMusic(milliseconds); 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 // Resol el nom contra el cache de recursos i retorna la duración pre-calculada
auto Audio::getRealMusicState() -> MusicState { // al `loadMusic`. 0 si la pista no existeix — así el caller pot decidir
JA_Music_state ja_state = JA_GetMusicState(); // fallback (p. ex. usar un timeout fix) sin haver de propagar errors.
switch (ja_state) { auto Audio::getMusicDurationMs(const std::string& name) -> int {
case JA_MUSIC_PLAYING: 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; return MusicState::PLAYING;
case JA_MUSIC_PAUSED: case Ja::MusicState::PAUSED:
return MusicState::PAUSED; return MusicState::PAUSED;
case JA_MUSIC_STOPPED: case Ja::MusicState::STOPPED:
case JA_MUSIC_INVALID: case Ja::MusicState::INVALID:
case JA_MUSIC_DISABLED:
default: default:
return MusicState::STOPPED; return MusicState::STOPPED;
} }
} }
// Establece el volumen de los sonidos // Aplica el gate master (enabled_) + el gate del canal (sound/music_enabled_)
void Audio::setSoundVolume(float sound_volume, Group group) const { // i retorna el volum escalat pel master config_.volume. 0 si algun gate está
if (sound_enabled_) { // tancat. Así los dos setters comparteixen la misma política.
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME); auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume; volume = std::clamp(volume, MIN_VOLUME, MAX_VOLUME);
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group)); return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
}
} }
// Establece el volumen de la música // Estableix el volum dels sons (float 0.0..1.0)
void Audio::setMusicVolume(float music_volume) const { void Audio::setSoundVolume(float sound_volume, Group group) {
if (music_enabled_) { engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
JA_SetMusicVolume(CONVERTED_VOLUME);
}
} }
// Aplica la configuración // Estableix el volum de la música (float 0.0..1.0)
void Audio::applySettings() { void Audio::setMusicVolume(float music_volume) {
enable(Options::audio.enabled); 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) { void Audio::enable(bool value) {
enabled_ = value; enabled_ = value;
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME); setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME);
setMusicVolume(enabled_ ? Options::audio.music.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() { void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) { if (!SDL_Init(SDL_INIT_AUDIO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError()); std::fprintf(stderr, "Audio: SDL_AUDIO could not initialize! SDL Error: %s\n", SDL_GetError());
} else { return;
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
std::cout << "\n** AUDIO SYSTEM **\n";
std::cout << "Audio system initialized successfully\n";
} }
} 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);
}
+144 -78
View File
@@ -1,97 +1,163 @@
#pragma once #pragma once
#include <string> // Para string #include <cmath> // Para std::lround
#include <utility> // Para move #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.01.0; la capa de
// presentació (menús, notificacions) usa las helpers toPercent/fromPercent
// per mostrar 0100 a l'usuari.
class Audio { class Audio {
public: public:
// --- Enums --- // --- Configuración injectada (Options la construeix via buildAudioConfig) ---
enum class Group : int { struct Config {
ALL = -1, // Todos los grupos bool enabled{true};
GAME = 0, // Sonidos del juego float volume{1.0F}; // Master 0..1
INTERFACE = 1 // Sonidos de la interfaz bool music_enabled{true};
}; float music_volume{0.8F};
bool sound_enabled{true};
float sound_volume{1.0F};
};
enum class MusicState { // --- Enums ---
PLAYING, // Reproduciendo música enum class Group : std::int8_t {
PAUSED, // Música pausada ALL = -1, // Tots los grups
STOPPED, // Música detenida GAME = 0, // Sons del joc
}; INTERFACE = 1 // Sons de la interfície
};
// --- Constantes --- enum class MusicState : std::uint8_t {
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo PLAYING, // Reproduint música
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo PAUSED, // Música pausada
static constexpr int FREQUENCY = 48000; // Frecuencia de audio STOPPED, // Música aturada
};
// --- Singleton --- // --- Constants ---
static void init(); // Inicializa el objeto Audio static constexpr float MAX_VOLUME = 1.0F; // Volum màxim (float 0..1)
static void destroy(); // Libera el objeto Audio static constexpr float MIN_VOLUME = 0.0F; // Volum mínim (float 0..1)
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
Audio(const Audio&) = delete; // Evitar copia
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
static void update(); // Actualización del sistema de audio // --- Singleton ---
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;
// --- Control de música --- static void update(); // Actualització del sistema d'àudio
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
// --- Control de sonidos --- // --- Control de música ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproduir música per nom (amb crossfade opcional)
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproduir música per punter (amb crossfade opcional)
void stopAllSounds() const; // Detener todos los sonidos void pauseMusic(); // Pausar la reproducció de música
void resumeMusic(); // Continua la música pausada
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 volumen --- // --- Control de sons ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
void setMusicVolume(float volume) const; // Ajustar volumen de música 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)
// --- Configuración general --- // --- Control de volum (API interna: float 0.0..1.0) ---
void enable(bool value); // Establecer estado general void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general void setMusicVolume(float volume); // Ajusta el volum de la música
void applySettings(); // Aplica la configuración
// --- Configuración de sonidos --- // --- Helpers de conversió para la capa de presentació ---
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos // UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos // No són constexpr porque std::lround no ho es en C++20; s'usen en runtime.
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos static auto toPercent(float volume) -> int {
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos return static_cast<int>(std::lround(volume * 100.0F));
}
static auto fromPercent(int percent) -> float {
return static_cast<float>(percent) / 100.0F;
}
// --- Configuración de música --- // --- Configuración general ---
void enableMusic() { music_enabled_ = true; } // Habilitar música void enable(bool value); // Estableix l'estat general (reaplica volums)
void disableMusic() { music_enabled_ = false; } // Deshabilitar música void toggleEnabled() { enable(!enabled_); } // Alterna l'estat general (reaplica volums)
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música void applySettings(const Config& config); // Aplica una nueva configuración
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
// --- Consultas de estado --- // --- Configuración de sons ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; } void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; } void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (reaplica volum)
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
[[nodiscard]] static auto getRealMusicState() -> MusicState;
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
private: // --- Configuración de música ---
// --- Tipos anidados --- void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
struct Music { void toggleMusic() { enableMusic(!music_enabled_); } // Alterna l'estat de la música (reaplica volum)
MusicState state{MusicState::STOPPED}; // Estado actual de la música
std::string name; // Última pista de música reproducida
bool loop{false}; // Indica si se reproduce en bucle
};
// --- Métodos --- // --- Consultes d'estat ---
Audio(); // Constructor privado [[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
~Audio(); // Destructor privado [[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
void initSDLAudio(); // Inicializa SDL Audio [[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[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;
// --- Variables miembro --- private:
static Audio* instance; // Instancia única de Audio // --- Tipus anidats ---
struct Music {
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
};
Music music_; // Estado de la música // --- Mètodes ---
bool enabled_{true}; // Estado general del audio explicit Audio(const Config& config); // Constructor privat: rep la config
bool sound_enabled_{true}; // Estado de los efectos de sonido void initSDLAudio(); // Inicialitza SDL Audio
bool music_enabled_{true}; // Estado de lasica 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 membre ---
static std::unique_ptr<Audio> instance; // Instància única d'Audio
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
};
+99
View File
@@ -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
+19
View File
@@ -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
-142
View File
@@ -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;
}
-42
View File
@@ -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);
};
+251
View File
@@ -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
+38
View File
@@ -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
+645
View File
@@ -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(&current_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(&current_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
View File
@@ -2,481 +2,257 @@
// --- Includes --- // --- Includes ---
#include <SDL3/SDL.h> #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 <cstdint>
#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory #include <functional>
#include <memory>
#include <string>
#include <vector>
// --- Public Enums --- // Forward-declaració del decoder de vorbis. La implementació viu a
enum JA_Channel_state { JA_CHANNEL_INVALID, // jail_audio.cpp (únic TU que compila external/stb_vorbis.c). Qualsevol caller
JA_CHANNEL_FREE, // solo necessita `stb_vorbis*` per punter — nunca per valor — así que el
JA_CHANNEL_PLAYING, // forward decl n'hay prou i evita arrossegar el .c a tots los TU.
JA_CHANNEL_PAUSED, // NOLINTNEXTLINE(readability-identifier-naming) — nom imposat per l'API de stb_vorbis
JA_SOUND_DISABLED }; struct stb_vorbis;
enum JA_Music_state { JA_MUSIC_INVALID,
JA_MUSIC_PLAYING,
JA_MUSIC_PAUSED,
JA_MUSIC_STOPPED,
JA_MUSIC_DISABLED };
// --- Struct Definitions --- // Deleter stateless para buffers reservats con `SDL_malloc` / `SDL_LoadWAV*`.
#define JA_MAX_SIMULTANEOUS_CHANNELS 20 // Compatible con `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size overhead
#define JA_MAX_GROUPS 2 // gràcies a EBO, igual que un unique_ptr con default_delete.
struct SdlFreeDeleter {
struct JA_Sound_t { void operator()(Uint8* p) const noexcept {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; if (p != nullptr) { SDL_free(p); }
Uint32 length{0}; }
Uint8* buffer{NULL};
}; };
struct JA_Channel_t { // Motor de baix nivell d'àudio del projecte jailgames: streaming OGG
JA_Sound_t* sound{nullptr}; // (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 pos{0};
int times{0}; int times{0};
int group{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}; SDL_AudioStream* stream{nullptr};
JA_Channel_state state{JA_CHANNEL_FREE}; MusicState state{MusicState::INVALID};
}; };
struct JA_Music_t { struct FadeState {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; bool active{false};
Uint32 length{0}; Uint64 start_time{0};
Uint8* buffer{nullptr}; int duration_ms{0};
char* filename{nullptr}; float initial_volume{0.0F};
};
int pos{0}; struct OutgoingMusic {
int times{0};
SDL_AudioStream* stream{nullptr}; SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID}; // 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
// --- Internal Global State --- // SDL drena el stream més ràpid del previst cuando hay sounds
// Marcado 'inline' (C++17) para asegurar una única instancia. // bound a la misma device (~2x), buidant-lo a meitat del
// fade i sentint-se como un tall sec.
inline JA_Music_t* current_music{nullptr}; Music* music{nullptr};
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; FadeState fade;
};
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
inline float JA_musicVolume{1.0F}; // --- Engine ---
inline float JA_soundVolume[JA_MAX_GROUPS]; // Encapsula tot l'estat que antes vivia como a globals inline. Un sol Engine
inline bool JA_musicEnabled{true}; // viu per procés (enforceat via assert al ctor contra `active_`). El ctor
inline bool JA_soundEnabled{true}; // obre el device SDL; el dtor el tanca (RAII). Els deleters
inline SDL_AudioDeviceID sdlAudioDevice{0}; // `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via
// `Engine::active()` per parar canals antes d'alliberar.
inline bool fading{false}; class Engine {
inline int fade_start_time{0}; public:
inline int fade_duration{0}; Engine(int freq, SDL_AudioFormat format, int num_channels);
inline float fade_initial_volume{0.0F}; // Corregido de 'int' a 'float' ~Engine();
Engine(const Engine&) = delete;
// --- Forward Declarations --- auto operator=(const Engine&) -> Engine& = delete;
inline void JA_StopMusic(); Engine(Engine&&) = delete;
inline void JA_StopChannel(const int channel); auto operator=(Engine&&) -> Engine& = delete;
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
// Retorna el motor actiu o nullptr si sin ha estat construït. L'usen
// --- Core Functions --- // los deleters de recursos porque no los arriba sin referència directa.
[[nodiscard]] static auto active() noexcept -> Engine*;
inline void JA_Update() {
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) { void update();
if (fading) {
int time = SDL_GetTicks(); // --- Música ---
if (time > (fade_start_time + fade_duration)) { void playMusic(Music* music, int loop = -1);
fading = false; void pauseMusic();
JA_StopMusic(); void resumeMusic();
return; void stopMusic();
} else { void fadeOutMusic(int milliseconds);
const int time_passed = time - fade_start_time; void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
const float percent = (float)time_passed / (float)fade_duration; [[nodiscard]] auto getMusicState() const -> MusicState;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent)); 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
if (current_music->times != 0) { // "chipmunk") — es el comportament arcade clàssic dels comptes
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) { // enrere. Cada `playMusic` crea un stream nuevo con ratio 1.0,
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length); // así que un canvi de track reseteja la velocitat
} // implícitament. No-op si no hay música activa.
if (current_music->times > 0) current_music->times--; void setMusicSpeed(float ratio);
} else { // Registra un callback que es disparà cuando la música actual acabi de
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic(); // 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.
if (JA_soundEnabled) { void setOnMusicEnded(std::function<void()> callback);
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) // Notifica al motor que un Music s'está destruint: si es el current_music
if (channels[i].state == JA_CHANNEL_PLAYING) { // s'atura antes que los seus recursos (stream/vorbis) deixin de ser vàlids.
if (channels[i].times != 0) { void onMusicDeleted(const Music* music);
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); // --- So ---
if (channels[i].times > 0) channels[i].times--; auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
} auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
} else { // Ajusta la velocitat de reproducció d'un canal actiu via
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i); // `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ó.
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) { void setChannelSpeed(int channel, float ratio);
#ifdef _DEBUG // Reproducció con so processat per un efecte. Retorna el canal
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG); // assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
#endif // El sound original solo s'usa per consultar el spec/buffer; el
// canal manipula el buffer ya processat (no reapunta a `sound`).
JA_audioSpec = {format, num_channels, freq}; auto playSoundWithEcho(const Sound* sound, const EchoParams& params, int group = 0) -> int;
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice auto playSoundWithReverb(const Sound* sound, const ReverbParams& params, int group = 0) -> int;
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); void pauseChannel(int channel);
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!"); void resumeChannel(int channel);
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE; void stopChannel(int channel);
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5F; 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.
inline void JA_Quit() { void onSoundDeleted(const Sound* sound);
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
sdlAudioDevice = 0; private:
} void stealCurrentIntoOutgoing(int duration_ms);
void updateOutgoingFade();
// --- Music Functions --- void updateIncomingFade();
void updateCurrentMusic();
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { void updateSoundChannels();
JA_Music_t* music = new JA_Music_t(); // Empenta un buffer ya processat (S16) a un canal lliure y el deixa
// sonar sin bucle. Camí comú dels dos overloads playSoundWith*.
int chan, samplerate; // Retorna el canal o -1 si no queden slots.
short* output; auto playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, int group) -> int;
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
template <typename Fn>
music->spec.channels = chan; void forEachTargetChannel(int channel, Fn&& fn);
music->spec.freq = samplerate;
music->spec.format = SDL_AUDIO_S16; Music* current_music_{nullptr};
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length)); Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{};
SDL_memcpy(music->buffer, output, music->length); SDL_AudioSpec audio_spec_{DEFAULT_SPEC};
free(output); float music_volume_{1.0F};
music->pos = 0; float sound_volume_[MAX_GROUPS]{};
music->state = JA_MUSIC_STOPPED; SDL_AudioDeviceID sdl_audio_device_{0};
OutgoingMusic outgoing_music_;
return music; FadeState incoming_fade_;
} std::function<void()> on_music_ended_;
// Comptador derivat de Channel::has_effect — evita haver-lo de
inline JA_Music_t* JA_LoadMusic(const char* filename) { // recalcular cada vegada que algú demana un play con efecte.
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid. int effect_channels_active_{0};
FILE* f = fopen(filename, "rb");
if (!f) return NULL; // Añadida comprobación de apertura // NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static con sufix _
fseek(f, 0, SEEK_END); static Engine* active_;
long fsize = ftell(f); };
fseek(f, 0, SEEK_SET);
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1)); // --- Factories y destructors (permanents) ---
if (!buffer) { // Añadida comprobación de malloc // No depenen de l'estat del motor: loadMusic/loadSound solo construeixen
fclose(f); // objectes, deleteMusic/deleteSound consulten Engine::active() per parar
return NULL; // canals antes d'alliberar (si el motor aún viu).
} [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*;
if (fread(buffer, fsize, 1, f) != 1) { [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*;
fclose(f); void deleteMusic(Music* music);
free(buffer); [[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
return NULL; void deleteSound(Sound* sound);
}
fclose(f); } // namespace Ja
JA_Music_t* music = JA_LoadMusic(buffer, fsize);
if (music) { // Comprobar que JA_LoadMusic tuvo éxito
music->filename = static_cast<char*>(malloc(strlen(filename) + 1));
if (music->filename) {
strcpy(music->filename, filename);
}
}
free(buffer);
return music;
}
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
if (!JA_musicEnabled || !music) return; // Añadida comprobación de music
JA_StopMusic();
current_music = music;
current_music->pos = 0;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) { // Comprobar creación de stream
SDL_Log("Failed to create audio stream!");
current_music->state = JA_MUSIC_STOPPED;
return;
}
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
}
inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
if (!music) music = current_music;
if (!music) return nullptr; // Añadida comprobación
return music->filename;
}
inline void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
current_music->state = JA_MUSIC_PAUSED;
SDL_UnbindAudioStream(current_music->stream);
}
inline void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada
current_music->state = JA_MUSIC_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
inline void JA_StopMusic() {
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// No liberamos filename aquí, se debería liberar en JA_DeleteMusic
}
inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
}
inline JA_Music_state JA_GetMusicState() {
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
if (!current_music) return JA_MUSIC_INVALID;
return current_music->state;
}
inline void JA_DeleteMusic(JA_Music_t* music) {
if (!music) return;
if (current_music == music) {
JA_StopMusic();
current_music = nullptr;
}
SDL_free(music->buffer);
if (music->stream) SDL_DestroyAudioStream(music->stream);
free(music->filename); // filename se libera aquí
delete music;
}
inline float JA_SetMusicVolume(float volume) {
JA_musicVolume = SDL_clamp(volume, 0.0F, 1.0F);
if (current_music && current_music->stream) {
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
}
return JA_musicVolume;
}
inline void JA_SetMusicPosition(float value) {
if (!current_music) return;
current_music->pos = value * current_music->spec.freq;
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
// el streaming. El streaming siempre parece empezar desde el principio.
}
inline float JA_GetMusicPosition() {
if (!current_music) return 0;
return float(current_music->pos) / float(current_music->spec.freq);
// Nota: Ver `JA_SetMusicPosition`
}
inline void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
// --- Sound Functions ---
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
// Nota: spec se queda con los valores por defecto.
return sound;
}
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
delete sound;
return nullptr;
}
return sound;
}
inline JA_Sound_t* JA_LoadSound(const char* filename) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
delete sound;
return nullptr;
}
return sound;
}
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
if (!JA_soundEnabled || !sound) return -1;
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
// No hay canal libre, reemplazamos el primero
channel = 0;
}
return JA_PlaySoundOnChannel(sound, channel, loop, group);
}
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
if (!JA_soundEnabled || !sound) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group; // Asignar grupo
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) {
SDL_Log("Failed to create audio stream for sound!");
channels[channel].state = JA_CHANNEL_FREE;
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
inline void JA_DeleteSound(JA_Sound_t* sound) {
if (!sound) return;
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
SDL_free(sound->buffer);
delete sound;
}
inline void JA_PauseChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PLAYING) {
channels[i].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PLAYING) {
channels[channel].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
inline void JA_ResumeChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PAUSED) {
channels[i].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PAUSED) {
channels[channel].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
}
}
}
inline void JA_StopChannel(const int channel) {
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state != JA_CHANNEL_FREE) {
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = NULL;
}
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != JA_CHANNEL_FREE) {
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = NULL;
}
}
}
inline JA_Channel_state JA_GetChannelState(const int channel) {
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
return channels[channel].state;
}
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos
{
const float v = SDL_clamp(volume, 0.0F, 1.0F);
if (group == -1) {
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
JA_soundVolume[i] = v;
}
} else if (group >= 0 && group < JA_MAX_GROUPS) {
JA_soundVolume[group] = v;
} else {
return v; // Grupo inválido
}
// Aplicar volumen a canales activos
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
if (group == -1 || channels[i].group == group) {
if (channels[i].stream) {
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
}
}
}
}
return v;
}
inline void JA_EnableSound(const bool value) {
if (!value) {
JA_StopChannel(-1); // Detener todos los canales
}
JA_soundEnabled = value;
}
inline float JA_SetVolume(float volume) {
float v = JA_SetMusicVolume(volume);
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido
return v;
}
@@ -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_;
};
+108
View File
@@ -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
+21
View File
@@ -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
+121 -126
View File
@@ -8,38 +8,38 @@
namespace Defaults { namespace Defaults {
// Configuración de ventana // Configuración de ventana
namespace Window { namespace Window {
constexpr int WIDTH = 640; constexpr int WIDTH = 1280;
constexpr int HEIGHT = 480; constexpr int HEIGHT = 720;
constexpr int MIN_WIDTH = 320; // Mínimo: mitad del original constexpr int MIN_WIDTH = 640; // Mínimo: mitad del baseline (16:9)
constexpr int MIN_HEIGHT = 240; constexpr int MIN_HEIGHT = 360;
// Zoom system // Zoom system
constexpr float BASE_ZOOM = 1.0F; // 640x480 baseline constexpr float BASE_ZOOM = 1.0F; // 1280x720 baseline (16:9)
constexpr float MIN_ZOOM = 0.5F; // 320x240 minimum constexpr float MIN_ZOOM = 0.5F; // 640x360 minimum
constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2) 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 } // namespace Window
// Dimensions base del joc (coordenades lògiques) // Dimensiones base del juego (coordenadas lógicas, 16:9)
namespace Game { namespace Game {
constexpr int WIDTH = 640; constexpr int WIDTH = 1280;
constexpr int HEIGHT = 480; constexpr int HEIGHT = 720;
} // namespace Game } // 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 { namespace Zones {
// --- CONFIGURACIÓ DE PORCENTATGES --- // --- 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 SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior
constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central
constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior 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 constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat
// --- CÀLCULS AUTOMÀTICS DE PÍXELS --- // --- 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 // Alçades
constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT; 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) --- // --- ZONES FINALS (SDL_FRect) ---
// Marcador superior (reservat per a futur ús) // Marcador superior (reservado para futuro uso)
// Ocupa: 10% superior (0-48px) // Ocupa el 2% superior
constexpr SDL_FRect SCOREBOARD_TOP = { constexpr SDL_FRect SCOREBOARD_TOP = {
0.0F, // x = 0.0 0.0F, // x = 0.0
SCOREBOARD_TOP_Y, // y = 0.0 SCOREBOARD_TOP_Y, // y = 0.0
static_cast<float>(Game::WIDTH), // w = 640.0 static_cast<float>(Game::WIDTH), // ancho completo
SCOREBOARD_TOP_H // h = 48.0 SCOREBOARD_TOP_H // alto
}; };
// Àrea de joc principal (contenidor del 80% central, sense padding) // Área de juego principal (contenedor del 80% central, sin padding)
// Ocupa: 10-90% (48-432px), ample complet // Ocupa el 88% central, ancho completo
constexpr SDL_FRect MAIN_PLAYAREA = { constexpr SDL_FRect MAIN_PLAYAREA = {
0.0F, // x = 0.0 0.0F, // x = 0.0
MAIN_PLAYAREA_Y, // y = 48.0 MAIN_PLAYAREA_Y, // debajo del scoreboard superior
static_cast<float>(Game::WIDTH), // w = 640.0 static_cast<float>(Game::WIDTH), // ancho completo
MAIN_PLAYAREA_H // h = 384.0 MAIN_PLAYAREA_H // alto
}; };
// Zona de joc real (amb padding horizontal del 5%) // Zona de juego real (con padding horizontal del 5%)
// Ocupa: dins de MAIN_PLAYAREA, amb marges laterals // Ocupa: dentro de MAIN_PLAYAREA, con márgenes laterales
// S'utilitza per a límits del joc, col·lisions, spawn // Se utiliza para límites del juego, colisiones, spawn
constexpr SDL_FRect PLAYAREA = { constexpr SDL_FRect PLAYAREA = {
PLAYAREA_PADDING_H, // x = 32.0 PLAYAREA_PADDING_H, // padding horizontal
MAIN_PLAYAREA_Y, // y = 48.0 (igual que MAIN_PLAYAREA) MAIN_PLAYAREA_Y, // debajo del scoreboard superior (igual que MAIN_PLAYAREA)
Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // w = 576.0 Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // ancho con padding
MAIN_PLAYAREA_H // h = 384.0 (igual que MAIN_PLAYAREA) MAIN_PLAYAREA_H // alto (igual que MAIN_PLAYAREA)
}; };
// Marcador inferior (marcador actual) // Marcador inferior (marcador actual)
// Ocupa: 10% inferior (432-480px) // Ocupa el 10% inferior
constexpr SDL_FRect SCOREBOARD = { constexpr SDL_FRect SCOREBOARD = {
0.0F, // x = 0.0 0.0F, // x = 0.0
SCOREBOARD_BOTTOM_Y, // y = 432.0 SCOREBOARD_BOTTOM_Y, // fondo
static_cast<float>(Game::WIDTH), // w = 640.0 static_cast<float>(Game::WIDTH), // ancho completo
SCOREBOARD_BOTTOM_H // h = 48.0 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; constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f;
} // namespace Zones } // namespace Zones
@@ -101,14 +101,24 @@ constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f;
namespace Entities { namespace Entities {
constexpr int MAX_ORNIS = 15; constexpr int MAX_ORNIS = 15;
constexpr int MAX_BALES = 3; constexpr int MAX_BALES = 3;
constexpr int MAX_IPUNTS = 30;
constexpr float SHIP_RADIUS = 12.0F; constexpr float SHIP_RADIUS = 12.0F;
constexpr float ENEMY_RADIUS = 20.0F; constexpr float ENEMY_RADIUS = 20.0F;
constexpr float BULLET_RADIUS = 3.0F; constexpr float BULLET_RADIUS = 3.0F;
} // namespace Entities } // 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 { namespace Ship {
// Invulnerabilidad post-respawn // Invulnerabilidad post-respawn
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad 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 constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
// Ratios de animación (inicio y fin como porcentajes del tiempo total) // 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_INIT = 0.30F;
constexpr float INIT_HUD_RECT_RATIO_END = 0.85F; 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_INIT = 0.60F;
constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F; 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_INIT = 0.0F;
constexpr float INIT_HUD_SHIP1_RATIO_END = 1.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_INIT = 0.20F;
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F; 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) // Explosions (debris physics)
namespace Debris { 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 VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (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_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
constexpr float ROTACIO_MAX = 0.3F; // Rotació màxima (rad/s ~17.2°/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 = 2.0F; // Duració màxima (segons) - enemy/bullet debris
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION) constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s) 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_MIN = 0.7F; // Mínimo 70% del drotacio heredat
constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% 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²) 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 // Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies // Prevents "vortex trap" problem with high-rotation enemies
constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s) 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>; constexpr float PI = std::numbers::pi_v<float>;
} // namespace Math } // namespace Math
// Colores (oscilación para efecto CRT) // La antigua oscilación CPU (namespace Color) se ha migrado al shader de
namespace Color { // postpro. Los parámetros de flicker / background pulse viven ahora en
// Frecuencia de oscilación // data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl.
constexpr float FREQUENCY = 6.0F; // 1 Hz (1 ciclo/segundo)
// Color de líneas (efecto fósforo verde CRT) // Brillantor (control de intensitat per cada type de entidad)
constexpr uint8_t LINE_MIN_R = 100; // Verde oscuro
constexpr uint8_t LINE_MIN_G = 200;
constexpr uint8_t LINE_MIN_B = 100;
constexpr uint8_t LINE_MAX_R = 100; // Verde brillante
constexpr uint8_t LINE_MAX_G = 255;
constexpr uint8_t LINE_MAX_B = 100;
// Color de fondo (pulso sutil verde oscuro)
constexpr uint8_t BACKGROUND_MIN_R = 0; // Negro
constexpr uint8_t BACKGROUND_MIN_G = 5;
constexpr uint8_t BACKGROUND_MIN_B = 0;
constexpr uint8_t BACKGROUND_MAX_R = 0; // Verde muy oscuro
constexpr uint8_t BACKGROUND_MAX_G = 15;
constexpr uint8_t BACKGROUND_MAX_B = 0;
} // namespace Color
// Brillantor (control de intensitat per cada tipus d'entitat)
namespace Brightness { namespace Brightness {
// Brillantor estàtica per entitats de joc (0.0-1.0) // Brillantor estàtica per entidades de juego (0.0-1.0)
constexpr float NAU = 1.0F; // Màxima visibilitat (jugador) constexpr float NAU = 1.0F; // Màxima visibilitat (player)
constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys) constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys)
constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat) constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat)
// Starfield: gradient segons distància al centre // Starfield: gradient segons distancia al centro
// distancia_centre: 0.0 (centre) → 1.0 (vora pantalla) // distancia_centre: 0.0 (centro) → 1.0 (vora pantalla)
// brightness = MIN + (MAX - MIN) * distancia_centre // 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) constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla)
} // namespace Brightness } // namespace Brightness
// Renderització (V-Sync i altres opcions de render) // Renderització (V-Sync i altres opciones de render)
namespace Rendering { namespace Rendering {
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
} // namespace Rendering } // namespace Rendering
// Audio (sistema de so i música) // Audio (sistema de sonido y música) — usado por Audio::Config en init()
namespace Audio { namespace Audio {
constexpr float VOLUME = 1.0F; // Volumen maestro (0.0 a 1.0) constexpr bool ENABLED = true; // Audio habilitado por defecto
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 } // namespace Audio
// Música (pistas de fondo) // Música (pistas de fondo)
namespace Music { 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* GAME_TRACK = "game.ogg"; // Pista de juego
constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo
constexpr int FADE_DURATION_MS = 1000; // Fade out duration 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) // Efectes de so (sons puntuals)
namespace Sound { 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* CONTINUE = "effects/continue.wav"; // Cuenta atras
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa 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* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
constexpr const char* LOGO = "effects/logo.wav"; // Logo 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" constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
} // namespace Sound } // namespace Sound
@@ -328,7 +323,7 @@ constexpr SDL_Keycode SHOOT = SDLK_LSHIFT;
} // namespace P2 } // namespace P2
} // namespace Controls } // namespace Controls
// Enemy type configuration (tipus d'enemics) // Enemy type configuration (type de enemigos)
namespace Enemies { namespace Enemies {
// Pentagon (esquivador - zigzag evasion) // Pentagon (esquivador - zigzag evasion)
namespace Pentagon { 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"; constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon } // namespace Pentagon
// Quadrat (perseguidor - tracks player) // Cuadrado (perseguidor - tracks player)
namespace Quadrat { namespace Cuadrado {
constexpr float VELOCITAT = 40.0F; // px/s (medium speed) 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_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%] constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%]
constexpr float DROTACIO_MAX = 1.5F; // [+50%] constexpr float DROTACIO_MAX = 1.5F; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp"; constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Quadrat } // namespace Cuadrado
// Molinillo (agressiu - fast straight lines, proximity spin-up) // Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Molinillo { namespace Molinillo {
@@ -396,54 +391,54 @@ constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
} // namespace Spawn } // namespace Spawn
// Scoring system (puntuació per tipus d'enemic) // Scoring system (puntuación per type de enemy)
namespace Scoring { namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentàgon (esquivador, 35 px/s) constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
constexpr int QUADRAT_SCORE = 150; // Quadrat (perseguidor, 40 px/s) constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s)
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s) constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring } // namespace Scoring
} // namespace Enemies } // 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 Title {
namespace Ships { namespace Ships {
// ============================================================ // ============================================================
// PARÀMETRES BASE (ajustar aquí per experimentar) // 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) constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp)
// 2. Altura vertical (cercanía al centro) // 2. Altura vertical (cercanía al centro)
// Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla) // Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla)
constexpr float TARGET_Y_RATIO = 0.15625F; constexpr float TARGET_Y_RATIO = 0.15625F;
// 3. Radio orbital (distancia radial desde centro en coordenadas polares) // 3. Radio orbital (distance radial desde centro en coordenadas polares)
constexpr float CLOCK_RADIUS = 150.0F; // Distància des del centre constexpr float CLOCK_RADIUS = 150.0F; // Distancia des del centro
// 4. Ángulos de posición (clock positions en coordenadas polares) // 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_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) 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 constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp
// 6. Margen de seguridad para offset de entrada // 6. Margen de seguridad para offset de entrada
constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado) 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) // Centro de la pantalla (point de referència)
constexpr float CENTER_X = Game::WIDTH / 2.0F; // 320.0f constexpr float CENTER_X = Game::WIDTH / 2.0F; // auto-derivado de Game::WIDTH
constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // 240.0f constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // auto-derivado de Game::HEIGHT
// Posicions target (calculades dinàmicament des dels paràmetres base) // Posicions target (calculades dinàmicament des dels parámetros base)
// Nota: std::cos/sin no són constexpr en C++20, però funcionen en runtime // Nota: std::cos/sin no són constexpr en C++20, pero funcionen en runtime
// Les funcions inline són optimitzades pel compilador (zero overhead) // Les funciones inline són optimitzades por el compilador (zero overhead)
inline float P1_TARGET_X() { inline float P1_TARGET_X() {
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE)); 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); return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
} }
// Escales d'animació (relatives a SHIP_BASE_SCALE) // Escales de animación (relatives a SHIP_BASE_SCALE)
constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més gran constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més grande
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotant: escala base constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base
// Offset d'entrada (ajustat automàticament a l'escala) // Offset de entrada (ajustat automáticoament a l'scale)
// Fórmula: (radi màxim de la nau * escala d'entrada) + marge // 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; constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
// Punt de fuga (centre per a l'animació de sortida) // Vec2 de fuga (centro para l'animación de salida)
constexpr float VANISHING_POINT_X = CENTER_X; // 320.0f constexpr float VANISHING_POINT_X = CENTER_X; // auto-derivado de Game::WIDTH
constexpr float VANISHING_POINT_Y = CENTER_Y; // 240.0f constexpr float VANISHING_POINT_Y = CENTER_Y; // auto-derivado de Game::HEIGHT
// ============================================================ // ============================================================
// ANIMACIONS (durades, oscil·lacions, delays) // ANIMACIONS (durades, oscil·lacions, delays)
// ============================================================ // ============================================================
// Durades d'animació // Durades de animación
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) 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_X = 4.0F; // Amplitud X (píxels)
constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (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_FREQUENCY_Y_BASE = 0.7F; // Hz
constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°) 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 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 // Delay global antes de start l'animación de entrada al state MAIN
constexpr float ENTRANCE_DELAY = 5.0F; // Temps d'espera abans que les naus entrin 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 P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
} // namespace Ships } // namespace Ships
namespace Layout { 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 LOGO_POS = 0.20F; // Logo "ORNI"
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY" constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY"
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright 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 LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px)
constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px) 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 LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY" constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright 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; constexpr float TEXT_SPACING = 2.0F;
} // namespace Layout } // namespace Layout
} // namespace Title } // namespace Title
// Floating score numbers (números flotants de puntuació) // Floating score numbers (números flotantes de puntuación)
namespace FloatingScore { namespace FloatingScore {
constexpr float LIFETIME = 2.0F; // Duració màxima (segons) constexpr float LIFETIME = 2.0F; // Duració màxima (segons)
constexpr float VELOCITY_Y = -30.0F; // Velocitat vertical (px/s, negatiu = amunt) constexpr float VELOCITY_Y = -30.0F; // Velocidad vertical (px/s, negatiu = amunt)
constexpr float VELOCITY_X = 0.0F; // Velocitat horizontal (px/s) 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 SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador)
constexpr float SPACING = 0.0F; // Espaiat entre caràcters constexpr float SPACING = 0.0F; // Espaiat entre caràcters
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS) constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
-49
View File
@@ -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
+72
View File
@@ -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
+19 -19
View File
@@ -1,5 +1,5 @@
// shape.cpp - Implementació del sistema de formes vectorials // 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" #include "core/graphics/shape.hpp"
@@ -11,31 +11,31 @@
namespace Graphics { namespace Graphics {
Shape::Shape(const std::string& filepath) Shape::Shape(const std::string& filepath)
: centre_({.x = 0.0F, .y = 0.0F}), : center_({.x = 0.0F, .y = 0.0F}),
escala_defecte_(1.0F), escala_defecte_(1.0F),
nom_("unnamed") { nom_("unnamed") {
carregar(filepath); load(filepath);
} }
bool Shape::carregar(const std::string& filepath) { bool Shape::load(const std::string& filepath) {
// Llegir fitxer // Llegir file
std::ifstream file(filepath); std::ifstream file(filepath);
if (!file.is_open()) { if (!file.is_open()) {
std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n'; std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n';
return false; return false;
} }
// Llegir tot el contingut // Llegir todo el contingut
std::stringstream buffer; std::stringstream buffer;
buffer << file.rdbuf(); buffer << file.rdbuf();
std::string contingut = buffer.str(); std::string contingut = buffer.str();
file.close(); file.close();
// Parsejar // 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::istringstream iss(contingut);
std::string line; std::string line;
@@ -55,7 +55,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
try { try {
escala_defecte_ = std::stof(extract_value(line)); escala_defecte_ = std::stof(extract_value(line));
} catch (...) { } 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; escala_defecte_ = 1.0F;
} }
} else if (starts_with(line, "center:")) { } else if (starts_with(line, "center:")) {
@@ -65,7 +65,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
if (points.size() >= 2) { if (points.size() >= 2) {
primitives_.push_back({PrimitiveType::POLYLINE, points}); primitives_.push_back({PrimitiveType::POLYLINE, points});
} else { } else {
std::cerr << "[Shape] Warning: polyline amb menys de 2 punts ignorada" std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada"
<< '\n'; << '\n';
} }
} else if (starts_with(line, "line:")) { } else if (starts_with(line, "line:")) {
@@ -73,7 +73,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
if (points.size() == 2) { if (points.size() == 2) {
primitives_.push_back({PrimitiveType::LINE, points}); primitives_.push_back({PrimitiveType::LINE, points});
} else { } else {
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 punts" std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points"
<< '\n'; << '\n';
} }
} }
@@ -81,7 +81,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
} }
if (primitives_.empty()) { if (primitives_.empty()) {
std::cerr << "[Shape] Error: cap primitiva carregada" << '\n'; std::cerr << "[Shape] Error: sin primitiva carregada" << '\n';
return false; return false;
} }
@@ -124,18 +124,18 @@ void Shape::parse_center(const std::string& value) {
size_t comma = val.find(','); size_t comma = val.find(',');
if (comma != std::string::npos) { if (comma != std::string::npos) {
try { try {
centre_.x = std::stof(trim(val.substr(0, comma))); center_.x = std::stof(trim(val.substr(0, comma)));
centre_.y = std::stof(trim(val.substr(comma + 1))); center_.y = std::stof(trim(val.substr(comma + 1)));
} catch (...) { } catch (...) {
std::cerr << "[Shape] Warning: centre invàlid, usant (0,0)" << '\n'; std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n';
centre_ = {.x = 0.0F, .y = 0.0F}; center_ = {.x = 0.0F, .y = 0.0F};
} }
} }
} }
// Helper: parse points "x1,y1 x2,y2 x3,y3" // Helper: parse points "x1,y1 x2,y2 x3,y3"
std::vector<Punt> Shape::parse_points(const std::string& str) const { std::vector<Vec2> Shape::parse_points(const std::string& str) const {
std::vector<Punt> points; std::vector<Vec2> points;
std::istringstream iss(trim(str)); std::istringstream iss(trim(str));
std::string pair; 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)); float y = std::stof(pair.substr(comma + 1));
points.push_back({x, y}); points.push_back({x, y});
} catch (...) { } catch (...) {
std::cerr << "[Shape] Warning: punt invàlid ignorat: " << pair std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair
<< '\n'; << '\n';
} }
} }
+15 -15
View File
@@ -1,5 +1,5 @@
// shape.hpp - Sistema de formes vectorials // shape.hpp - Sistema de formes vectorials
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#pragma once #pragma once
@@ -10,38 +10,38 @@
namespace Graphics { namespace Graphics {
// Tipus de primitiva dins d'una forma // Tipo de primitiva dins de una shape
enum class PrimitiveType { enum class PrimitiveType {
POLYLINE, // Seqüència de punts connectats POLYLINE, // Secuencia de points connectats
LINE // Línia individual (2 punts) LINE // Línia individual (2 points)
}; };
// Primitiva individual (polyline o line) // Primitiva individual (polyline o line)
struct ShapePrimitive { struct ShapePrimitive {
PrimitiveType type; 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 { class Shape {
public: public:
// Constructors // Constructors
Shape() = default; Shape() = default;
explicit Shape(const std::string& filepath); explicit Shape(const std::string& filepath);
// Carregar forma des de fitxer .shp // Carregar shape desde file .shp
bool carregar(const std::string& filepath); bool load(const std::string& filepath);
// Parsejar forma des de buffer de memòria (per al sistema de recursos) // Parsejar shape desde buffer de memòria (per al sistema de recursos)
bool parsejar_fitxer(const std::string& contingut); bool parseFile(const std::string& contingut);
// Getters // Getters
[[nodiscard]] const std::vector<ShapePrimitive>& get_primitives() const { [[nodiscard]] const std::vector<ShapePrimitive>& get_primitives() const {
return primitives_; 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]] 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ó // Info de depuració
[[nodiscard]] std::string get_nom() const { return nom_; } [[nodiscard]] std::string get_nom() const { return nom_; }
@@ -49,16 +49,16 @@ class Shape {
private: private:
std::vector<ShapePrimitive> primitives_; 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) 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 // Helpers privats per parsejar
[[nodiscard]] std::string trim(const std::string& str) const; [[nodiscard]] std::string trim(const std::string& str) const;
[[nodiscard]] bool starts_with(const std::string& str, const std::string& prefix) const; [[nodiscard]] bool starts_with(const std::string& str, const std::string& prefix) const;
[[nodiscard]] std::string extract_value(const std::string& line) const; [[nodiscard]] std::string extract_value(const std::string& line) const;
void parse_center(const std::string& value); 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 } // namespace Graphics
+10 -10
View File
@@ -1,5 +1,5 @@
// shape_loader.cpp - Implementació del carregador amb caché // shape_loader.cpp - Implementació del carregador con caché
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#include "core/graphics/shape_loader.hpp" #include "core/graphics/shape_loader.hpp"
@@ -9,7 +9,7 @@
namespace Graphics { 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::unordered_map<std::string, std::shared_ptr<Shape>> ShapeLoader::cache_;
std::string ShapeLoader::base_path_ = "data/shapes/"; 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 // Load from resource system
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized); std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
if (data.empty()) { if (data.empty()) {
std::cerr << "[ShapeLoader] Error: no s'ha pogut carregar " << normalized std::cerr << "[ShapeLoader] Error: no s'ha pogut load " << normalized
<< '\n'; << '\n';
return nullptr; return nullptr;
} }
@@ -40,15 +40,15 @@ std::shared_ptr<Shape> ShapeLoader::load(const std::string& filename) {
// Convert bytes to string and parse // Convert bytes to string and parse
std::string file_content(data.begin(), data.end()); std::string file_content(data.begin(), data.end());
auto shape = std::make_shared<Shape>(); 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 std::cerr << "[ShapeLoader] Error: no s'ha pogut parsejar " << normalized
<< '\n'; << '\n';
return nullptr; return nullptr;
} }
// Verify shape is valid // Verify shape is valid
if (!shape->es_valida()) { if (!shape->isValid()) {
std::cerr << "[ShapeLoader] Error: forma invàlida " << normalized << '\n'; std::cerr << "[ShapeLoader] Error: shape invàlida " << normalized << '\n';
return nullptr; return nullptr;
} }
@@ -69,17 +69,17 @@ void ShapeLoader::clear_cache() {
size_t ShapeLoader::get_cache_size() { return cache_.size(); } size_t ShapeLoader::get_cache_size() { return cache_.size(); }
std::string ShapeLoader::resolve_path(const std::string& filename) { 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] == '/') { if (!filename.empty() && filename[0] == '/') {
return filename; 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_)) { if (filename.starts_with(base_path_)) {
return filename; return filename;
} }
// Altrament, afegir base_path (ara suporta subdirectoris) // Altrament, añadir base_path (ara suporta subdirectoris)
return base_path_ + filename; return base_path_ + filename;
} }
+4 -4
View File
@@ -1,5 +1,5 @@
// shape_loader.hpp - Carregador estàtic de formes amb caché // shape_loader.hpp - Carregador estàtic de formes con caché
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#pragma once #pragma once
@@ -11,13 +11,13 @@
namespace Graphics { namespace Graphics {
// Carregador estàtic de formes amb caché // Carregador estàtic de formes con caché
class ShapeLoader { class ShapeLoader {
public: public:
// No instanciable (tot estàtic) // No instanciable (tot estàtic)
ShapeLoader() = delete; ShapeLoader() = delete;
// Carregar forma des de fitxer (amb caché) // Carregar shape desde file (con caché)
// Retorna punter compartit (nullptr si error) // Retorna punter compartit (nullptr si error)
// Exemple: load("ship.shp") → busca a "data/shapes/ship.shp" // Exemple: load("ship.shp") → busca a "data/shapes/ship.shp"
static std::shared_ptr<Shape> load(const std::string& filename); static std::shared_ptr<Shape> load(const std::string& filename);
+53 -54
View File
@@ -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 // © 2025 Orni Attack
#include "core/graphics/starfield.hpp" #include "core/graphics/starfield.hpp"
@@ -14,38 +14,38 @@
namespace Graphics { namespace Graphics {
// Constructor // Constructor
Starfield::Starfield(SDL_Renderer* renderer, Starfield::Starfield(Rendering::Renderer* renderer,
const Punt& punt_fuga, const Vec2& punt_fuga,
const SDL_FRect& area, const SDL_FRect& area,
int densitat) int densitat)
: renderer_(renderer), : renderer_(renderer),
punt_fuga_(punt_fuga), punt_fuga_(punt_fuga),
area_(area), area_(area),
densitat_(densitat) { densitat_(densitat) {
// Carregar forma d'estrella amb ShapeLoader // Carregar shape de estrella con ShapeLoader
shape_estrella_ = ShapeLoader::load("star.shp"); shape_estrella_ = ShapeLoader::load("star.shp");
if (!shape_estrella_ || !shape_estrella_->es_valida()) { if (!shape_estrella_ || !shape_estrella_->isValid()) {
std::cerr << "ERROR: No s'ha pogut carregar star.shp" << '\n'; std::cerr << "ERROR: No s'ha pogut load star.shp" << '\n';
return; return;
} }
// Configurar 3 capes amb diferents velocitats i escales // Configurar 3 capes con diferents velocitats i escales
// Capa 0: Fons llunyà (lenta, petita) // Capa 0: Fons llunyà (lenta, pequeña)
capes_.push_back({20.0F, 0.3F, 0.8F, densitat / 3}); capes_.push_back({20.0F, 0.3F, 0.8F, densitat / 3});
// Capa 1: Profunditat mitjana // Capa 1: Profunditat mitjana
capes_.push_back({40.0F, 0.5F, 1.2F, densitat / 3}); 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}); 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 dx = std::max(punt_fuga_.x, area_.w - punt_fuga_.x);
float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y); float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y);
radi_max_ = std::sqrt((dx * dx) + (dy * dy)); 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++) { for (int capa_idx = 0; capa_idx < 3; capa_idx++) {
int num = capes_[capa_idx].num_estrelles; int num = capes_[capa_idx].num_estrelles;
for (int i = 0; i < num; i++) { for (int i = 0; i < num; i++) {
@@ -55,55 +55,55 @@ Starfield::Starfield(SDL_Renderer* renderer,
// Angle aleatori // Angle aleatori
estrella.angle = (static_cast<float>(rand()) / RAND_MAX) * 2.0F * Defaults::Math::PI; 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; 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_; float radi = estrella.distancia_centre * radi_max_;
estrella.posicio.x = punt_fuga_.x + (radi * std::cos(estrella.angle)); estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
estrella.posicio.y = punt_fuga_.y + (radi * std::sin(estrella.angle)); estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle));
estrelles_.push_back(estrella); estrelles_.push_back(estrella);
} }
} }
} }
// Inicialitzar una estrella (nova o regenerada) // Inicialitzar una estrella (nueva o regenerada)
void Starfield::inicialitzar_estrella(Estrella& estrella) const { 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; 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; 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_; float radi = estrella.distancia_centre * radi_max_;
estrella.posicio.x = punt_fuga_.x + (radi * std::cos(estrella.angle)); estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
estrella.posicio.y = punt_fuga_.y + (radi * std::sin(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 { bool Starfield::fora_area(const Estrella& estrella) const {
return (estrella.posicio.x < area_.x || return (estrella.position.x < area_.x ||
estrella.posicio.x > area_.x + area_.w || estrella.position.x > area_.x + area_.w ||
estrella.posicio.y < area_.y || estrella.position.y < area_.y ||
estrella.posicio.y > area_.y + area_.h); 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 { float Starfield::calcular_escala(const Estrella& estrella) const {
const CapaConfig& capa = capes_[estrella.capa]; const CapaConfig& capa = capes_[estrella.capa];
// Interpolació lineal basada en distància del centre // Interpolació lineal basada en distancia del centro
// distancia_centre: 0.0 (centre) → 1.0 (vora) // distancia_centre: 0.0 (centro) → 1.0 (vora)
return capa.escala_min + return capa.escala_min +
((capa.escala_max - capa.escala_min) * estrella.distancia_centre); ((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 { float Starfield::calcular_brightness(const Estrella& estrella) const {
// Interpolació lineal: estrelles properes (vora) més brillants // 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 + float brightness_base = Defaults::Brightness::STARFIELD_MIN +
((Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) * ((Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) *
estrella.distancia_centre); estrella.distancia_centre);
@@ -112,23 +112,23 @@ float Starfield::calcular_brightness(const Estrella& estrella) const {
return std::min(1.0F, brightness_base * multiplicador_brightness_); return std::min(1.0F, brightness_base * multiplicador_brightness_);
} }
// Actualitzar posicions de les estrelles // Actualitzar posicions de las estrelles
void Starfield::actualitzar(float delta_time) { void Starfield::update(float delta_time) {
for (auto& estrella : estrelles_) { for (auto& estrella : estrelles_) {
// Obtenir configuració de la capa // Obtenir configuración de la capa
const CapaConfig& capa = capes_[estrella.capa]; const CapaConfig& capa = capes_[estrella.capa];
// Moure cap a fora des del centre // Moure hacia fuera des del centro
float velocitat = capa.velocitat_base; float velocity = capa.velocitat_base;
float dx = velocitat * std::cos(estrella.angle) * delta_time; float dx = velocity * std::cos(estrella.angle) * delta_time;
float dy = velocitat * std::sin(estrella.angle) * delta_time; float dy = velocity * std::sin(estrella.angle) * delta_time;
estrella.posicio.x += dx; estrella.position.x += dx;
estrella.posicio.y += dy; estrella.position.y += dy;
// Actualitzar distància del centre // Actualitzar distancia del centro
float dx_centre = estrella.posicio.x - punt_fuga_.x; float dx_centre = estrella.position.x - punt_fuga_.x;
float dy_centre = estrella.posicio.y - punt_fuga_.y; float dy_centre = estrella.position.y - punt_fuga_.y;
float dist_px = std::sqrt((dx_centre * dx_centre) + (dy_centre * dy_centre)); float dist_px = std::sqrt((dx_centre * dx_centre) + (dy_centre * dy_centre));
estrella.distancia_centre = dist_px / radi_max_; 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 multiplicador_brightness_ = std::max(0.0F, multiplier); // Evitar valors negatius
} }
// Dibuixar totes les estrelles // Dibuixar todas las estrelles
void Starfield::dibuixar() { void Starfield::draw() {
if (!shape_estrella_->es_valida()) { if (!shape_estrella_->isValid()) {
return; return;
} }
for (const auto& estrella : estrelles_) { for (const auto& estrella : estrelles_) {
// Calcular escala i brightness dinàmicament // Calcular scale i brightness dinàmicament
float escala = calcular_escala(estrella); float scale = calcular_escala(estrella);
float brightness = calcular_brightness(estrella); float brightness = calcular_brightness(estrella);
// Renderitzar estrella sense rotació // Renderizar estrella sin rotación
Rendering::render_shape( Rendering::render_shape(
renderer_, renderer_,
shape_estrella_, shape_estrella_,
estrella.posicio, estrella.position,
0.0F, // angle (les estrelles no giren) 0.0F, // angle (las estrelles no giren)
escala, // escala dinàmica scale, // scale dinàmica
true, // dibuixar 1.0F, // progress (siempre visible)
1.0F, // progress (sempre visible)
brightness // brightness dinàmica brightness // brightness dinàmica
); );
} }
+33 -31
View File
@@ -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 // © 2025 Orni Attack
#pragma once #pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <memory> #include <memory>
@@ -13,70 +15,70 @@
namespace Graphics { namespace Graphics {
// Configuració per cada capa de profunditat // Configuración per cada capa de profunditat
struct CapaConfig { struct CapaConfig {
float velocitat_base; // Velocitat base d'aquesta capa (px/s) float velocitat_base; // Velocidad base de esta capa (px/s)
float escala_min; // Escala mínima prop del centre float escala_min; // Escala mínima prop del centro
float escala_max; // Escala màxima al límit de pantalla 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 { class Starfield {
public: public:
// Constructor // Constructor
// - renderer: SDL renderer // - renderer: SDL renderer
// - punt_fuga: punt d'origen/fuga des d'on surten les estrelles // - punt_fuga: point de origin/fuga des de on surten las estrelles
// - area: rectangle on actuen les estrelles (SDL_FRect) // - area: rectangle on actuen las estrelles (SDL_FRect)
// - densitat: nombre total d'estrelles (es divideix entre capes) // - densitat: nombre total de estrelles (es divideix entre capes)
Starfield(SDL_Renderer* renderer, Starfield(Rendering::Renderer* renderer,
const Punt& punt_fuga, const Vec2& punt_fuga,
const SDL_FRect& area, const SDL_FRect& area,
int densitat = 150); int densitat = 150);
// Actualitzar posicions de les estrelles // Actualitzar posicions de las estrelles
void actualitzar(float delta_time); void update(float delta_time);
// Dibuixar totes les estrelles // Dibuixar todas las estrelles
void dibuixar(); void draw();
// Setters per ajustar paràmetres en temps real // Setters per ajustar parámetros en time real
void set_punt_fuga(const Punt& punt) { punt_fuga_ = punt; } void set_punt_fuga(const Vec2& point) { punt_fuga_ = point; }
void set_brightness(float multiplier); void set_brightness(float multiplier);
private: private:
// Estructura interna per cada estrella // Estructura interna per cada estrella
struct Estrella { struct Estrella {
Punt posicio; // Posició actual Vec2 position; // Posición actual
float angle; // Angle de moviment (radians) float angle; // Angle de movement (radians)
float distancia_centre; // Distància normalitzada del centre (0.0-1.0) float distancia_centre; // Distancia normalitzada del centro (0.0-1.0)
int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop) 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; 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; [[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; [[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; [[nodiscard]] float calcular_brightness(const Estrella& estrella) const;
// Dades // Dades
std::vector<Estrella> estrelles_; 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_; std::shared_ptr<Shape> shape_estrella_;
SDL_Renderer* renderer_; Rendering::Renderer* renderer_;
// Configuració // Configuración
Punt punt_fuga_; // Punt d'origen de les estrelles Vec2 punt_fuga_; // Vec2 de origin de las estrelles
SDL_FRect area_; // Àrea activa SDL_FRect area_; // Àrea activa
float radi_max_; // Distància màxima del centre al límit de pantalla float radi_max_; // Distancia màxima del centro al límit de pantalla
int densitat_; // Nombre total d'estrelles int densitat_; // Nombre total de estrelles
float multiplicador_brightness_{1.0F}; // Multiplicador de brillantor (1.0 = default) float multiplicador_brightness_{1.0F}; // Multiplicador de brightness (1.0 = default)
}; };
} // namespace Graphics } // namespace Graphics
+37 -37
View File
@@ -1,5 +1,5 @@
// vector_text.cpp - Implementació del sistema de text vectorial // 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 // Test pre-commit hook
#include "core/graphics/vector_text.hpp" #include "core/graphics/vector_text.hpp"
@@ -11,11 +11,11 @@
namespace Graphics { 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_width = 20.0F; // Amplada base del caràcter
constexpr float char_height = 40.0F; // Altura 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) { : renderer_(renderer) {
load_charset(); load_charset();
} }
@@ -26,10 +26,10 @@ void VectorText::load_charset() {
std::string filename = get_shape_filename(c); std::string filename = get_shape_filename(c);
auto shape = ShapeLoader::load(filename); auto shape = ShapeLoader::load(filename);
if (shape && shape->es_valida()) { if (shape && shape->isValid()) {
chars_[c] = shape; chars_[c] = shape;
} else { } else {
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n'; << '\n';
} }
} }
@@ -39,10 +39,10 @@ void VectorText::load_charset() {
std::string filename = get_shape_filename(c); std::string filename = get_shape_filename(c);
auto shape = ShapeLoader::load(filename); auto shape = ShapeLoader::load(filename);
if (shape && shape->es_valida()) { if (shape && shape->isValid()) {
chars_[c] = shape; chars_[c] = shape;
} else { } else {
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n'; << '\n';
} }
} }
@@ -54,25 +54,25 @@ void VectorText::load_charset() {
std::string filename = get_shape_filename(c); std::string filename = get_shape_filename(c);
auto shape = ShapeLoader::load(filename); auto shape = ShapeLoader::load(filename);
if (shape && shape->es_valida()) { if (shape && shape->isValid()) {
chars_[c] = shape; chars_[c] = shape;
} else { } else {
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n'; << '\n';
} }
} }
// Cargar símbolo de copyright (©) - UTF-8 U+00A9 // 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 char c = '\xA9'; // 169 decimal
std::string filename = "font/char_copyright.shp"; std::string filename = "font/char_copyright.shp";
auto shape = ShapeLoader::load(filename); auto shape = ShapeLoader::load(filename);
if (shape && shape->es_valida()) { if (shape && shape->isValid()) {
chars_[c] = shape; chars_[c] = shape;
} else { } else {
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
<< '\n'; << '\n';
} }
} }
@@ -82,7 +82,7 @@ void VectorText::load_charset() {
} }
std::string VectorText::get_shape_filename(char c) const { 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) { switch (c) {
case '0': case '0':
case '1': case '1':
@@ -168,7 +168,7 @@ std::string VectorText::get_shape_filename(char c) const {
case '?': case '?':
return "font/char_question.shp"; return "font/char_question.shp";
case ' ': case ' ':
return ""; // Espai es maneja sense carregar shape return ""; // Espai es maneja sin load shape
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9 case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
return "font/char_copyright.shp"; return "font/char_copyright.shp";
@@ -182,23 +182,23 @@ bool VectorText::is_supported(char c) const {
return chars_.contains(c); 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) { if (renderer_ == nullptr) {
return; return;
} }
// Ancho de un carácter base (20 px a escala 1.0) // Ancho de un carácter base (20 px a scale 1.0)
const float char_width_scaled = char_width * escala; const float char_width_scaled = char_width * scale;
// Spacing escalado // Spacing escalado
const float spacing_scaled = spacing * escala; const float spacing_scaled = spacing * scale;
// Altura de un carácter escalado (necesario para ajustar Y) // 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 // Posición X del borde izquierdo del carácter actual
// (se ajustará +char_width/2 para obtener el centro al renderizar) // (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) // Iterar sobre cada byte del string (con detecció UTF-8)
for (size_t i = 0; i < text.length(); i++) { 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) // Detectar copyright UTF-8 (0xC2 0xA9)
if (c == 0xC2 && i + 1 < text.length() && if (c == 0xC2 && i + 1 < text.length() &&
static_cast<unsigned char>(text[i + 1]) == 0xA9) { 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 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); auto it = chars_.find(c);
if (it != chars_.end()) { if (it != chars_.end()) {
// Renderizar carácter // 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) // (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)}; 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, escala, true, 1.0F, brightness); Rendering::render_shape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness);
// Avanzar posición // Avanzar posición
current_x += char_width_scaled + spacing_scaled; 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 // Calcular dimensions del text
float text_width = get_text_width(text, escala, spacing); float text_width = get_text_width(text, scale, spacing);
float text_height = get_text_height(escala); float text_height = get_text_height(scale);
// Calcular posició de l'esquina superior esquerra // Calcular posición de l'esquina superior izquierda
// restant la meitat de les dimensions del punt central // restant la meitat de las dimensions del point central
Punt posicio_esquerra = { Vec2 posicio_esquerra = {
.x = centre_punt.x - (text_width / 2.0F), .x = centre_punt.x - (text_width / 2.0F),
.y = centre_punt.y - (text_height / 2.0F)}; .y = centre_punt.y - (text_height / 2.0F)};
// Delegar al mètode render() existent // Delegar al método render() existent
render(text, posicio_esquerra, escala, spacing, brightness); 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()) { if (text.empty()) {
return 0.0F; return 0.0F;
} }
const float char_width_scaled = char_width * escala; const float char_width_scaled = char_width * scale;
const float spacing_scaled = spacing * escala; const float spacing_scaled = spacing * scale;
// Contar caracteres visuals (no bytes) - manejar UTF-8 // Contar caracteres visuals (no bytes) - manejar UTF-8
size_t visual_chars = 0; 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); return (visual_chars * char_width_scaled) + ((visual_chars - 1) * spacing_scaled);
} }
float VectorText::get_text_height(float escala) const { float VectorText::get_text_height(float scale) const {
return char_height * escala; return char_height * scale;
} }
} // namespace Graphics } // namespace Graphics
+16 -14
View File
@@ -1,8 +1,10 @@
// vector_text.hpp - Sistema de texto vectorial con display de 7-segmentos // 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 #pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <memory> #include <memory>
@@ -16,36 +18,36 @@ namespace Graphics {
class VectorText { class VectorText {
public: public:
VectorText(SDL_Renderer* renderer); VectorText(Rendering::Renderer* renderer);
// Renderizar string completo // Renderizar string completo
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':', // - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
// '!', '?', ' ') // '!', '?', ' ')
// - posicio: posición inicial (esquina superior izquierda) // - position: posición inicial (esquina superior izquierda)
// - escala: factor de escala (1.0 = 20×40 px por carácter) // - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a escala 1.0) // - spacing: espacio entre caracteres en píxeles (a scale 1.0)
// - brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor) // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
void render(const std::string& text, const Punt& posicio, float escala = 1.0F, float spacing = 2.0F, float brightness = 1.0F) const; 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 // Renderizar string centrado en un punto
// - text: cadena a renderizar // - text: cadena a renderizar
// - centre_punt: punto central del texto (no esquina superior izquierda) // - centre_punt: punto central del texto (no esquina superior izquierda)
// - escala: factor de escala (1.0 = 20×40 px por carácter) // - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a escala 1.0) // - spacing: espacio entre caracteres en píxeles (a scale 1.0)
// - brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor) // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
void render_centered(const std::string& text, const Punt& centre_punt, float escala = 1.0F, float spacing = 2.0F, float brightness = 1.0F) const; 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) // 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) // 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 // Verificar si un carácter está soportado
[[nodiscard]] bool is_supported(char c) const; [[nodiscard]] bool is_supported(char c) const;
private: private:
SDL_Renderer* renderer_; Rendering::Renderer* renderer_;
std::unordered_map<char, std::shared_ptr<Shape>> chars_; std::unordered_map<char, std::shared_ptr<Shape>> chars_;
void load_charset(); void load_charset();
+8 -8
View File
@@ -30,7 +30,7 @@ Input::Input(std::string game_controller_db_path)
// Inicializar bindings del teclado (valores por defecto) // Inicializar bindings del teclado (valores por defecto)
// Estos serán sobrescritos por applyPlayer1BindingsFromOptions() // Estos serán sobrescritos por applyPlayer1BindingsFromOptions()
keyboard_.bindings = { keyboard_.bindings = {
// Movimiento del jugador // Movimiento del player
{Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}}, {Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}},
{Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}}, {Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}},
{Action::THRUST, KeyState{.scancode = SDL_SCANCODE_UP}}, {Action::THRUST, KeyState{.scancode = SDL_SCANCODE_UP}},
@@ -188,7 +188,7 @@ auto Input::checkAnyButton(bool repeat) -> bool {
return false; 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 { auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
for (const auto& action : actions) { for (const auto& action : actions) {
if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) { if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
@@ -388,14 +388,14 @@ void Input::update() {
binding.second.is_held = key_is_down_now; binding.second.is_held = key_is_down_now;
} }
// Actualizar bindings de jugador 1 // Actualizar bindings de player 1
for (auto& binding : player1_keyboard_bindings_) { for (auto& binding : player1_keyboard_bindings_) {
bool key_is_down_now = key_states[binding.second.scancode]; bool key_is_down_now = key_states[binding.second.scancode];
binding.second.just_pressed = key_is_down_now && !binding.second.is_held; binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
binding.second.is_held = key_is_down_now; binding.second.is_held = key_is_down_now;
} }
// Actualizar bindings de jugador 2 // Actualizar bindings de player 2
for (auto& binding : player2_keyboard_bindings_) { for (auto& binding : player2_keyboard_bindings_) {
bool key_is_down_now = key_states[binding.second.scancode]; bool key_is_down_now = key_states[binding.second.scancode];
binding.second.just_pressed = key_is_down_now && !binding.second.is_held; 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) ========== // ========== 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() { void Input::applyPlayer1BindingsFromOptions() {
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico) // 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
player1_keyboard_bindings_[Action::LEFT].scancode = Options::player1.keyboard.key_left; player1_keyboard_bindings_[Action::LEFT].scancode = Options::player1.keyboard.key_left;
@@ -527,7 +527,7 @@ void Input::applyPlayer1BindingsFromOptions() {
player1_gamepad_ = gamepad; player1_gamepad_ = gamepad;
} }
// Aplica configuración de controles del jugador 2 // Aplica configuración de controles del player 2
void Input::applyPlayer2BindingsFromOptions() { void Input::applyPlayer2BindingsFromOptions() {
// 1. Aplicar bindings de teclado (mapa específico de P2, no sobrescribe P1) // 1. Aplicar bindings de teclado (mapa específico de P2, no sobrescribe P1)
player2_keyboard_bindings_[Action::LEFT].scancode = Options::player2.keyboard.key_left; player2_keyboard_bindings_[Action::LEFT].scancode = Options::player2.keyboard.key_left;
@@ -561,7 +561,7 @@ void Input::applyPlayer2BindingsFromOptions() {
player2_gamepad_ = gamepad; player2_gamepad_ = gamepad;
} }
// Consulta de input para jugador 1 // Consulta de input para player 1
auto Input::checkActionPlayer1(Action action, bool repeat) -> bool { auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
// Comprobar teclado con el mapa específico de P1 // Comprobar teclado con el mapa específico de P1
bool keyboard_active = false; bool keyboard_active = false;
@@ -583,7 +583,7 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
return keyboard_active || gamepad_active; 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 { auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
// Comprobar teclado con el mapa específico de P2 // Comprobar teclado con el mapa específico de P2
bool keyboard_active = false; bool keyboard_active = false;
+5 -5
View File
@@ -58,7 +58,7 @@ class Input {
name(std::string(SDL_GetGamepadName(gamepad))), name(std::string(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))), path(std::string(SDL_GetGamepadPath(pad))),
bindings{ bindings{
// Movimiento y acciones del jugador // Movimiento y acciones del player
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}}, {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::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}, {Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
@@ -92,7 +92,7 @@ class Input {
void applyKeyboardBindingsFromOptions(); void applyKeyboardBindingsFromOptions();
void applyGamepadBindingsFromOptions(); void applyGamepadBindingsFromOptions();
// Configuración por jugador (Orni - dos jugadores) // Configuración por player (Orni - dos jugadores)
void applyPlayer1BindingsFromOptions(); void applyPlayer1BindingsFromOptions();
void applyPlayer2BindingsFromOptions(); void applyPlayer2BindingsFromOptions();
@@ -105,7 +105,7 @@ class Input {
auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool; auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
void resetInputStates(); void resetInputStates();
// Consulta por jugador (Orni - dos jugadores) // Consulta por player (Orni - dos jugadores)
auto checkActionPlayer1(Action action, bool repeat = true) -> bool; auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
auto checkActionPlayer2(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) Keyboard keyboard_{}; // Estado del teclado (solo acciones globales)
std::string gamepad_mappings_file_; // Ruta al archivo de mappings 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> player1_gamepad_;
std::shared_ptr<Gamepad> player2_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> player1_keyboard_bindings_;
std::unordered_map<Action, KeyState> player2_keyboard_bindings_; std::unordered_map<Action, KeyState> player2_keyboard_bindings_;
}; };
+1 -1
View File
@@ -13,7 +13,7 @@ enum class InputAction : int { // Acciones de entrada posibles en el juego
RIGHT, // Rotar derecha RIGHT, // Rotar derecha
THRUST, // Acelerar THRUST, // Acelerar
SHOOT, // Disparar SHOOT, // Disparar
START, // Empezar partida START, // Empezar match
// Inputs de sistema (globales) // Inputs de sistema (globales)
WINDOW_INC_ZOOM, // F2 WINDOW_INC_ZOOM, // F2
+9 -9
View File
@@ -12,19 +12,19 @@ bool cursor_visible = false; // Estado del cursor (inicia ocult)
// SDLManager controla esto mediante llamadas a setForceHidden(). // SDLManager controla esto mediante llamadas a setForceHidden().
bool force_hidden = false; 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; Uint32 initialization_time = 0;
constexpr Uint32 IGNORE_MOTION_DURATION = 1000; // Ignorar primers 1000ms constexpr Uint32 IGNORE_MOTION_DURATION = 1000; // Ignorar primers 1000ms
void forceHide() { void forceHide() {
// Forçar ocultació sincronitzant estat SDL i estat intern // Forçar ocultació sincronitzant state SDL i state intern
std::cout << "[Mouse::forceHide] Ocultant cursor i sincronitzant estat. cursor_visible=" << cursor_visible std::cout << "[Mouse::forceHide] Ocultant cursor i sincronitzant state. cursor_visible=" << cursor_visible
<< " -> false" << '\n'; << " -> false" << '\n';
SDL_HideCursor(); SDL_HideCursor();
cursor_visible = false; cursor_visible = false;
last_mouse_move_time = 0; last_mouse_move_time = 0;
initialization_time = SDL_GetTicks(); // Marcar temps per ignorar esdeveniments inicials initialization_time = SDL_GetTicks(); // Marcar time per ignorar esdeveniments inicials
std::cout << "[Mouse::forceHide] Ignorant moviments durant " << IGNORE_MOTION_DURATION << "ms" << '\n'; std::cout << "[Mouse::forceHide] Ignorant moviments durante " << IGNORE_MOTION_DURATION << "ms" << '\n';
} }
void setForceHidden(bool force) { void setForceHidden(bool force) {
@@ -56,16 +56,16 @@ void handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_MOUSE_MOTION) { if (event.type == SDL_EVENT_MOUSE_MOTION) {
Uint32 current_time = SDL_GetTicks(); 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)) { if (initialization_time > 0 && (current_time - initialization_time < IGNORE_MOTION_DURATION)) {
std::cout << "[Mouse::handleEvent] Ignorant moviment fantasma de SDL. time=" << current_time std::cout << "[Mouse::handleEvent] Ignorant movement fantasma de SDL. time=" << current_time
<< " (inicialització fa " << (current_time - initialization_time) << "ms)" << '\n'; << " (inicialización hace " << (current_time - initialization_time) << "ms)" << '\n';
return; return;
} }
last_mouse_move_time = current_time; last_mouse_move_time = current_time;
if (!cursor_visible) { 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(); SDL_ShowCursor();
cursor_visible = true; cursor_visible = true;
} }
+1 -1
View File
@@ -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 Uint32 last_mouse_move_time; // Última vez que el ratón se movió
extern bool cursor_visible; // Estado del cursor 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 handleEvent(const SDL_Event& event);
void updateCursorVisibility(); void updateCursorVisibility();
+5 -5
View File
@@ -1,4 +1,4 @@
// easing.hpp - Funcions d'interpolació i easing // easing.hpp - Funciones de interpolació i easing
// © 2025 Orni Attack // © 2025 Orni Attack
#pragma once #pragma once
@@ -7,21 +7,21 @@ namespace Easing {
// Ease-out quadratic: empieza rápido, desacelera suavemente // Ease-out quadratic: empieza rápido, desacelera suavemente
// t = progreso normalizado [0.0 - 1.0] // 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) { inline float ease_out_quad(float t) {
return 1.0F - ((1.0F - t) * (1.0F - t)); return 1.0F - ((1.0F - t) * (1.0F - t));
} }
// Ease-in quadratic: empieza lento, acelera // Ease-in quadratic: empieza lento, acelera
// t = progreso normalizado [0.0 - 1.0] // 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) { inline float ease_in_quad(float t) {
return t * t; return t * t;
} }
// Ease-in-out quadratic: acelera al inicio, desacelera al final // Ease-in-out quadratic: acelera al inicio, desacelera al final
// t = progreso normalizado [0.0 - 1.0] // 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) { inline float ease_in_out_quad(float t) {
return (t < 0.5F) return (t < 0.5F)
? 2.0F * t * t ? 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 // Ease-out cubic: desaceleración más suave que quadratic
// t = progreso normalizado [0.0 - 1.0] // 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) { inline float ease_out_cubic(float t) {
float t1 = 1.0F - t; float t1 = 1.0F - t;
return 1.0F - (t1 * t1 * t1); return 1.0F - (t1 * t1 * t1);
+10 -10
View File
@@ -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 // © 2025 Orni Attack - Sistema de física
#pragma once #pragma once
#include "core/entities/entitat.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
namespace Physics { namespace Physics {
// Comprovació genèrica de col·lisió entre dues entitats // Comprobación genèrica de colisión entre dues entidades
inline bool check_collision(const Entities::Entitat& a, const Entities::Entitat& b, float amplifier = 1.0F) { inline bool check_collision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) {
// Comprovar si ambdós són col·lisionables // Comprovar si ambdós són col·lisionables
if (!a.es_collidable() || !b.es_collidable()) { if (!a.isCollidable() || !b.isCollidable()) {
return false; return false;
} }
// Calcular radi combinat (amb amplificador per hitbox generós) // Calcular radi combinat (con amplificador per hitbox generós)
float suma_radis = (a.get_collision_radius() + b.get_collision_radius()) * amplifier; float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier;
float suma_radis_sq = suma_radis * suma_radis; float suma_radis_sq = suma_radis * suma_radis;
// Comprovació distància al quadrat (sense sqrt) // Comprobación distancia al cuadrado (sin sqrt)
const Punt& pos_a = a.get_centre(); const Vec2& pos_a = a.getCenter();
const Punt& pos_b = b.get_centre(); const Vec2& pos_b = b.getCenter();
float dx = pos_a.x - pos_b.x; float dx = pos_a.x - pos_b.x;
float dy = pos_a.y - pos_b.y; float dy = pos_a.y - pos_b.y;
float dist_sq = (dx * dx) + (dy * dy); float dist_sq = (dx * dx) + (dy * dy);
+178
View File
@@ -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
+64
View File
@@ -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
+75
View File
@@ -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 // coordinate_transform.cpp - Inicialización de variables globals
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#include "core/rendering/coordinate_transform.hpp" #include "core/rendering/coordinate_transform.hpp"
namespace Rendering { 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; float g_current_scale_factor = 1.0F;
} // namespace Rendering } // namespace Rendering
@@ -1,5 +1,5 @@
// coordinate_transform.hpp - Transformació de coordenades lògiques a físiques // 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 #pragma once
@@ -7,10 +7,10 @@
namespace Rendering { 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; 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) { inline int transform_x(int logical_x, float scale) {
return static_cast<int>(std::round(logical_x * 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)); 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) { inline int transform_x(int logical_x) {
return transform_x(logical_x, g_current_scale_factor); return transform_x(logical_x, g_current_scale_factor);
} }
+105
View File
@@ -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
+59
View File
@@ -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
+40 -87
View File
@@ -1,102 +1,55 @@
// line_renderer.cpp - Implementació de renderitzat de línies // line_renderer.cpp - Implementación de renderizado de líneas (SDL3 GPU)
// © 1999 Visente i Sergi (versió Pascal) // © 1999 Visente i Sergi (versión Pascal)
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
#include <cmath>
#include "core/rendering/coordinate_transform.hpp"
namespace Rendering { namespace Rendering {
// [NUEVO] Color global compartit (actualitzat per ColorOscillator via // Color global compartido para líneas sin paleta propia (HUD, debug, texto
// SDLManager) // genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
SDL_Color g_current_line_color = {255, 255, 255, 255}; // Blanc inicial // 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) { // Grosor global por defecto. Configurable via setLineThickness.
// Algorisme de Bresenham per dibuixar línies // 1.5 da una línea visible y crujiente; 1.0 se ve demasiado fino en pantallas grandes.
// Basat en el codi Pascal original float g_current_line_thickness = 1.5F;
// Helper function: retorna el signe d'un nombre void linea(Renderer* renderer,
auto sign = [](int x) -> int { int x1, int y1, int x2, int y2,
if (x < 0) { float brightness,
return -1; float thickness,
} SDL_Color color) {
if (x > 0) { if (renderer == nullptr) {
return 1; return;
}
return 0;
};
// Variables per a l'algorisme (no utilitzades fins Fase 10 - detecció de
// col·lisions) int x = x1, y = y1; int xs = x2 - x1; int ys = y2 - y1; int
// xm = sign(xs); int ym = sign(ys); xs = std::abs(xs); ys = std::abs(ys);
// Suprimir warning de variable no usada
(void)sign;
// Detecció de col·lisió (TODO per Fase 10)
// El codi Pascal original llegia pixels del framebuffer bit-packed
// i comptava col·lisions. Per ara, usem SDL_RenderDrawLine i retornem false.
bool colisio = false;
// Dibuixar amb SDL3 (més eficient que Bresenham píxel a píxel)
if (dibuixar && (renderer != nullptr)) {
// Transformar coordenades lògiques (640x480) a físiques (resolució real)
float scale = g_current_scale_factor;
int px1 = transform_x(x1, scale);
int py1 = transform_y(y1, scale);
int px2 = transform_x(x2, scale);
int py2 = transform_y(y2, scale);
// Aplicar brightness al color oscil·lat global
SDL_Color color_final;
color_final.r = static_cast<uint8_t>(g_current_line_color.r * brightness);
color_final.g = static_cast<uint8_t>(g_current_line_color.g * brightness);
color_final.b = static_cast<uint8_t>(g_current_line_color.b * brightness);
color_final.a = 255;
SDL_SetRenderDrawColor(renderer, color_final.r, color_final.g, color_final.b, 255);
// Renderitzar amb coordenades físiques
SDL_RenderLine(renderer, static_cast<float>(px1), static_cast<float>(py1), static_cast<float>(px2), static_cast<float>(py2));
} }
// Algorisme de Bresenham original (conservat per a futura detecció de // Coords lógicas (1280×720). El shader hace el mapeo a NDC; el viewport
// col·lisió) // del SDLManager hace el letterbox a píxeles físicos.
/* const float FX1 = static_cast<float>(x1);
if (xs > ys) { const float FY1 = static_cast<float>(y1);
// Línia plana (<45 graus) const float FX2 = static_cast<float>(x2);
int count = -(xs / 2); const float FY2 = static_cast<float>(y2);
while (x != x2) {
count = count + ys;
x = x + xm;
if (count > 0) {
y = y + ym;
count = count - xs;
}
// Aquí aniria la detecció de col·lisió píxel a píxel
}
} else {
// Línia pronunciada (>=45 graus)
int count = -(ys / 2);
while (y != y2) {
count = count + xs;
y = y + ym;
if (count > 0) {
x = x + xm;
count = count - ys;
}
// Aquí aniria la detecció de col·lisió píxel a píxel
}
}
*/
return colisio; // 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 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 } // namespace Rendering
+28 -8
View File
@@ -1,16 +1,36 @@
// line_renderer.hpp - Renderitzat de línies // line_renderer.hpp - Renderizado de líneas vectoriales (SDL3 GPU)
// © 1999 Visente i Sergi (versió Pascal) // © 1999 Visente i Sergi (versión Pascal)
// © 2025 Port a C++20 amb SDL3 // © 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 #pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
namespace Rendering { 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); 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 } // 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
-66
View File
@@ -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;
}
-32
View File
@@ -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);
+22
View File
@@ -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
+76 -220
View File
@@ -1,12 +1,15 @@
// sdl_manager.cpp - Implementació del gestor SDL3 // 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 "sdl_manager.hpp"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <cstdint>
#include <format> #include <format>
#include <iostream> #include <iostream>
#include "core/config/postfx_config.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/rendering/coordinate_transform.hpp" #include "core/rendering/coordinate_transform.hpp"
@@ -14,12 +17,53 @@
#include "game/options.hpp" #include "game/options.hpp"
#include "project.h" #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() SDLManager::SDLManager()
: finestra_(nullptr), : finestra_(nullptr),
renderer_(nullptr),
fps_accumulator_(0.0F),
fps_frame_count_(0),
fps_display_(0),
current_width_(Defaults::Window::WIDTH), current_width_(Defaults::Window::WIDTH),
current_height_(Defaults::Window::HEIGHT), current_height_(Defaults::Window::HEIGHT),
is_fullscreen_(false), is_fullscreen_(false),
@@ -29,61 +73,27 @@ SDLManager::SDLManager()
windowed_width_(Defaults::Window::WIDTH), windowed_width_(Defaults::Window::WIDTH),
windowed_height_(Defaults::Window::HEIGHT), windowed_height_(Defaults::Window::HEIGHT),
max_zoom_(1.0F) { max_zoom_(1.0F) {
// Inicialitzar SDL3
if (!SDL_Init(SDL_INIT_VIDEO)) { if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n'; std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n';
return; return;
} }
// Calcular mida màxima des del display
calculateMaxWindowSize(); calculateMaxWindowSize();
// Construir títol dinàmic if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, false)) {
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';
SDL_Quit(); SDL_Quit();
return; return;
} }
// IMPORTANT: Centrar explícitament la finestra updateViewport();
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();
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_ std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
<< " (logic: " << Defaults::Game::WIDTH << "x" << " (logic: " << Defaults::Game::WIDTH << "x"
<< Defaults::Game::HEIGHT << ")" << '\n'; << Defaults::Game::HEIGHT << ")" << '\n';
} }
// Constructor amb configuració
SDLManager::SDLManager(int width, int height, bool fullscreen) SDLManager::SDLManager(int width, int height, bool fullscreen)
: finestra_(nullptr), : finestra_(nullptr),
renderer_(nullptr),
fps_accumulator_(0.0F),
fps_frame_count_(0),
fps_display_(0),
current_width_(width), current_width_(width),
current_height_(height), current_height_(height),
is_fullscreen_(fullscreen), is_fullscreen_(fullscreen),
@@ -93,56 +103,21 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
windowed_width_(width), windowed_width_(width),
windowed_height_(height), windowed_height_(height),
max_zoom_(1.0F) { max_zoom_(1.0F) {
// Inicialitzar SDL3
if (!SDL_Init(SDL_INIT_VIDEO)) { if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n'; std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n';
return; return;
} }
// Calcular mida màxima des del display
calculateMaxWindowSize(); calculateMaxWindowSize();
// Construir títol dinàmic if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, is_fullscreen_)) {
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';
SDL_Quit(); SDL_Quit();
return; return;
} }
// Centrar explícitament la finestra (si no és fullscreen) updateViewport();
if (!is_fullscreen_) {
SDL_SetWindowPosition(finestra_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Crear renderer amb acceleració // En fullscreen: forzar ocultació permanent del cursor.
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
if (is_fullscreen_) { if (is_fullscreen_) {
Mouse::setForceHidden(true); Mouse::setForceHidden(true);
} }
@@ -157,10 +132,7 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
} }
SDLManager::~SDLManager() { SDLManager::~SDLManager() {
if (renderer_ != nullptr) { gpu_renderer_.destroy();
SDL_DestroyRenderer(renderer_);
renderer_ = nullptr;
}
if (finestra_ != nullptr) { if (finestra_ != nullptr) {
SDL_DestroyWindow(finestra_); SDL_DestroyWindow(finestra_);
@@ -176,36 +148,27 @@ void SDLManager::calculateMaxWindowSize() {
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display); const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
if (mode != nullptr) { 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_width_ = mode->w - 100;
max_height_ = mode->h - 100; max_height_ = mode->h - 100;
std::cout << "Display detectat: " << mode->w << "x" << mode->h std::cout << "Display detectat: " << mode->w << "x" << mode->h
<< " (max finestra: " << max_width_ << "x" << max_height_ << ")" << " (max finestra: " << max_width_ << "x" << max_height_ << ")"
<< '\n'; << '\n';
} else { } else {
// Fallback conservador
max_width_ = 1920; max_width_ = 1920;
max_height_ = 1080; max_height_ = 1080;
std::cerr << "No s'ha pogut detectar el display, usant fallback: " std::cerr << "No s'ha pogut detectar el display, usant fallback: "
<< max_width_ << "x" << max_height_ << '\n'; << max_width_ << "x" << max_height_ << '\n';
} }
// Calculate max zoom immediately after determining max size
calculateMaxZoom(); calculateMaxZoom();
} }
void SDLManager::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_width = static_cast<float>(max_width_) / Defaults::Window::WIDTH;
float max_zoom_height = static_cast<float>(max_height_) / Defaults::Window::HEIGHT; 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); 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; 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); max_zoom_ = std::max(max_zoom_, Defaults::Window::MIN_ZOOM);
std::cout << "Max zoom: " << max_zoom_ << "x (display: " std::cout << "Max zoom: " << max_zoom_ << "x (display: "
@@ -213,37 +176,25 @@ void SDLManager::calculateMaxZoom() {
} }
void SDLManager::applyZoom(float new_zoom) { void SDLManager::applyZoom(float new_zoom) {
// Clamp to valid range
new_zoom = std::max(Defaults::Window::MIN_ZOOM, new_zoom = std::max(Defaults::Window::MIN_ZOOM,
std::min(new_zoom, max_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; 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) { if (std::abs(new_zoom - zoom_factor_) < 0.01F) {
return; return;
} }
zoom_factor_ = new_zoom; 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_width = static_cast<int>(std::round( int new_height = static_cast<int>(std::round(Defaults::Window::HEIGHT * zoom_factor_));
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); applyWindowSize(new_width, new_height);
// Update viewport for new zoom
updateViewport(); updateViewport();
// Update windowed size cache
windowed_width_ = new_width; windowed_width_ = new_width;
windowed_height_ = new_height; windowed_height_ = new_height;
// Persist
Options::window.width = new_width; Options::window.width = new_width;
Options::window.height = new_height; Options::window.height = new_height;
Options::window.zoom_factor = zoom_factor_; Options::window.zoom_factor = zoom_factor_;
@@ -252,30 +203,23 @@ void SDLManager::applyZoom(float new_zoom) {
<< new_width << "x" << new_height << ")" << '\n'; << 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() { 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_; float scale = zoom_factor_;
int scaled_width = static_cast<int>(std::round(Defaults::Game::WIDTH * scale)); int scaled_width = static_cast<int>(std::round(Defaults::Game::WIDTH * scale));
int scaled_height = static_cast<int>(std::round(Defaults::Game::HEIGHT * 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_x = (current_width_ - scaled_width) / 2;
int offset_y = (current_height_ - scaled_height) / 2; int offset_y = (current_height_ - scaled_height) / 2;
// Evitar offsets negatius
offset_x = std::max(offset_x, 0); offset_x = std::max(offset_x, 0);
offset_y = std::max(offset_y, 0); offset_y = std::max(offset_y, 0);
// Configurar viewport per al renderitzat gpu_renderer_.setViewport(static_cast<float>(offset_x),
SDL_Rect viewport = {offset_x, offset_y, scaled_width, scaled_height}; static_cast<float>(offset_y),
SDL_SetRenderViewport(renderer_, &viewport); static_cast<float>(scaled_width),
static_cast<float>(scaled_height));
std::cout << "Viewport: " << scaled_width << "x" << scaled_height std::cout << "Viewport: " << scaled_width << "x" << scaled_height
<< " @ (" << offset_x << "," << offset_y << ") [scale=" << scale << "]" << " @ (" << offset_x << "," << offset_y << ") [scale=" << scale << "]"
@@ -283,7 +227,6 @@ void SDLManager::updateViewport() {
} }
void SDLManager::updateRenderingContext() const { void SDLManager::updateRenderingContext() const {
// Actualitzar el factor d'escala global per a totes les funcions de renderitzat
Rendering::g_current_scale_factor = zoom_factor_; Rendering::g_current_scale_factor = zoom_factor_;
} }
@@ -291,10 +234,8 @@ void SDLManager::increaseWindowSize() {
if (is_fullscreen_) { if (is_fullscreen_) {
return; return;
} }
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT; float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
applyZoom(new_zoom); applyZoom(new_zoom);
std::cout << "F2: Zoom aumentat a " << zoom_factor_ << "x" << '\n'; std::cout << "F2: Zoom aumentat a " << zoom_factor_ << "x" << '\n';
} }
@@ -302,15 +243,12 @@ void SDLManager::decreaseWindowSize() {
if (is_fullscreen_) { if (is_fullscreen_) {
return; return;
} }
float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT; float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
applyZoom(new_zoom); applyZoom(new_zoom);
std::cout << "F1: Zoom reduït a " << zoom_factor_ << "x" << '\n'; std::cout << "F1: Zoom reduït a " << zoom_factor_ << "x" << '\n';
} }
void SDLManager::applyWindowSize(int new_width, int new_height) { void SDLManager::applyWindowSize(int new_width, int new_height) {
// Obtenir posició actual ABANS del resize
int old_x; int old_x;
int old_y; int old_y;
SDL_GetWindowPosition(finestra_, &old_x, &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_width = current_width_;
int old_height = current_height_; int old_height = current_height_;
// Actualitzar mida
SDL_SetWindowSize(finestra_, new_width, new_height); SDL_SetWindowSize(finestra_, new_width, new_height);
current_width_ = new_width; current_width_ = new_width;
current_height_ = new_height; 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_width = old_width - new_width;
int delta_height = old_height - new_height; int delta_height = old_height - new_height;
int new_x = old_x + (delta_width / 2); int new_x = old_x + (delta_width / 2);
int new_y = old_y + (delta_height / 2); int new_y = old_y + (delta_height / 2);
// Evitar que la finestra surti de la pantalla constexpr int TITLEBAR_HEIGHT = 35;
constexpr int TITLEBAR_HEIGHT = 35; // Alçada aproximada de la barra de títol
new_x = std::max(new_x, 0); new_x = std::max(new_x, 0);
new_y = std::max(new_y, TITLEBAR_HEIGHT); new_y = std::max(new_y, TITLEBAR_HEIGHT);
SDL_SetWindowPosition(finestra_, new_x, new_y); SDL_SetWindowPosition(finestra_, new_x, new_y);
// Actualitzar viewport després del resize
updateViewport(); updateViewport();
} }
void SDLManager::toggleFullscreen() { void SDLManager::toggleFullscreen() {
if (!is_fullscreen_) { if (!is_fullscreen_) {
// ENTERING FULLSCREEN
windowed_width_ = current_width_; windowed_width_ = current_width_;
windowed_height_ = current_height_; windowed_height_ = current_height_;
is_fullscreen_ = true; is_fullscreen_ = true;
SDL_SetWindowFullscreen(finestra_, true); SDL_SetWindowFullscreen(finestra_, true);
std::cout << "F3: Fullscreen activat (guardada: " std::cout << "F3: Fullscreen activat (guardada: "
<< windowed_width_ << "x" << windowed_height_ << ")" << '\n'; << windowed_width_ << "x" << windowed_height_ << ")" << '\n';
} else { } else {
// EXITING FULLSCREEN
is_fullscreen_ = false; is_fullscreen_ = false;
SDL_SetWindowFullscreen(finestra_, false); SDL_SetWindowFullscreen(finestra_, false);
// CRITICAL: Explicitly restore windowed size
applyWindowSize(windowed_width_, windowed_height_); applyWindowSize(windowed_width_, windowed_height_);
std::cout << "F3: Fullscreen desactivat (restaurada: " std::cout << "F3: Fullscreen desactivat (restaurada: "
<< windowed_width_ << "x" << windowed_height_ << ")" << '\n'; << windowed_width_ << "x" << windowed_height_ << ")" << '\n';
} }
Options::window.fullscreen = is_fullscreen_; 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_); 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) { if (event.type == SDL_EVENT_WINDOW_RESIZED) {
SDL_GetWindowSize(finestra_, &current_width_, &current_height_); SDL_GetWindowSize(finestra_, &current_width_, &current_height_);
// Calculate zoom from actual size (may not align to 0.1 increments)
float new_zoom = static_cast<float>(current_width_) / Defaults::Window::WIDTH; float new_zoom = static_cast<float>(current_width_) / Defaults::Window::WIDTH;
zoom_factor_ = std::max(Defaults::Window::MIN_ZOOM, zoom_factor_ = std::max(Defaults::Window::MIN_ZOOM,
std::min(new_zoom, max_zoom_)); std::min(new_zoom, max_zoom_));
// Update windowed cache (if not in fullscreen)
if (!is_fullscreen_) { if (!is_fullscreen_) {
windowed_width_ = current_width_; windowed_width_ = current_width_;
windowed_height_ = current_height_; windowed_height_ = current_height_;
} }
// Actualitzar viewport després del resize manual
updateViewport(); updateViewport();
std::cout << "Finestra redimensionada: " << current_width_ std::cout << "Finestra redimensionada: " << current_width_
@@ -398,84 +316,22 @@ bool SDLManager::handleWindowEvent(const SDL_Event& event) {
return false; return false;
} }
void SDLManager::neteja(uint8_t r, uint8_t g, uint8_t b) { void SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) {
if (renderer_ == nullptr) { // El fondo lo dibuja ahora el shader de postpro (background pulse). El
return; // offscreen se limpia en negro dentro de beginFrame. Los argumentos r/g/b
} // se mantienen por compatibilidad de API.
// [MODIFICAT] Usar color oscil·lat del fons en lloc dels paràmetres
(void)r; (void)r;
(void)g; (void)g;
(void)b; // Suprimir warnings (void)b;
SDL_Color bg = color_oscillator_.getCurrentBackgroundColor(); gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F);
SDL_SetRenderDrawColor(renderer_, bg.r, bg.g, bg.b, 255);
SDL_RenderClear(renderer_);
} }
void SDLManager::presenta() { void SDLManager::present() {
if (renderer_ == nullptr) { gpu_renderer_.endFrame();
return;
}
SDL_RenderPresent(renderer_);
} }
// [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() { void SDLManager::toggleVSync() {
// Toggle: 1 → 0 → 1
Options::rendering.vsync = (Options::rendering.vsync == 1) ? 0 : 1; Options::rendering.vsync = (Options::rendering.vsync == 1) ? 0 : 1;
gpu_renderer_.setVSync(Options::rendering.vsync != 0);
// 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ó
Options::saveToFile(); Options::saveToFile();
} }
+21 -37
View File
@@ -1,61 +1,50 @@
// sdl_manager.hpp - Gestor d'inicialització de SDL3 // sdl_manager.hpp - Gestor de inicialización de SDL3
// © 2025 Port a C++20 amb 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 #pragma once
#define SDL_MANAGER_HPP
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint> #include <cstdint>
#include <string>
#include "core/rendering/color_oscillator.hpp" #include "core/rendering/render_context.hpp"
class SDLManager { class SDLManager {
public: public:
SDLManager(); // Constructor per defecte (usa Defaults::) 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(); ~SDLManager();
// No permetre còpia ni assignació // No permetre còpia ni assignació
SDLManager(const SDLManager&) = delete; SDLManager(const SDLManager&) = delete;
SDLManager& operator=(const SDLManager&) = delete; auto operator=(const SDLManager&) -> SDLManager& = delete;
// [NUEVO] Gestió de finestra dinàmica // [NUEVO] Gestió de finestra dinàmica
void increaseWindowSize(); // F2: +100px void increaseWindowSize(); // F2: +100px
void decreaseWindowSize(); // F1: -100px void decreaseWindowSize(); // F1: -100px
void toggleFullscreen(); // F3 void toggleFullscreen(); // F3
void toggleVSync(); // F4 void toggleVSync(); // F4
bool 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) // Funciones principals (renderizado)
void neteja(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0); void clear(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);
void presenta(); void present();
// [NUEVO] Actualització de colors (oscil·lació)
void updateColors(float delta_time);
// [NUEVO] Actualitzar comptador de FPS
void updateFPS(float delta_time);
// Getters // Getters
SDL_Renderer* obte_renderer() { return renderer_; } auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
[[nodiscard]] float getScaleFactor() const { return zoom_factor_; } [[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; }
// [NUEVO] Actualitzar títol de la finestra // [NUEVO] Actualitzar context de renderizado (factor de scale global)
void setWindowTitle(const std::string& title);
// [NUEVO] Actualitzar context de renderitzat (factor d'escala global)
void updateRenderingContext() const; void updateRenderingContext() const;
private: private:
SDL_Window* finestra_; SDL_Window* finestra_;
SDL_Renderer* renderer_; Rendering::Renderer gpu_renderer_; // GpuFrameRenderer (SDL3 GPU)
// [NUEVO] Variables FPS
float fps_accumulator_;
int fps_frame_count_;
int fps_display_;
// [NUEVO] Estat de la finestra // [NUEVO] Estat de la finestra
int current_width_; // Mida física actual int current_width_; // Mida física actual
@@ -70,16 +59,11 @@ class SDLManager {
int windowed_height_; // Saved size before fullscreen int windowed_height_; // Saved size before fullscreen
float max_zoom_; // Maximum zoom (calculated from display) float max_zoom_; // Maximum zoom (calculated from display)
// [NUEVO] Funcions internes // [NUEVO] Funciones internes
void calculateMaxWindowSize(); // Llegir resolució del display void calculateMaxWindowSize(); // Llegir resolució del display
void calculateMaxZoom(); // Calculate max zoom from display void calculateMaxZoom(); // Calculate max zoom from display
void applyZoom(float new_zoom); // Apply zoom and resize window void applyZoom(float new_zoom); // Apply zoom and resize window
void applyWindowSize(int width, int height); // Canviar mida + centrar void applyWindowSize(int width, int height); // Canviar mida + centrar
void updateLogicalPresentation(); // Actualitzar viewport void updateViewport(); // Configurar viewport con letterbox
void updateViewport(); // Configurar viewport amb letterbox
// [NUEVO] Oscil·lador de colors
Rendering::ColorOscillator color_oscillator_;
}; };
#endif // SDL_MANAGER_HPP
+40 -48
View File
@@ -1,5 +1,5 @@
// shape_renderer.cpp - Implementació del renderitzat de formes // shape_renderer.cpp - Implementació del renderizado de formes
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
@@ -10,108 +10,100 @@
namespace Rendering { namespace Rendering {
// Helper: aplicar rotació 3D a un punt 2D (assumeix Z=0) // Helper: aplicar rotación 3D a un point 2D (assumeix Z=0)
static Punt apply_3d_rotation(float x, float y, const Rotation3D& rot) { static Vec2 apply_3d_rotation(float x, float y, const Rotation3D& rot) {
float z = 0.0F; // Tots els punts 2D comencen a Z=0 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 cos_pitch = std::cos(rot.pitch);
float sin_pitch = std::sin(rot.pitch); float sin_pitch = std::sin(rot.pitch);
float y1 = (y * cos_pitch) - (z * sin_pitch); float y1 = (y * cos_pitch) - (z * sin_pitch);
float z1 = (y * sin_pitch) + (z * cos_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 cos_yaw = std::cos(rot.yaw);
float sin_yaw = std::sin(rot.yaw); float sin_yaw = std::sin(rot.yaw);
float x2 = (x * cos_yaw) + (z1 * sin_yaw); float x2 = (x * cos_yaw) + (z1 * sin_yaw);
float z2 = (-x * sin_yaw) + (z1 * cos_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 cos_roll = std::cos(rot.roll);
float sin_roll = std::sin(rot.roll); float sin_roll = std::sin(rot.roll);
float x3 = (x2 * cos_roll) - (y1 * sin_roll); float x3 = (x2 * cos_roll) - (y1 * sin_roll);
float y3 = (x2 * sin_roll) + (y1 * cos_roll); float y3 = (x2 * sin_roll) + (y1 * cos_roll);
// Proyecció perspectiva (Z-divide simple) // Proyecció perspectiva (Z-divide simple)
// Naus volen cap al punt de fuga (320, 240) a "infinit" (Z → +∞) // Naves quieren hacia el point de fuga (320, 240) a "infinit" (Z → +∞)
// Z més gran = més lluny = més petit a pantalla // Z més grande = més lluny = més pequeño a pantalla
constexpr float perspective_factor = 500.0F; constexpr float perspective_factor = 500.0F;
float scale_factor = perspective_factor / (perspective_factor + z2); float scale_factor = perspective_factor / (perspective_factor + z2);
return {.x = x3 * scale_factor, .y = y3 * scale_factor}; return {.x = x3 * scale_factor, .y = y3 * scale_factor};
} }
// Helper: transformar un punt amb rotació, escala i trasllació // Helper: transformar un point con rotación, scale i traslación
static Punt transform_point(const Punt& point, const Punt& shape_centre, const Punt& posicio, float angle, float escala, const Rotation3D* rotation_3d) { 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 punt respecte al centre de la forma // 1. Centrar el point respecte al centro de la shape
float centered_x = point.x - shape_centre.x; float centered_x = point.x - shape_centre.x;
float centered_y = point.y - shape_centre.y; 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()) { 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_x = rotated_3d.x;
centered_y = rotated_3d.y; centered_y = rotated_3d.y;
} }
// 3. Aplicar escala al punt (després de rotació 3D) // 3. Aplicar scale al point (después de rotación 3D)
float scaled_x = centered_x * escala; float scaled_x = centered_x * scale;
float scaled_y = centered_y * escala; float scaled_y = centered_y * scale;
// 4. Aplicar rotació 2D (Z-axis, tradicional) // 4. Aplicar rotación 2D (Z-axis, tradicional)
// IMPORTANT: En el sistema original, angle=0 apunta AMUNT (no dreta) // IMPORTANT: En el sistema original, angle=0 apunta AMUNT (no derecha)
// Per això usem (angle - PI/2) per compensar // 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 cos_a = std::cos(angle);
float sin_a = std::sin(angle); float sin_a = std::sin(angle);
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a); float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a);
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a); float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a);
// 5. Aplicar trasllació a posició mundial // 5. Aplicar traslación a posición mundial
return {.x = rotated_x + posicio.x, .y = rotated_y + posicio.y}; 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 std::shared_ptr<Graphics::Shape>& shape,
const Punt& posicio, const Vec2& position,
float angle, float angle,
float escala, float scale,
bool dibuixar,
float progress, float progress,
float brightness, float brightness,
const Rotation3D* rotation_3d) { const Rotation3D* rotation_3d,
// Verificar que la forma és vàlida SDL_Color color) {
if (!shape || !shape->es_valida()) { if (!shape || !shape->isValid()) {
return; return;
} }
// Si progress < 1.0, no dibuixar (tot o res)
if (progress < 1.0F) { if (progress < 1.0F) {
return; return;
} }
// Obtenir el centre de la forma per a transformacions const Vec2& SHAPE_CENTRE = shape->getCenter();
const Punt& shape_centre = shape->get_centre();
// Iterar sobre totes les primitives
for (const auto& primitive : shape->get_primitives()) { for (const auto& primitive : shape->get_primitives()) {
if (primitive.type == Graphics::PrimitiveType::POLYLINE) { 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++) { 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); const Vec2 P1 = transform_point(primitive.points[i], SHAPE_CENTRE, position, angle, scale, rotation_3d);
Punt p2 = transform_point(primitive.points[i + 1], shape_centre, posicio, angle, escala, 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),
linea(renderer, static_cast<int>(p1.x), static_cast<int>(p1.y), static_cast<int>(p2.x), static_cast<int>(p2.y), dibuixar, brightness); static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, 0.0F, color);
}
} 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);
} }
} 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);
} }
} }
} }
+19 -18
View File
@@ -1,8 +1,10 @@
// shape_renderer.hpp - Renderitzat de formes vectorials // shape_renderer.hpp - Renderizado de formes vectorials
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#pragma once #pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <memory> #include <memory>
@@ -14,9 +16,9 @@ namespace Rendering {
// Estructura per rotacions 3D (pitch, yaw, roll) // Estructura per rotacions 3D (pitch, yaw, roll)
struct Rotation3D { struct Rotation3D {
float pitch; // Rotació eix X (cabeceo arriba/baix) float pitch; // Rotación eix X (cabeceo arriba/baix)
float yaw; // Rotació eix Y (guiñada esquerra/dreta) float yaw; // Rotación eix Y (guiñada izquierda/derecha)
float roll; // Rotació eix Z (alabeo lateral) float roll; // Rotación eix Z (alabeo lateral)
Rotation3D() Rotation3D()
: pitch(0.0F), : pitch(0.0F),
@@ -32,23 +34,22 @@ struct Rotation3D {
} }
}; };
// Renderitzar forma amb transformacions // Renderizar shape con transformacions
// - renderer: SDL renderer // - renderer: SDL renderer
// - shape: forma vectorial a dibuixar // - shape: shape vectorial a draw
// - posicio: posició del centre en coordenades mundials // - position: posición del centro en coordenades mundials
// - angle: rotació en radians (0 = amunt, sentit horari) // - angle: rotación en radians (0 = amunt, sentit horari)
// - escala: factor d'escala (1.0 = mida original) // - scale: factor de scale (1.0 = mida original)
// - dibuixar: flag per dibuixar (false per col·lisions futures) // - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible)
// - progress: progrés de l'animació (0.0-1.0, default 1.0 = tot visible) // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
// - brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor) void render_shape(Rendering::Renderer* renderer,
void render_shape(SDL_Renderer* renderer,
const std::shared_ptr<Graphics::Shape>& shape, const std::shared_ptr<Graphics::Shape>& shape,
const Punt& posicio, const Vec2& position,
float angle, float angle,
float escala = 1.0F, float scale = 1.0F,
bool dibuixar = true,
float progress = 1.0F, float progress = 1.0F,
float brightness = 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 } // namespace Rendering
+6 -6
View File
@@ -1,5 +1,5 @@
// resource_helper.cpp - Implementació de funcions d'ajuda // resource_helper.cpp - Implementació de funciones de ajuda
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#include "resource_helper.hpp" #include "resource_helper.hpp"
@@ -15,7 +15,7 @@ bool initializeResourceSystem(const std::string& pack_file, bool fallback) {
return Loader::get().initialize(pack_file, fallback); return Loader::get().initialize(pack_file, fallback);
} }
// Carregar un fitxer // Carregar un file
std::vector<uint8_t> loadFile(const std::string& filepath) { std::vector<uint8_t> loadFile(const std::string& filepath) {
// Normalitzar la ruta // Normalitzar la ruta
std::string normalized = normalizePath(filepath); std::string normalized = normalizePath(filepath);
@@ -24,7 +24,7 @@ std::vector<uint8_t> loadFile(const std::string& filepath) {
return Loader::get().loadResource(normalized); return Loader::get().loadResource(normalized);
} }
// Comprovar si existeix un fitxer // Comprovar si existeix un file
bool fileExists(const std::string& filepath) { bool fileExists(const std::string& filepath) {
std::string normalized = normalizePath(filepath); std::string normalized = normalizePath(filepath);
return Loader::get().resourceExists(normalized); 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) // Eliminar rutes absolutes (detectar / o C:\ al principi)
if (!path.empty() && path[0] == '/') { 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/"); size_t data_pos = path.find("/data/");
if (data_pos != std::string::npos) { if (data_pos != std::string::npos) {
path = path.substr(data_pos + 6); // Saltar "/data/" path = path.substr(data_pos + 6); // Saltar "/data/"
@@ -73,7 +73,7 @@ std::string normalizePath(const std::string& path) {
return getPackPath(path); return getPackPath(path);
} }
// Comprovar si hi ha paquet carregat // Comprovar si hay paquet carregat
bool isPackLoaded() { bool isPackLoaded() {
return Loader::get().isPackLoaded(); return Loader::get().isPackLoaded();
} }
+4 -4
View File
@@ -1,5 +1,5 @@
// resource_helper.hpp - Funcions d'ajuda per gestió de recursos // resource_helper.hpp - Funciones de ajuda per gestió de recursos
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
// API simplificada i normalització de rutes // API simplificada i normalització de rutes
#pragma once #pragma once
@@ -10,10 +10,10 @@
namespace Resource::Helper { namespace Resource::Helper {
// Inicialització del sistema // Inicialización del sistema
bool initializeResourceSystem(const std::string& pack_file, bool fallback); 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); std::vector<uint8_t> loadFile(const std::string& filepath);
bool fileExists(const std::string& filepath); bool fileExists(const std::string& filepath);
+14 -14
View File
@@ -1,5 +1,5 @@
// resource_loader.cpp - Implementació del carregador de recursos // 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" #include "resource_loader.hpp"
@@ -19,18 +19,18 @@ Loader& Loader::get() {
bool Loader::initialize(const std::string& pack_file, bool enable_fallback) { bool Loader::initialize(const std::string& pack_file, bool enable_fallback) {
fallback_enabled_ = enable_fallback; fallback_enabled_ = enable_fallback;
// Intentar carregar el paquet // Intentar load el paquet
pack_ = std::make_unique<Pack>(); pack_ = std::make_unique<Pack>();
if (!pack_->loadPack(pack_file)) { if (!pack_->loadPack(pack_file)) {
if (!fallback_enabled_) { if (!fallback_enabled_) {
std::cerr << "[ResourceLoader] ERROR FATAL: No es pot carregar " << pack_file std::cerr << "[ResourceLoader] ERROR FATAL: No es pot load " << pack_file
<< " i el fallback està desactivat\n"; << " y el fallback está desactivat\n";
return false; return false;
} }
std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de fitxers\n"; std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de archivos\n";
pack_.reset(); // No hi ha paquet pack_.reset(); // No hay paquet
return true; return true;
} }
@@ -40,7 +40,7 @@ bool Loader::initialize(const std::string& pack_file, bool enable_fallback) {
// Carregar un recurs // Carregar un recurs
std::vector<uint8_t> Loader::loadResource(const std::string& filename) { std::vector<uint8_t> Loader::loadResource(const std::string& filename) {
// Intentar carregar del paquet primer // Intentar load del paquet primer
if (pack_) { if (pack_) {
if (pack_->hasResource(filename)) { if (pack_->hasResource(filename)) {
auto data = pack_->getResource(filename); auto data = pack_->getResource(filename);
@@ -51,7 +51,7 @@ std::vector<uint8_t> Loader::loadResource(const std::string& filename) {
<< "\n"; << "\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_) { if (!fallback_enabled_) {
std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: " std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: "
<< filename << "\n"; << 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_) { if (fallback_enabled_) {
return loadFromFilesystem(filename); return loadFromFilesystem(filename);
} }
@@ -74,7 +74,7 @@ bool Loader::resourceExists(const std::string& filename) {
return true; 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_) { if (fallback_enabled_) {
std::string fullpath = base_path_.empty() ? "data/" + filename : base_path_ + "/data/" + filename; std::string fullpath = base_path_.empty() ? "data/" + filename : base_path_ + "/data/" + filename;
return std::filesystem::exists(fullpath); return std::filesystem::exists(fullpath);
@@ -86,14 +86,14 @@ bool Loader::resourceExists(const std::string& filename) {
// Validar el paquet // Validar el paquet
bool Loader::validatePack() { bool Loader::validatePack() {
if (!pack_) { 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 false;
} }
return pack_->validatePack(); return pack_->validatePack();
} }
// Comprovar si hi ha paquet carregat // Comprovar si hay paquet carregat
bool Loader::isPackLoaded() const { bool Loader::isPackLoaded() const {
return pack_ != nullptr; return pack_ != nullptr;
} }
@@ -109,7 +109,7 @@ std::string Loader::getBasePath() const {
return base_path_; 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) { std::vector<uint8_t> Loader::loadFromFilesystem(const std::string& filename) {
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp") // The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
// We need to prepend base_path + "data/" // We need to prepend base_path + "data/"
@@ -136,7 +136,7 @@ std::vector<uint8_t> Loader::loadFromFilesystem(const std::string& filename) {
return {}; 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; return data;
} }
+4 -4
View File
@@ -1,6 +1,6 @@
// resource_loader.hpp - Carregador de recursos (Singleton) // resource_loader.hpp - Carregador de recursos (Singleton)
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
// Coordina càrrega des del paquet i/o sistema de fitxers // Coordina càrrega des del paquet i/o sistema de archivos
#pragma once #pragma once
@@ -18,7 +18,7 @@ class Loader {
// Singleton // Singleton
static Loader& get(); static Loader& get();
// Inicialització // Inicialización
bool initialize(const std::string& pack_file, bool enable_fallback); bool initialize(const std::string& pack_file, bool enable_fallback);
// Càrrega de recursos // Càrrega de recursos
@@ -46,7 +46,7 @@ class Loader {
bool fallback_enabled_ = false; bool fallback_enabled_ = false;
std::string base_path_; std::string base_path_;
// Funcions auxiliars // Funciones auxiliars
std::vector<uint8_t> loadFromFilesystem(const std::string& filename); std::vector<uint8_t> loadFromFilesystem(const std::string& filename);
}; };
+16 -16
View File
@@ -1,5 +1,5 @@
// resource_pack.cpp - Implementació del sistema d'empaquetament // resource_pack.cpp - Implementació del sistema de empaquetament
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#include "resource_pack.hpp" #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) { void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
// XOR és simètric // XOR es simètric
encryptData(data, key); 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::vector<uint8_t> Pack::readFile(const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary | std::ios::ate); std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) { if (!file) {
@@ -54,7 +54,7 @@ std::vector<uint8_t> Pack::readFile(const std::string& filepath) {
return data; 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) { bool Pack::addFile(const std::string& filepath, const std::string& pack_name) {
auto file_data = readFile(filepath); auto file_data = readFile(filepath);
if (file_data.empty()) { if (file_data.empty()) {
@@ -67,17 +67,17 @@ bool Pack::addFile(const std::string& filepath, const std::string& pack_name) {
.size = file_data.size(), .size = file_data.size(),
.checksum = calculateChecksum(file_data)}; .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()); data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry; 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"; << " bytes)\n";
return true; 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, bool Pack::addDirectory(const std::string& dir_path,
const std::string& base_path) { const std::string& base_path) {
namespace fs = std::filesystem; namespace fs = std::filesystem;
@@ -100,7 +100,7 @@ bool Pack::addDirectory(const std::string& dir_path,
// Convertir barres invertides a normals (Windows) // Convertir barres invertides a normals (Windows)
std::ranges::replace(relative_path, '\\', '/'); std::ranges::replace(relative_path, '\\', '/');
// Saltar fitxers de desenvolupament // Saltar archivos de desenvolupament
if (relative_path.find(".world") != std::string::npos || if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos || relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != 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 // Escriure metadades de recursos
for (const auto& [name, entry] : resources_) { for (const auto& [name, entry] : resources_) {
// Nom del fitxer // Nom del file
auto name_len = static_cast<uint32_t>(entry.filename.length()); auto name_len = static_cast<uint32_t>(entry.filename.length());
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len)); file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
file.write(entry.filename.c_str(), 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_; std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY); 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()); 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*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_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; return true;
} }
// Carregar paquet des de disc // Carregar paquet desde disc
bool Pack::loadPack(const std::string& pack_file) { bool Pack::loadPack(const std::string& pack_file) {
std::ifstream file(pack_file, std::ios::binary); std::ifstream file(pack_file, std::ios::binary);
if (!file) { if (!file) {
@@ -180,7 +180,7 @@ bool Pack::loadPack(const std::string& pack_file) {
uint32_t version; uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version)); file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) { if (version != VERSION) {
std::cerr << "[ResourcePack] Error: versió incompatible (esperava " << VERSION std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
<< ", trobat " << version << ")\n"; << ", trobat " << version << ")\n";
return false; return false;
} }
@@ -192,7 +192,7 @@ bool Pack::loadPack(const std::string& pack_file) {
// Llegir metadades de recursos // Llegir metadades de recursos
resources_.clear(); resources_.clear();
for (uint32_t i = 0; i < resource_count; ++i) { for (uint32_t i = 0; i < resource_count; ++i) {
// Nom del fitxer // Nom del file
uint32_t name_len; uint32_t name_len;
file.read(reinterpret_cast<char*>(&name_len), sizeof(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 std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum << " (esperat " << entry.checksum << ", calculat " << computed_checksum
<< ")\n"; << ")\n";
// No falla, però adverteix // No falla, pero adverteix
} }
return resource_data; return resource_data;
@@ -261,7 +261,7 @@ bool Pack::hasResource(const std::string& filename) const {
return resources_.contains(filename); 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> Pack::getResourceList() const {
std::vector<std::string> list; std::vector<std::string> list;
list.reserve(resources_.size()); list.reserve(resources_.size());
+11 -11
View File
@@ -1,6 +1,6 @@
// resource_pack.hpp - Sistema d'empaquetament de recursos // resource_pack.hpp - Sistema de empaquetament de recursos
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
// Basat en el sistema de "pollo" amb adaptacions per Orni Attack // Basat en el sistema de "pollo" con adaptacions per Orni Attack
#pragma once #pragma once
@@ -11,31 +11,31 @@
namespace Resource { namespace Resource {
// Capçalera del fitxer de paquet // Capçalera del file de paquet
struct PackHeader { struct PackHeader {
char magic[4]; // "ORNI" 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 // Entrada de recurs dins el paquet
struct ResourceEntry { struct ResourceEntry {
std::string filename; // Nom del recurs (amb barres normals) std::string filename; // Nom del recurs (con barres normals)
uint64_t offset; // Posició dins el bloc de dades uint64_t offset; // Posición dins el bloc de dades
uint64_t size; // Mida en bytes uint64_t size; // Mida en bytes
uint32_t checksum; // Checksum CRC32 per verificació uint32_t checksum; // Checksum CRC32 per verificació
}; };
// Classe principal per gestionar paquets de recursos // Clase principal per gestionar paquets de recursos
class Pack { class Pack {
public: public:
Pack() = default; Pack() = default;
~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 addFile(const std::string& filepath, const std::string& pack_name);
bool addDirectory(const std::string& dir_path, const std::string& base_path = ""); 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 savePack(const std::string& pack_file);
bool loadPack(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::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_; std::vector<uint8_t> data_;
// Funcions auxiliars // Funciones auxiliars
std::vector<uint8_t> readFile(const std::string& filepath); std::vector<uint8_t> readFile(const std::string& filepath);
[[nodiscard]] uint32_t calculateChecksum(const std::vector<uint8_t>& data) const; [[nodiscard]] uint32_t calculateChecksum(const std::vector<uint8_t>& data) const;
void encryptData(std::vector<uint8_t>& data, const std::string& key); void encryptData(std::vector<uint8_t>& data, const std::string& key);
-81
View File
@@ -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
+64
View File
@@ -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
+38
View File
@@ -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
+120 -68
View File
@@ -3,13 +3,18 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <algorithm>
#include <cerrno> #include <cerrno>
#include <cstdlib> #include <cstdlib>
#include <iostream> #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.hpp"
#include "core/audio/audio_cache.hpp" #include "core/audio/audio_adapter.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
@@ -17,9 +22,9 @@
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "core/resources/resource_loader.hpp" #include "core/resources/resource_loader.hpp"
#include "core/utils/path_utils.hpp" #include "core/utils/path_utils.hpp"
#include "game/escenes/escena_joc.hpp" #include "game/scenes/game_scene.hpp"
#include "game/escenes/escena_logo.hpp" #include "game/scenes/logo_scene.hpp"
#include "game/escenes/escena_titol.hpp" #include "game/scenes/title_scene.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "project.h" #include "project.h"
@@ -29,14 +34,14 @@
#endif #endif
// Using declarations per simplificar el codi // Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes; using SceneManager::SceneContext;
using Escena = ContextEscenes::Escena; using SceneType = SceneContext::SceneType;
// Constructor // Constructor
Director::Director(std::vector<std::string> const& args) { Director::Director(std::vector<std::string> const& args) {
std::cout << "Orni Attack - Inici\n"; std::cout << "Orni Attack - Inici\n";
// Inicialitzar opcions amb valors per defecte // Inicialitzar opciones con valors per defecte
Options::init(); Options::init();
// Comprovar arguments del programa // Comprovar arguments del programa
@@ -50,28 +55,28 @@ Director::Director(std::vector<std::string> const& args) {
// Inicialitzar sistema de recursos // Inicialitzar sistema de recursos
#ifdef RELEASE_BUILD #ifdef RELEASE_BUILD
// Mode release: paquet obligatori, sense fallback // Mode release: paquet obligatori, sin fallback
std::string pack_path = resource_base + "/resources.pack"; std::string pack_path = resource_base + "/resources.pack";
if (!Resource::Helper::initializeResourceSystem(pack_path, false)) { if (!Resource::Helper::initializeResourceSystem(pack_path, false)) {
std::cerr << "ERROR FATAL: No es pot carregar " << pack_path << "\n"; std::cerr << "ERROR FATAL: No es pot load " << pack_path << "\n";
std::cerr << "El joc no pot continuar sense els recursos.\n"; std::cerr << "El juego no pot continuar sin los recursos.\n";
std::exit(1); std::exit(1);
} }
// Validar integritat del paquet // Validar integritat del paquet
if (!Resource::Loader::get().validatePack()) { 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::exit(1);
} }
std::cout << "Sistema de recursos inicialitzat (mode release)\n"; std::cout << "Sistema de recursos inicialitzat (mode release)\n";
#else #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"; std::string pack_path = resource_base + "/resources.pack";
Resource::Helper::initializeResourceSystem(pack_path, true); Resource::Helper::initializeResourceSystem(pack_path, true);
if (Resource::Helper::isPackLoaded()) { 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 { } else {
std::cout << "Sistema de recursos inicialitzat (mode dev, fallback a data/)\n"; 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("jailgames");
createSystemFolder(std::string("jailgames/") + Project::NAME); 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"); Options::setConfigFile(system_folder_ + "/config.yaml");
// Carregar o crear configuració // Carregar o crear configuración
Options::loadFromFile(); Options::loadFromFile();
// Inicialitzar sistema d'input // Inicialitzar sistema de input
Input::init("data/gamecontrollerdb.txt"); Input::init("data/gamecontrollerdb.txt");
// Aplicar configuració de controls dels jugadors // Aplicar configuración de controls dels jugadors
Input::get()->applyPlayer1BindingsFromOptions(); Input::get()->applyPlayer1BindingsFromOptions();
Input::get()->applyPlayer2BindingsFromOptions(); Input::get()->applyPlayer2BindingsFromOptions();
if (Options::console) { if (Options::console) {
std::cout << "Configuració carregada\n"; std::cout << "Configuración carregada\n";
std::cout << " Finestra: " << Options::window.width << "×" std::cout << " Finestra: " << Options::window.width << "×"
<< Options::window.height << '\n'; << Options::window.height << '\n';
std::cout << " Física: rotation=" << Options::physics.rotation_speed std::cout << " Física: rotation=" << Options::physics.rotation_speed
@@ -111,7 +116,7 @@ Director::Director(std::vector<std::string> const& args) {
} }
Director::~Director() { Director::~Director() {
// Guardar opcions // Guardar opciones
Options::saveToFile(); Options::saveToFile();
// Cleanup input // Cleanup input
@@ -138,7 +143,7 @@ auto Director::checkProgramArguments(std::vector<std::string> const& args)
} else if (argument == "--reset-config") { } else if (argument == "--reset-config") {
Options::init(); Options::init();
Options::saveToFile(); 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); exit(EXIT_FAILURE);
case EEXIST: case EEXIST:
// La carpeta ja existeix (race condition), continuar // La carpeta ya existeix (race condition), continuar
break; break;
case ENAMETOOLONG: 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 { auto Director::run() -> int {
// Calculate initial size from saved zoom_factor // Calculate initial size from saved zoom_factor
int initial_width = static_cast<int>(std::round( int initial_width = static_cast<int>(std::round(
@@ -215,68 +220,115 @@ auto Director::run() -> int {
int initial_height = static_cast<int>(std::round( int initial_height = static_cast<int>(std::round(
Defaults::Window::HEIGHT * Options::window.zoom_factor)); 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); SDLManager sdl(initial_width, initial_height, Options::window.fullscreen);
// CRÍTIC: Forçar ocultació del cursor DESPRÉS de tota la inicialització SDL // CRÍTIC: Forçar ocultació del cursor DESPRÉS de toda la inicialización SDL
// Això evita que SDL mostre el cursor automàticament durant la creació de la finestra // Això evita que SDL mostre el cursor automàticament durante la creació de la finestra
if (!Options::window.fullscreen) { if (!Options::window.fullscreen) {
Mouse::forceHide(); Mouse::forceHide();
} }
// Inicialitzar sistema d'audio // Inicializar sistema de audio (config inyectada desde Defaults)
Audio::init(); const Audio::Config AUDIO_CONFIG{
Audio::get()->setMusicVolume(1.0); .enabled = Defaults::Audio::ENABLED,
Audio::get()->setSoundVolume(0.4); .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 // Precachear música para evitar lag al empezar
AudioCache::getMusic("title.ogg"); AudioResource::getMusic("title.ogg");
AudioCache::getMusic("game.ogg"); AudioResource::getMusic("game.ogg");
if (Options::console) { if (Options::console) {
std::cout << "Música precachejada: " std::cout << "Música precacheada\n";
<< AudioCache::getMusicCacheSize() << " fitxers\n";
} }
// Crear context d'escenes // Crear context de escenes
ContextEscenes context; SceneContext context;
#ifdef _DEBUG #ifdef _DEBUG
context.canviar_escena(Escena::TITOL); context.setNextScene(SceneType::TITLE);
#else #else
context.canviar_escena(Escena::LOGO); context.setNextScene(SceneType::LOGO);
#endif #endif
// Bucle principal de gestió d'escenes // Overlay de debug (FPS + VSync). Vive en el Director porque es global
while (context.escena_desti() != Escena::EIXIR) { // a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG).
// Sincronitzar GestorEscenes::actual amb context System::DebugOverlay debug_overlay(sdl.getRenderer());
// (altres sistemes encara poden llegir GestorEscenes::actual)
GestorEscenes::actual = context.escena_desti();
switch (context.escena_desti()) { // Bucle principal: construir escena → frame loop → destruir → siguiente.
case Escena::LOGO: { while (context.nextScene() != SceneType::EXIT) {
EscenaLogo logo(sdl, context); SceneManager::actual = context.nextScene();
logo.executar(); std::unique_ptr<Scene> scene = buildScene(context.nextScene(), sdl, context);
break; if (!scene) {
} break;
case Escena::TITOL: {
EscenaTitol titol(sdl, context);
titol.executar();
break;
}
case Escena::JOC: {
EscenaJoc joc(sdl, context);
joc.executar();
break;
}
default:
break;
} }
runFrameLoop(*scene, sdl, context, debug_overlay);
} }
// Sincronitzar final amb GestorEscenes::actual SceneManager::actual = SceneType::EXIT;
GestorEscenes::actual = Escena::EIXIR;
return 0; 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();
}
}
+21
View File
@@ -1,8 +1,15 @@
#pragma once #pragma once
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include "scene_context.hpp"
class Scene;
class SDLManager;
namespace System { class DebugOverlay; }
class Director { class Director {
public: public:
explicit Director(std::vector<std::string> const& args); explicit Director(std::vector<std::string> const& args);
@@ -17,4 +24,18 @@ class Director {
static auto checkProgramArguments(std::vector<std::string> const& args) static auto checkProgramArguments(std::vector<std::string> const& args)
-> std::string; -> std::string;
void createSystemFolder(const std::string& folder); 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);
}; };
+14 -14
View File
@@ -4,43 +4,43 @@
namespace GameConfig { namespace GameConfig {
// Mode de joc // Mode de juego
enum class Mode { enum class Mode {
NORMAL, // Partida normal NORMAL, // Partida normal
DEMO // Mode demostració (futur) DEMO // Mode demostració (futur)
}; };
// Configuració d'una partida // Configuración de una match
struct ConfigPartida { struct MatchConfig {
bool jugador1_actiu{false}; // És actiu el jugador 1? bool jugador1_actiu{false}; // Es active el player 1?
bool jugador2_actiu{false}; // És actiu el jugador 2? bool jugador2_actiu{false}; // Es active el player 2?
Mode mode{Mode::NORMAL}; // Mode de joc 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 { [[nodiscard]] bool es_un_jugador() const {
return (jugador1_actiu && !jugador2_actiu) || return (jugador1_actiu && !jugador2_actiu) ||
(!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 { [[nodiscard]] bool son_dos_jugadors() const {
return jugador1_actiu && jugador2_actiu; 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 { [[nodiscard]] bool cap_jugador() const {
return !jugador1_actiu && !jugador2_actiu; 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 { [[nodiscard]] uint8_t compte_jugadors() const {
return (jugador1_actiu ? 1 : 0) + (jugador2_actiu ? 1 : 0); return (jugador1_actiu ? 1 : 0) + (jugador2_actiu ? 1 : 0);
} }
// Retorna l'ID de l'únic jugador actiu (0 o 1) // Retorna l'ID de l'únic player active (0 o 1)
// Només vàlid si es_un_jugador() retorna true // Solo vàlid si es_un_jugador() retorna true
[[nodiscard]] uint8_t id_unic_jugador() const { [[nodiscard]] uint8_t id_unic_jugador() const {
if (jugador1_actiu && !jugador2_actiu) { if (jugador1_actiu && !jugador2_actiu) {
return 0; return 0;
@@ -48,7 +48,7 @@ struct ConfigPartida {
if (!jugador1_actiu && jugador2_actiu) { if (!jugador1_actiu && jugador2_actiu) {
return 1; return 1;
} }
return 0; // Fallback (cal comprovar es_un_jugador() primer) return 0; // Fallback (necesario comprovar es_un_jugador() primer)
} }
}; };
+8 -8
View File
@@ -5,18 +5,18 @@
#include <iostream> #include <iostream>
#include "context_escenes.hpp" #include "scene_context.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.hpp" #include "core/rendering/sdl_manager.hpp"
// Using declarations per simplificar el codi // Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes; using SceneManager::SceneContext;
using Escena = ContextEscenes::Escena; using SceneType = SceneContext::SceneType;
namespace GlobalEvents { 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) // 1. Permitir que Input procese el evento (para hotplug de gamepads)
auto event_msg = Input::get()->handleEvent(event); auto event_msg = Input::get()->handleEvent(event);
if (!event_msg.empty()) { 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) // 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
if (event.type == SDL_EVENT_QUIT) { if (event.type == SDL_EVENT_QUIT) {
context.canviar_escena(Escena::EIXIR); context.setNextScene(SceneType::EXIT);
GestorEscenes::actual = Escena::EIXIR; SceneManager::actual = SceneType::EXIT;
return true; return true;
} }
@@ -54,8 +54,8 @@ bool handle(const SDL_Event& event, SDLManager& sdl, ContextEscenes& context) {
return true; return true;
case SDL_SCANCODE_ESCAPE: case SDL_SCANCODE_ESCAPE:
context.canviar_escena(Escena::EIXIR); context.setNextScene(SceneType::EXIT);
GestorEscenes::actual = Escena::EIXIR; SceneManager::actual = SceneType::EXIT;
return true; return true;
default: default:
+5 -5
View File
@@ -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" // Basat en el patró del projecte "pollo"
// © 2025 Port a C++20 // © 2025 Port a C++20
@@ -8,12 +8,12 @@
// Forward declarations // Forward declarations
class SDLManager; class SDLManager;
namespace GestorEscenes { namespace SceneManager {
class ContextEscenes; class SceneContext;
} }
namespace GlobalEvents { namespace GlobalEvents {
// Processa events globals (F1/F2/F3/ESC/QUIT) // Processa events globals (F1/F2/F3/ESC/QUIT)
// Retorna true si l'event ha estat processat i no cal seguir processant-lo // Retorna true si l'event ha state processat y no necesario seguir processant-lo
bool handle(const SDL_Event& event, SDLManager& sdl, GestorEscenes::ContextEscenes& context); bool handle(const SDL_Event& event, SDLManager& sdl, SceneManager::SceneContext& context);
} // namespace GlobalEvents } // namespace GlobalEvents
+41
View File
@@ -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;
};
+81
View File
@@ -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
View File
@@ -1,43 +1,72 @@
// types.hpp - Tipos básicos compartidos
// © 2025 Orni Attack
#pragma once #pragma once
#include <array> #include <cmath>
#include <cstdint>
#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) constexpr auto operator+=(const Vec2& o) -> Vec2& {
struct IPunt { x += o.x;
float r; // Radi y += o.y;
float angle; // Angle en radians 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à constexpr auto operator+(Vec2 a, const Vec2& b) -> Vec2 {
struct Punt { a += b;
float x, y; return a;
}; }
constexpr auto operator-(Vec2 a, const Vec2& b) -> Vec2 {
// ============================================================================== a -= b;
// DEPRECATED: Legacy types (replaced by Shape system) return a;
// ============================================================================== }
// These types are kept temporarily for chatarra_cosmica_ (Phase 10: explosions) constexpr auto operator*(Vec2 v, float s) -> Vec2 {
// TODO Phase 10: Replace with particle system or remove completely v *= s;
return v;
// Nau (triangle) - DEPRECATED: Now using Shape system (ship.shp) }
struct Triangle { constexpr auto operator*(float s, Vec2 v) -> Vec2 {
IPunt p1, p2, p3; v *= s;
Punt centre; return v;
float angle; }
float velocitat; constexpr auto operator/(Vec2 v, float s) -> Vec2 {
}; v /= s;
return v;
// Polígon (enemics i bales) - DEPRECATED: Now using Shape system (.shp files) }
struct Poligon { constexpr auto operator-(const Vec2& v) -> Vec2 { return {.x = -v.x, .y = -v.y}; }
std::array<IPunt, Defaults::Entities::MAX_IPUNTS> ipuntx; constexpr auto operator==(const Vec2& a, const Vec2& b) -> bool {
Punt centre; return a.x == b.x && a.y == b.y;
float angle; }
float velocitat;
uint8_t n;
float drotacio;
float rotacio;
bool esta;
};
+7 -7
View File
@@ -1,5 +1,5 @@
// path_utils.cpp - Implementació de utilitats de rutes // 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" #include "path_utils.hpp"
@@ -13,10 +13,10 @@ namespace Utils {
static std::string executable_path_; static std::string executable_path_;
static std::string executable_directory_; 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) { void initializePathSystem(const char* argv0) {
if (argv0 == nullptr) { 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_path_ = "";
executable_directory_ = "."; executable_directory_ = ".";
return; return;
@@ -50,7 +50,7 @@ bool isMacOSBundle() {
#ifdef MACOS_BUNDLE #ifdef MACOS_BUNDLE
return true; return true;
#else #else
// Detecció en temps d'execució // Detecció en time de execució
// Cercar ".app/Contents/MacOS" a la ruta de l'executable // Cercar ".app/Contents/MacOS" a la ruta de l'executable
std::string exe_dir = getExecutableDirectory(); std::string exe_dir = getExecutableDirectory();
return exe_dir.find(".app/Contents/MacOS") != std::string::npos; return exe_dir.find(".app/Contents/MacOS") != std::string::npos;
@@ -62,10 +62,10 @@ std::string getResourceBasePath() {
std::string exe_dir = getExecutableDirectory(); std::string exe_dir = getExecutableDirectory();
if (isMacOSBundle()) { 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"; std::cout << "[PathUtils] Detectat bundle de macOS\n";
return exe_dir + "/../Resources"; return exe_dir + "/../Resources";
} // Executable normal: recursos al mateix directori } // Executable normal: recursos al mismo directori
return exe_dir; return exe_dir;
} }
@@ -76,7 +76,7 @@ std::string normalizePath(const std::string& path) {
// Convertir barres invertides a normals // Convertir barres invertides a normals
std::ranges::replace(normalized, '\\', '/'); std::ranges::replace(normalized, '\\', '/');
// Simplificar rutes amb filesystem // Simplificar rutes con filesystem
try { try {
std::filesystem::path fs_path(normalized); std::filesystem::path fs_path(normalized);
normalized = fs_path.lexically_normal().string(); normalized = fs_path.lexically_normal().string();
+2 -2
View File
@@ -1,5 +1,5 @@
// path_utils.hpp - Utilitats de gestió de rutes // 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 // Detecció de directoris i bundles multiplataforma
#pragma once #pragma once
@@ -8,7 +8,7 @@
namespace Utils { namespace Utils {
// Inicialització amb argv[0] // Inicialización con argv[0]
void initializePathSystem(const char* argv0); void initializePathSystem(const char* argv0);
// Obtenció de rutes // Obtenció de rutes
+116 -116
View File
@@ -2,9 +2,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_NODE_HPP #ifndef FK_YAML_NODE_HPP
@@ -25,9 +25,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_MACROS_DEFINE_MACROS_HPP #ifndef FK_YAML_DETAIL_MACROS_DEFINE_MACROS_HPP
@@ -38,9 +38,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
// Check version definitions if already defined. // Check version definitions if already defined.
@@ -94,9 +94,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_MACROS_CPP_CONFIG_MACROS_HPP #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 // With the MSVC compilers, the value of __cplusplus is by default always
// "199611L"(C++98). To avoid that, the library instead references _MSVC_LANG // "199611L"(C++98). To avoid that, the library instead references _MSVC_LANG
// which is always set a correct value. See // 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. // for more details.
#if defined(_MSVC_LANG) && !defined(__clang__) #if defined(_MSVC_LANG) && !defined(__clang__)
#define FK_YAML_CPLUSPLUS _MSVC_LANG #define FK_YAML_CPLUSPLUS _MSVC_LANG
@@ -265,9 +265,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_ASSERT_HPP #ifndef FK_YAML_DETAIL_ASSERT_HPP
@@ -290,9 +290,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_DOCUMENT_METAINFO_HPP #ifndef FK_YAML_DETAIL_DOCUMENT_METAINFO_HPP
@@ -308,9 +308,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_META_NODE_TRAITS_HPP #ifndef FK_YAML_DETAIL_META_NODE_TRAITS_HPP
@@ -323,9 +323,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_META_DETECT_HPP #ifndef FK_YAML_DETAIL_META_DETECT_HPP
@@ -341,9 +341,9 @@
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_META_STL_SUPPLEMENT_HPP #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. /// @brief An alias template for std::add_pointer::type with C++11.
/// @note std::add_pointer_t is available since C++14. /// @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. /// @tparam T A type to be added a pointer.
template <typename T> using add_pointer_t = typename std::add_pointer<T>::type; 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. /// @brief An alias template for std::enable_if::type with C++11.
/// @note std::enable_if_t is available since C++14. /// @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 Condition A condition tested at compile time.
/// @tparam T The type defined only if Condition is true. /// @tparam T The type defined only if Condition is true.
template <bool Condition, typename T = void> 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. /// @brief A simple implementation to use std::is_null_pointer with C++11.
/// @note std::is_null_pointer is available since C++14. /// @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. /// @tparam T The type to be checked if it's equal to std::nullptr_t.
template <typename T> template <typename T>
struct is_null_pointer struct is_null_pointer
@@ -395,20 +395,20 @@ struct is_null_pointer
/// @brief An alias template for std::remove_cv::type with C++11. /// @brief An alias template for std::remove_cv::type with C++11.
/// @note std::remove_cv_t is available since C++14. /// @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. /// @tparam T A type from which const-volatile qualifiers are removed.
template <typename T> using remove_cv_t = typename std::remove_cv<T>::type; 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. /// @brief An alias template for std::remove_pointer::type with C++11.
/// @note std::remove_pointer_t is available since C++14. /// @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. /// @tparam T A type from which a pointer is removed.
template <typename T> template <typename T>
using remove_pointer_t = typename std::remove_pointer<T>::type; using remove_pointer_t = typename std::remove_pointer<T>::type;
/// @brief An alias template for std::remove_reference::type with C++11. /// @brief An alias template for std::remove_reference::type with C++11.
/// @note std::remove_reference_t is available since C++14. /// @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. /// @tparam T A type from which a reference is removed.
template <typename T> template <typename T>
using remove_reference_t = typename std::remove_reference<T>::type; 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 /// @note
/// std::conjunction is available since C++17. /// std::conjunction is available since C++17.
/// This is applied when no traits are specified as inputs. /// 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. /// @tparam Traits Type traits to be checked if their ::value are all true.
template <typename... Traits> struct conjunction : std::true_type {}; template <typename... Traits> struct conjunction : std::true_type {};
@@ -519,7 +519,7 @@ struct conjunction<First, Rest...>
/// @note /// @note
/// std::disjunction is available since C++17. /// std::disjunction is available since C++17.
/// This is applied when no traits are specified as inputs. /// 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 /// @tparam Traits Type traits to be checked if at least one of their ::value is
/// true. /// true.
template <typename... Traits> struct disjunction : std::false_type {}; 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. /// @brief A simple implementation to use std::negation with C++11/C++14.
/// @note std::negation is available since C++17. /// @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. /// @tparam Trait Type trait whose ::value is negated.
template <typename Trait> template <typename Trait>
struct negation : std::integral_constant<bool, !Trait::value> {}; 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. /// @brief A simple implementation to use std::void_t with C++11/C++14.
/// @note std::void_t is available since C++17. /// @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. /// @tparam Types Any types to be transformed to void type.
template <typename... Types> using void_t = typename make_void<Types...>::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 /// @brief A simple implementation to use std::remove_cvref_t with
/// C++11/C++14/C++17. /// C++11/C++14/C++17.
/// @note std::remove_cvref & std::remove_cvref_t are available since C++20. /// @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. /// @tparam T A type from which cv-qualifiers and reference are removed.
template <typename T> template <typename T>
using remove_cvref_t = using remove_cvref_t =
@@ -790,9 +790,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_META_TYPE_TRAITS_HPP #ifndef FK_YAML_DETAIL_META_TYPE_TRAITS_HPP
@@ -890,9 +890,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_FKYAML_FWD_HPP #ifndef FK_YAML_FKYAML_FWD_HPP
@@ -1106,9 +1106,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_YAML_VERSION_TYPE_HPP #ifndef FK_YAML_YAML_VERSION_TYPE_HPP
@@ -1171,9 +1171,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_EXCEPTION_SAFE_ALLOCATION_HPP #ifndef FK_YAML_DETAIL_EXCEPTION_SAFE_ALLOCATION_HPP
@@ -1278,9 +1278,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_DESERIALIZER_HPP #ifndef FK_YAML_DETAIL_INPUT_DESERIALIZER_HPP
@@ -1299,9 +1299,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_LEXICAL_ANALYZER_HPP #ifndef FK_YAML_DETAIL_INPUT_LEXICAL_ANALYZER_HPP
@@ -1320,9 +1320,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_ENCODINGS_URI_ENCODING_HPP #ifndef FK_YAML_DETAIL_ENCODINGS_URI_ENCODING_HPP
@@ -1453,9 +1453,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODINGS_HPP #ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODINGS_HPP
@@ -1471,9 +1471,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_EXCEPTION_HPP #ifndef FK_YAML_EXCEPTION_HPP
@@ -1491,9 +1491,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_STRING_FORMATTER_HPP #ifndef FK_YAML_DETAIL_STRING_FORMATTER_HPP
@@ -1541,9 +1541,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_TYPES_NODE_T_HPP #ifndef FK_YAML_DETAIL_TYPES_NODE_T_HPP
@@ -1558,9 +1558,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_NODE_TYPE_HPP #ifndef FK_YAML_NODE_TYPE_HPP
@@ -2155,9 +2155,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_BLOCK_SCALAR_HEADER_HPP #ifndef FK_YAML_DETAIL_INPUT_BLOCK_SCALAR_HEADER_HPP
@@ -2196,9 +2196,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_POSITION_TRACKER_HPP #ifndef FK_YAML_DETAIL_INPUT_POSITION_TRACKER_HPP
@@ -2213,9 +2213,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_STR_VIEW_HPP #ifndef FK_YAML_DETAIL_STR_VIEW_HPP
@@ -3346,9 +3346,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_TYPES_LEXICAL_TOKEN_T_HPP #ifndef FK_YAML_DETAIL_TYPES_LEXICAL_TOKEN_T_HPP
@@ -4396,7 +4396,7 @@ private:
if FK_YAML_UNLIKELY (cur_itr == m_end_itr) { if FK_YAML_UNLIKELY (cur_itr == m_end_itr) {
// Without the following iterator update, lexer cannot reach the end of // Without the following iterator update, lexer cannot reach the end of
// input buffer and causes infinite loops from the next loop. // 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; m_cur_itr = m_end_itr;
// If there's no non-empty line, the content indentation level is equal to // 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 // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_SCALAR_PARSER_HPP #ifndef FK_YAML_DETAIL_INPUT_SCALAR_PARSER_HPP
@@ -4743,9 +4743,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
// **NOTE FOR LIBRARY DEVELOPERS**: // **NOTE FOR LIBRARY DEVELOPERS**:
@@ -5644,9 +5644,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_ENCODINGS_YAML_ESCAPER_HPP #ifndef FK_YAML_DETAIL_ENCODINGS_YAML_ESCAPER_HPP
@@ -6005,9 +6005,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_SCALAR_SCANNER_HPP #ifndef FK_YAML_DETAIL_INPUT_SCALAR_SCANNER_HPP
@@ -6377,9 +6377,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_TAG_T_HPP #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 // conversion error from a scalar which is not tagged with !!int is
// recovered by treating it as a string scalar. See // 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())); return basic_node_type(string_type(token.begin(), token.end()));
} }
case node_type::FLOAT: { case node_type::FLOAT: {
@@ -6966,7 +6966,7 @@ private:
// conversion error from a scalar which is not tagged with !!float is // conversion error from a scalar which is not tagged with !!float is
// recovered by treating it as a string scalar. See // 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())); return basic_node_type(string_type(token.begin(), token.end()));
} }
case node_type::STRING: case node_type::STRING:
@@ -7000,9 +7000,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_TAG_RESOLVER_HPP #ifndef FK_YAML_DETAIL_INPUT_TAG_RESOLVER_HPP
@@ -7208,9 +7208,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_META_INPUT_ADAPTER_TRAITS_HPP #ifndef FK_YAML_DETAIL_META_INPUT_ADAPTER_TRAITS_HPP
@@ -7274,9 +7274,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_NODE_ATTRS_HPP #ifndef FK_YAML_DETAIL_NODE_ATTRS_HPP
@@ -7417,9 +7417,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_NODE_PROPERTY_HPP #ifndef FK_YAML_DETAIL_NODE_PROPERTY_HPP
@@ -8019,7 +8019,7 @@ private:
// (i.e., the properties are for a container node), the // (i.e., the properties are for a container node), the
// application and the line advancement must happen here. // application and the line advancement must happen here.
// Otherwise, a false indent error will be emitted. See // 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; line = line_after_props;
indent = lexer.get_last_token_begin_pos(); indent = lexer.get_last_token_begin_pos();
*mp_current_node = basic_node_type::mapping(); *mp_current_node = basic_node_type::mapping();
@@ -8910,9 +8910,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_INPUT_INPUT_ADAPTER_HPP #ifndef FK_YAML_DETAIL_INPUT_INPUT_ADAPTER_HPP
@@ -8935,9 +8935,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODE_DETECTOR_HPP #ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODE_DETECTOR_HPP
@@ -8953,9 +8953,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODE_T_HPP #ifndef FK_YAML_DETAIL_ENCODINGS_UTF_ENCODE_T_HPP
@@ -10496,9 +10496,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_ITERATOR_HPP #ifndef FK_YAML_DETAIL_ITERATOR_HPP
@@ -10914,7 +10914,7 @@ namespace std {
#ifdef __clang__ #ifdef __clang__
// clang emits warnings against mixed usage of class/struct for // clang emits warnings against mixed usage of class/struct for
// tuple_size/tuple_element. see also: // 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 push
#pragma clang diagnostic ignored "-Wmismatched-tags" #pragma clang diagnostic ignored "-Wmismatched-tags"
#endif #endif
@@ -10949,9 +10949,9 @@ struct tuple_element<I, ::fkyaml::detail::iterator<ValueType>> {
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_MAP_RANGE_PROXY_HPP #ifndef FK_YAML_DETAIL_MAP_RANGE_PROXY_HPP
@@ -11139,9 +11139,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_NODE_REF_STORAGE_HPP #ifndef FK_YAML_DETAIL_NODE_REF_STORAGE_HPP
@@ -11235,9 +11235,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_OUTPUT_SERIALIZER_HPP #ifndef FK_YAML_DETAIL_OUTPUT_SERIALIZER_HPP
@@ -11255,9 +11255,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_CONVERSIONS_TO_STRING_HPP #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 // If `v` is actually an integer and no scientific notation is used for
// serialization, ".0" must be appended. The result would cause a roundtrip // 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"); const std::size_t pos = s.find_first_of(".e");
if (pos == std::string::npos) { if (pos == std::string::npos) {
s += ".0"; s += ".0";
@@ -11744,9 +11744,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_REVERSE_ITERATOR_HPP #ifndef FK_YAML_DETAIL_REVERSE_ITERATOR_HPP
@@ -12016,9 +12016,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_TYPES_YAML_VERSION_T_HPP #ifndef FK_YAML_DETAIL_TYPES_YAML_VERSION_T_HPP
@@ -12073,9 +12073,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_NODE_VALUE_CONVERTER_HPP #ifndef FK_YAML_NODE_VALUE_CONVERTER_HPP
@@ -12090,9 +12090,9 @@ FK_YAML_DETAIL_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_CONVERSIONS_FROM_NODE_HPP #ifndef FK_YAML_DETAIL_CONVERSIONS_FROM_NODE_HPP
@@ -12875,9 +12875,9 @@ FK_YAML_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_DETAIL_CONVERSIONS_TO_NODE_HPP #ifndef FK_YAML_DETAIL_CONVERSIONS_TO_NODE_HPP
@@ -13262,9 +13262,9 @@ FK_YAML_NAMESPACE_END
// | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML // | __| |_/ | \_/ |/ _ \ / \/ \| | fkYAML: A C++ header-only YAML
// library | __| _ < \_ _/| ___ | _ | |___ version 0.4.2 // 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 // SPDX-License-Identifier: MIT
#ifndef FK_YAML_ORDERED_MAP_HPP #ifndef FK_YAML_ORDERED_MAP_HPP
@@ -13875,7 +13875,7 @@ public:
/// iterators into a basic_node object. /// iterators into a basic_node object.
/// @note /// @note
/// Iterators must satisfy the LegacyInputIterator requirements. /// 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 /// @tparam ItrType Type of a compatible iterator
/// @param[in] begin An iterator to the first element of an input sequence. /// @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 /// @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 // Whitespace before the literal operator identifier is deprecated in C++23 or
// better but required in C++11. Ignore the warning as a workaround. // 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__) #if defined(__clang__)
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated" #pragma clang diagnostic ignored "-Wdeprecated"
+68 -49
View File
@@ -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/ // http://nothings.org/stb_vorbis/
// //
// Original version written by Sean Barrett in 2007. // Original version written by Sean Barrett in 2007.
@@ -29,12 +29,15 @@
// Bernhard Wodo Evan Balster github:alxprd // Bernhard Wodo Evan Balster github:alxprd
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot // Tom Beaumont Ingo Leitgeb Nicolas Guillemot
// Phillip Bennefall Rohit Thiago Goulart // Phillip Bennefall Rohit Thiago Goulart
// github:manxorist saga musix github:infatum // github:manxorist Saga Musix github:infatum
// Timur Gagiev Maxwell Koo Peter Waller // Timur Gagiev Maxwell Koo Peter Waller
// github:audinowho Dougall Johnson David Reid // github:audinowho Dougall Johnson David Reid
// github:Clownacy Pedro J. Estebanez Remi Verschelde // github:Clownacy Pedro J. Estebanez Remi Verschelde
// AnthoFoxo github:morlat Gabriel Ravier
// //
// Partial history: // 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.20 - 2020-07-11 - several small fixes
// 1.19 - 2020-02-05 - warnings // 1.19 - 2020-02-05 - warnings
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc. // 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 // 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 first channel, and (*output)[1][0] contains the first sample from
// the second channel. // 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); extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
// inform stb_vorbis that your next datablock will not be contiguous with // inform stb_vorbis that your next datablock will not be contiguous with
@@ -579,7 +588,7 @@ enum STBVorbisError
#if defined(_MSC_VER) || defined(__MINGW32__) #if defined(_MSC_VER) || defined(__MINGW32__)
#include <malloc.h> #include <malloc.h>
#endif #endif
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__) #if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
#include <alloca.h> #include <alloca.h>
#endif #endif
#else // STB_VORBIS_NO_CRT #else // STB_VORBIS_NO_CRT
@@ -646,6 +655,12 @@ typedef signed int int32;
typedef float codetype; typedef float codetype;
#ifdef _MSC_VER
#define STBV_NOTUSED(v) (void)(v)
#else
#define STBV_NOTUSED(v) (void)sizeof(v)
#endif
// @NOTE // @NOTE
// //
// Some arrays below are tagged "//varies", which means it's actually // 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 sign = x & 0x80000000;
uint32 exp = (x & 0x7fe00000) >> 21; uint32 exp = (x & 0x7fe00000) >> 21;
double res = sign ? -(double)mantissa : (double)mantissa; 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 // find the first entry
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break; for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
if (k == n) { assert(c->sorted_entries == 0); return TRUE; } 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 to the list
add_entry(c, 0, k, m++, len[k], values); add_entry(c, 0, k, m++, len[k], values);
// add all available leaves // add all available leaves
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
uint32 res; uint32 res;
int z = len[i], y; int z = len[i], y;
if (z == NO_CODE) continue; 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, // find lowest available leaf (should always be earliest,
// which is what the specification calls for) // which is what the specification calls for)
// note that this property, and the fact we can never have // 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; while (z > 0 && !available[z]) --z;
if (z == 0) { return FALSE; } if (z == 0) { return FALSE; }
res = available[z]; res = available[z];
assert(z >= 0 && z < 32);
available[z] = 0; available[z] = 0;
add_entry(c, bit_reverse(res), i, m++, len[i], values); add_entry(c, bit_reverse(res), i, m++, len[i], values);
// propagate availability up the tree // propagate availability up the tree
if (z != len[i]) { if (z != len[i]) {
assert(len[i] >= 0 && len[i] < 32);
for (y=len[i]; y > z; --y) { for (y=len[i]; y > z; --y) {
assert(available[y] == 0); assert(available[y] == 0);
available[y] = res + (1 << (32-y)); 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) { while (z > base) {
float k00,k11; float k00,k11;
float l00,l11;
k00 = z[-0] - z[-8]; k00 = z[-0] - z[ -8];
k11 = z[-1] - z[-9]; k11 = z[-1] - z[ -9];
z[-0] = z[-0] + z[-8]; l00 = z[-2] - z[-10];
z[-1] = z[-1] + z[-9]; l11 = z[-3] - z[-11];
z[-8] = k00; z[ -0] = z[-0] + z[ -8];
z[-9] = k11 ; 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]; k00 = z[ -4] - z[-12];
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
k11 = z[ -5] - z[-13]; k11 = z[ -5] - z[-13];
l00 = z[ -6] - z[-14];
l11 = z[ -7] - z[-15];
z[ -4] = z[ -4] + z[-12]; z[ -4] = z[ -4] + z[-12];
z[ -5] = z[ -5] + z[-13]; 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[ -6] = z[ -6] + z[-14];
z[ -7] = z[ -7] + z[-15]; z[ -7] = z[ -7] + z[-15];
z[-14] = (k00+k11) * A2; z[-12] = k11;
z[-15] = (k00-k11) * A2; z[-13] = -k00;
z[-14] = (l11-l00) * A2;
z[-15] = (l00+l11) * -A2;
iter_54(z); iter_54(z);
iter_54(z-8); 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) { for (q=1; q < g->values; ++q) {
j = g->sorted_order[q]; j = g->sorted_order[q];
#ifndef STB_VORBIS_NO_DEFER_FLOOR #ifndef STB_VORBIS_NO_DEFER_FLOOR
STBV_NOTUSED(step2_flag);
if (finalY[j] >= 0) if (finalY[j] >= 0)
#else #else
if (step2_flag[j]) if (step2_flag[j])
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// WINDOWING // WINDOWING
STBV_NOTUSED(left_end);
n = f->blocksize[m->blockflag]; n = f->blocksize[m->blockflag];
map = &f->mapping[m->mapping]; 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 // this isn't to spec, but spec would require us to read ahead
// and decode the size of all current frames--could be done, // and decode the size of all current frames--could be done,
// but presumably it's not a commonly used feature // 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, // we might have to discard samples "from" the next frame too,
// if we're lapping a large block then a small at the start? // if we're lapping a large block then a small at the start?
f->discard_samples_deferred = n - right_end; f->discard_samples_deferred = n - right_end;
@@ -3642,9 +3658,11 @@ static int start_decoder(vorb *f)
f->vendor[len] = (char)'\0'; f->vendor[len] = (char)'\0';
//user comments //user comments
f->comment_list_length = get32_packet(f); f->comment_list_length = get32_packet(f);
if (f->comment_list_length > 0) { f->comment_list = NULL;
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length)); if (f->comment_list_length > 0)
if (f->comment_list == NULL) return error(f, VORBIS_outofmem); {
f->comment_list = (char**) setup_malloc(f, sizeof(char*) * (f->comment_list_length));
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
} }
for(i=0; i < f->comment_list_length; ++i) { for(i=0; i < f->comment_list_length; ++i) {
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
unsigned int div=1; unsigned int div=1;
for (k=0; k < c->dimensions; ++k) { for (k=0; k < c->dimensions; ++k) {
int off = (z / div) % c->lookup_values; int off = (z / div) % c->lookup_values;
float val = mults[off]; float val = mults[off]*c->delta_value + c->minimum_value + last;
val = mults[off]*c->delta_value + c->minimum_value + last;
c->multiplicands[j*c->dimensions + k] = val; c->multiplicands[j*c->dimensions + k] = val;
if (c->sequence_p) if (c->sequence_p)
last = val; 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); if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
} }
for (k=0; k < 1 << g->class_subclasses[j]; ++k) { 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); 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; *error = VORBIS_need_more_data;
else else
*error = p.error; *error = p.error;
vorbis_deinit(&p);
return NULL; return NULL;
} }
f = vorbis_alloc(&p); f = vorbis_alloc(&p);
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
header[i] = get8(f); header[i] = get8(f);
if (f->eof) return 0; if (f->eof) return 0;
if (header[4] != 0) goto invalid; 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) for (i=22; i < 26; ++i)
header[i] = 0; header[i] = 0;
crc = 0; crc = 0;
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
// set. whoops! // set. whoops!
break; 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); 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 * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
{ {
stb_vorbis *f, p; stb_vorbis *f, p;
if (data == NULL) return NULL; if (!data) {
if (error) *error = VORBIS_unexpected_eof;
return NULL;
}
vorbis_init(&p, alloc); vorbis_init(&p, alloc);
p.stream = (uint8 *) data; p.stream = (uint8 *) data;
p.stream_end = (uint8 *) data + len; 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) static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
{ {
#define BUFFER_SIZE 32 #define STB_BUFFER_SIZE 32
float buffer[BUFFER_SIZE]; float buffer[STB_BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE; int i,j,o,n = STB_BUFFER_SIZE;
check_endianness(); check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE) { for (o = 0; o < len; o += STB_BUFFER_SIZE) {
memset(buffer, 0, sizeof(buffer)); memset(buffer, 0, sizeof(buffer));
if (o + n > len) n = len - o; if (o + n > len) n = len - o;
for (j=0; j < num_c; ++j) { 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; 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) static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
{ {
#define BUFFER_SIZE 32 #define STB_BUFFER_SIZE 32
float buffer[BUFFER_SIZE]; float buffer[STB_BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE >> 1; int i,j,o,n = STB_BUFFER_SIZE >> 1;
// o is the offset in the source data // o is the offset in the source data
check_endianness(); 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 // o2 is the offset in the output data
int o2 = o << 1; int o2 = o << 1;
memset(buffer, 0, sizeof(buffer)); 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; 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) 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; float **outputs;
int len = num_shorts / channels; int len = num_shorts / channels;
int n=0; int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) { while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start; int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n; 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; float **outputs;
int n=0; int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) { while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start; int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n; if (n+k >= len) k = len - n;
+13
View File
@@ -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
View File
@@ -2,20 +2,19 @@
#include "core/defaults.hpp" #include "core/defaults.hpp"
// Aliases per a backward compatibility amb codi existent // Aliases para backward compatibility con codi existent
// Permet usar Constants::MARGE_ESQ en lloc de Defaults::Game::MARGIN_LEFT // Permet usar Constants::MARGIN_LEFT en lloc de Defaults::Game::MARGIN_LEFT
namespace Constants { namespace Constants {
// Marges de l'àrea de joc (derivats de Defaults::Zones::GAME) // Márgenes de l'àrea de juego (derivats de Defaults::Zones::GAME)
constexpr int MARGE_ESQ = static_cast<int>(Defaults::Zones::PLAYAREA.x); constexpr int MARGIN_LEFT = static_cast<int>(Defaults::Zones::PLAYAREA.x);
constexpr int MARGE_DRET = constexpr int MARGIN_RIGHT =
static_cast<int>(Defaults::Zones::PLAYAREA.x + Defaults::Zones::PLAYAREA.w); static_cast<int>(Defaults::Zones::PLAYAREA.x + Defaults::Zones::PLAYAREA.w);
constexpr int MARGE_DALT = static_cast<int>(Defaults::Zones::PLAYAREA.y); constexpr int MARGIN_TOP = static_cast<int>(Defaults::Zones::PLAYAREA.y);
constexpr int MARGE_BAIX = constexpr int MARGIN_BOTTOM =
static_cast<int>(Defaults::Zones::PLAYAREA.y + Defaults::Zones::PLAYAREA.h); static_cast<int>(Defaults::Zones::PLAYAREA.y + Defaults::Zones::PLAYAREA.h);
// Límits de polígons i objectes // Límits de objectes
constexpr int MAX_IPUNTS = Defaults::Entities::MAX_IPUNTS;
constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS; constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS;
constexpr int MAX_BALES = Defaults::Entities::MAX_BALES; 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 // Helpers per comprovar límits de zona
inline bool dins_zona_joc(float x, float y) { inline bool dins_zona_joc(float x, float y) {
const SDL_FPoint punt = {x, y}; const SDL_FPoint point = {x, y};
return SDL_PointInRectFloat(&punt, &Defaults::Zones::PLAYAREA); return SDL_PointInRectFloat(&point, &Defaults::Zones::PLAYAREA);
} }
inline void obtenir_limits_zona(float& min_x, float& max_x, float& min_y, float& max_y) { 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; 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) { 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; const auto& zona = Defaults::Zones::PLAYAREA;
constexpr float MARGE_SEGURETAT = 10.0F; // Safety margin 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; 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) { inline void obtenir_centre_zona(float& centre_x, float& centre_y) {
const auto& zona = Defaults::Zones::PLAYAREA; const auto& zona = Defaults::Zones::PLAYAREA;
centre_x = zona.x + (zona.w / 2.0F); centre_x = zona.x + (zona.w / 2.0F);
+19 -16
View File
@@ -1,37 +1,40 @@
// debris.hpp - Fragment de línia volant (explosió de formes) // debris.hpp - Fragment de línia volant (explosión de formes)
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#pragma once #pragma once
#include <SDL3/SDL.h>
#include "core/types.hpp" #include "core/types.hpp"
namespace Effects { namespace Effects {
// Debris: un segment de línia que vola perpendicular a sí mateix // Debris: un segment de línia que vola perpendicular a sí mismo
// Representa un fragment d'una forma destruïda (nau, enemic, bala) // Representa un fragment de una shape destruïda (ship, enemy, bullet)
struct Debris { struct Debris {
// Geometria del segment (2 punts en coordenades mundials) // Geometria del segment (2 points en coordenades mundials)
Punt p1; // Punt inicial del segment Vec2 p1; // Vec2 inicial del segment
Punt p2; // Punt final del segment Vec2 p2; // Vec2 final del segment
// Física // Física
Punt velocitat; // Velocitat en px/s (components x, y) Vec2 velocity; // Velocidad en px/s (components x, y)
float acceleracio; // Acceleració negativa (fricció) en px/s² float acceleration; // Aceleración negativa (fricció) en px/s²
// Rotació // Rotación
float angle_rotacio; // Angle de rotació acumulat (radians) float angle_rotacio; // Angle de rotación acumulat (radians)
float velocitat_rot; // Velocitat de rotació de TRAYECTORIA (rad/s) float velocitat_rot; // Velocidad de rotación de TRAYECTORIA (rad/s)
float velocitat_rot_visual; // Velocitat de rotació VISUAL del segment (rad/s) float velocitat_rot_visual; // Velocidad de rotación VISUAL del segment (rad/s)
// Estat de vida // Estat de vida
float temps_vida; // Temps transcorregut (segons) float temps_vida; // Temps transcorregut (segons)
float temps_max; // Temps de vida màxim (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) float factor_shrink; // Factor de reducció per segon (0.0-1.0)
// Rendering // 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 } // namespace Effects
+113 -112
View File
@@ -1,5 +1,5 @@
// debris_manager.cpp - Implementació del gestor de fragments // 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" #include "debris_manager.hpp"
@@ -14,60 +14,61 @@
namespace Effects { 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) // (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) { static Vec2 transform_point(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) {
// 1. Centrar el punt respecte al centre de la forma // 1. Centrar el point respecte al centro de la shape
float centered_x = point.x - shape_centre.x; float centered_x = point.x - shape_centre.x;
float centered_y = point.y - shape_centre.y; float centered_y = point.y - shape_centre.y;
// 2. Aplicar escala al punt centrat // 2. Aplicar scale al point centrat
float scaled_x = centered_x * escala; float scaled_x = centered_x * scale;
float scaled_y = centered_y * escala; float scaled_y = centered_y * scale;
// 3. Aplicar rotació // 3. Aplicar rotación
float cos_a = std::cos(angle); float cos_a = std::cos(angle);
float sin_a = std::sin(angle); float sin_a = std::sin(angle);
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a); float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a);
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a); float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a);
// 4. Aplicar trasllació a posició mundial // 4. Aplicar traslación a posición mundial
return {.x = rotated_x + posicio.x, .y = rotated_y + posicio.y}; return {.x = rotated_x + position.x, .y = rotated_y + position.y};
} }
DebrisManager::DebrisManager(SDL_Renderer* renderer) DebrisManager::DebrisManager(Rendering::Renderer* renderer)
: renderer_(renderer) { : renderer_(renderer) {
// Inicialitzar tots els debris com inactius // Inicialitzar todos los debris como inactius
for (auto& debris : debris_pool_) { for (auto& debris : debris_pool_) {
debris.actiu = false; debris.active = false;
} }
} }
void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape, void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
const Punt& centre, const Vec2& centro,
float angle, float angle,
float escala, float scale,
float velocitat_base, float velocitat_base,
float brightness, float brightness,
const Punt& velocitat_objecte, const Vec2& velocitat_objecte,
float velocitat_angular, float velocitat_angular,
float factor_herencia_visual, float factor_herencia_visual,
const std::string& sound) { const std::string& sound,
if (!shape || !shape->es_valida()) { SDL_Color color) {
if (!shape || !shape->isValid()) {
return; return;
} }
// Reproducir sonido de explosión // Reproducir sonido de explosión
Audio::get()->playSound(sound, Audio::Group::GAME); Audio::get()->playSound(sound, Audio::Group::GAME);
// Obtenir centre de la forma per a transformacions // Obtenir centro de la shape para transformacions
const Punt& shape_centre = shape->get_centre(); 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()) { for (const auto& primitive : shape->get_primitives()) {
// Processar cada segment de línia // 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) { if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
// Polyline: extreure segments consecutius // 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) { for (const auto& [local_p1, local_p2] : segments) {
// 1. Transformar punts locals → coordenades mundials // 1. Transformar points locals → coordenades mundials
Punt world_p1 = Vec2 world_p1 =
transform_point(local_p1, shape_centre, centre, angle, escala); transform_point(local_p1, shape_centre, centro, angle, scale);
Punt world_p2 = Vec2 world_p2 =
transform_point(local_p2, shape_centre, centre, angle, escala); transform_point(local_p2, shape_centre, centro, angle, scale);
// 2. Trobar slot lliure // 2. Trobar slot lliure
Debris* debris = trobar_slot_lliure(); Debris* debris = findFreeSlot();
if (debris == nullptr) { if (debris == nullptr) {
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n"; std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
return; // Pool ple return; // Pool ple
@@ -100,25 +101,25 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
debris->p1 = world_p1; debris->p1 = world_p1;
debris->p2 = world_p2; debris->p2 = world_p2;
// 4. Calcular direcció d'explosió (radial, des del centre cap a fora) // 4. Calcular direcció de explosión (radial, des del centro hacia fuera)
Punt direccio = calcular_direccio_explosio(world_p1, world_p2, centre); 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 = float speed =
velocitat_base + velocitat_base +
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) * (((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
Defaults::Physics::Debris::VARIACIO_VELOCITAT); Defaults::Physics::Debris::VARIACIO_VELOCITAT);
// Heredar velocitat de l'objecte original (suma vectorial) // Heredar velocity de l'objecte original (suma vectorial)
debris->velocitat.x = (direccio.x * speed) + velocitat_objecte.x; debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
debris->velocitat.y = (direccio.y * speed) + velocitat_objecte.y; debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
debris->acceleracio = Defaults::Physics::Debris::ACCELERACIO; 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) { 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 = float factor_herencia =
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN + Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) * ((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; ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
velocitat_ang_heretada *= (1.0F + variacio); 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; constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
float abs_ang = std::abs(velocitat_ang_heretada); float abs_ang = std::abs(velocitat_ang_heretada);
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F; float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
if (abs_ang > CAP) { if (abs_ang > CAP) {
// Excés: convertir a velocitat tangencial // Excés: convertir a velocity tangencial
float excess = abs_ang - CAP; float excess = abs_ang - CAP;
// Radi de la forma (enemics = 20 px) // Radi de la shape (enemigos = 20 px)
float radius = 20.0F; float radius = 20.0F;
// Velocitat tangencial = ω_excés × radi // Velocidad tangencial = ω_excés × radi
float v_tangential = excess * radius; float v_tangential = excess * radius;
// Direcció tangencial: perpendicular a la radial (90° CCW) // 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_x = -direccio.y;
float tangent_y = direccio.x; float tangent_y = direccio.x;
// Afegir velocitat tangencial (suma vectorial) // Añadir velocity tangencial (suma vectorial)
debris->velocitat.x += tangent_x * v_tangential; debris->velocity.x += tangent_x * v_tangential;
debris->velocitat.y += tangent_y * 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; debris->velocitat_rot = sign_ang * CAP;
} else { } else {
// Per sota del cap: comportament normal // Per sota del sin: comportament normal
debris->velocitat_rot = velocitat_ang_heretada; debris->velocitat_rot = velocitat_ang_heretada;
} }
} else { } else {
debris->velocitat_rot = 0.0F; // Nave: sin curvas 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) { if (factor_herencia_visual > 0.01F && std::abs(velocitat_angular) > 0.01F) {
// Heredar rotación visual con factor proporcional // Heredar rotación visual con factor proporcional
debris->velocitat_rot_visual = debris->velocitat_rot * factor_herencia_visual; 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 = float variacio_visual =
((std::rand() / static_cast<float>(RAND_MAX)) * 0.1F) - 0.05F; ((std::rand() / static_cast<float>(RAND_MAX)) * 0.1F) - 0.05F;
debris->velocitat_rot_visual *= (1.0F + variacio_visual); debris->velocitat_rot_visual *= (1.0F + variacio_visual);
} else { } 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 = debris->velocitat_rot_visual =
Defaults::Physics::Debris::ROTACIO_MIN + Defaults::Physics::Debris::ROTACIO_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) * ((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::ROTACIO_MAX - (Defaults::Physics::Debris::ROTACIO_MAX -
Defaults::Physics::Debris::ROTACIO_MIN)); 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) { if (std::rand() % 2 == 0) {
debris->velocitat_rot_visual = -debris->velocitat_rot_visual; 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->temps_max = Defaults::Physics::Debris::TEMPS_VIDA;
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE; debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
// 8. Heredar brightness // 8. Heredar brightness y color del padre
debris->brightness = brightness; debris->brightness = brightness;
debris->color = color;
// 9. Activar // 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_) { for (auto& debris : debris_pool_) {
if (!debris.actiu) { if (!debris.active) {
continue; continue;
} }
// 1. Actualitzar temps de vida // 1. Actualitzar time de vida
debris.temps_vida += delta_time; 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) { if (debris.temps_vida >= debris.temps_max) {
debris.actiu = false; debris.active = false;
continue; continue;
} }
// 2. Actualitzar velocitat (desacceleració) // 2. Actualitzar velocity (desacceleració)
// Aplicar fricció en la direcció del moviment // Aplicar fricció en la direcció del movement
float speed = std::sqrt((debris.velocitat.x * debris.velocitat.x) + float speed = std::sqrt((debris.velocity.x * debris.velocity.x) +
(debris.velocitat.y * debris.velocitat.y)); (debris.velocity.y * debris.velocity.y));
if (speed > 1.0F) { if (speed > 1.0F) {
// Calcular direcció normalitzada // Calcular direcció normalitzada
float dir_x = debris.velocitat.x / speed; float dir_x = debris.velocity.x / speed;
float dir_y = debris.velocitat.y / speed; float dir_y = debris.velocity.y / speed;
// Aplicar acceleració negativa (fricció) // Aplicar aceleración negativa (fricció)
float nova_speed = speed + (debris.acceleracio * delta_time); float nova_speed = speed + (debris.acceleration * delta_time);
nova_speed = std::max(nova_speed, 0.0F); nova_speed = std::max(nova_speed, 0.0F);
debris.velocitat.x = dir_x * nova_speed; debris.velocity.x = dir_x * nova_speed;
debris.velocitat.y = dir_y * nova_speed; debris.velocity.y = dir_y * nova_speed;
} else { } else {
// Velocitat molt baixa, aturar // Velocidad mucho baixa, aturar
debris.velocitat.x = 0.0F; debris.velocity.x = 0.0F;
debris.velocitat.y = 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) { 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; float dangle = debris.velocitat_rot * delta_time;
// Rotar vector de velocitat usant matriu de rotació 2D // Rotar vector de velocity usant matriu de rotación 2D
float vel_x_old = debris.velocitat.x; float vel_x_old = debris.velocity.x;
float vel_y_old = debris.velocitat.y; float vel_y_old = debris.velocity.y;
float cos_a = std::cos(dangle); float cos_a = std::cos(dangle);
float sin_a = std::sin(dangle); float sin_a = std::sin(dangle);
debris.velocitat.x = (vel_x_old * cos_a) - (vel_y_old * sin_a); debris.velocity.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.y = (vel_x_old * sin_a) + (vel_y_old * cos_a);
} }
// 2c. Aplicar fricció angular (desacceleració gradual) // 2c. Aplicar fricció angular (desacceleració gradual)
@@ -270,80 +272,79 @@ void DebrisManager::actualitzar(float delta_time) {
} }
} }
// 3. Calcular centre del segment // 3. Calcular centro del segment
Punt centre = {.x = (debris.p1.x + debris.p2.x) / 2.0F, Vec2 centro = {.x = (debris.p1.x + debris.p2.x) / 2.0F,
.y = (debris.p1.y + debris.p2.y) / 2.0F}; .y = (debris.p1.y + debris.p2.y) / 2.0F};
// 4. Actualitzar posició del centre // 4. Actualitzar posición del centro
centre.x += debris.velocitat.x * delta_time; centro.x += debris.velocity.x * delta_time;
centre.y += debris.velocitat.y * 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; 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 = float shrink_factor =
1.0F - (debris.factor_shrink * debris.temps_vida / debris.temps_max); 1.0F - (debris.factor_shrink * debris.temps_vida / debris.temps_max);
shrink_factor = std::max(0.0F, shrink_factor); // No negatiu 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 dx = debris.p2.x - debris.p1.x;
float dy = debris.p2.y - debris.p1.y; 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 half_length = std::sqrt((dx * dx) + (dy * dy)) * shrink_factor / 2.0F;
float original_angle = std::atan2(dy, dx); float original_angle = std::atan2(dy, dx);
float new_angle = original_angle + debris.angle_rotacio; float new_angle = original_angle + debris.angle_rotacio;
debris.p1.x = centre.x - (half_length * std::cos(new_angle)); debris.p1.x = centro.x - (half_length * std::cos(new_angle));
debris.p1.y = centre.y - (half_length * std::sin(new_angle)); debris.p1.y = centro.y - (half_length * std::sin(new_angle));
debris.p2.x = centre.x + (half_length * std::cos(new_angle)); debris.p2.x = centro.x + (half_length * std::cos(new_angle));
debris.p2.y = centre.y + (half_length * std::sin(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_) { for (const auto& debris : debris_pool_) {
if (!debris.actiu) { if (!debris.active) {
continue; continue;
} }
// Dibuixar segment de línia amb brightness heretat // Dibujar segmento con brightness y color heredados del padre.
Rendering::linea(renderer_, Rendering::linea(renderer_,
static_cast<int>(debris.p1.x), static_cast<int>(debris.p1.x),
static_cast<int>(debris.p1.y), static_cast<int>(debris.p1.y),
static_cast<int>(debris.p2.x), static_cast<int>(debris.p2.x),
static_cast<int>(debris.p2.y), static_cast<int>(debris.p2.y),
true, debris.brightness, 0.0F, debris.color);
debris.brightness);
} }
} }
Debris* DebrisManager::trobar_slot_lliure() { Debris* DebrisManager::findFreeSlot() {
for (auto& debris : debris_pool_) { for (auto& debris : debris_pool_) {
if (!debris.actiu) { if (!debris.active) {
return &debris; return &debris;
} }
} }
return nullptr; // Pool ple return nullptr; // Pool ple
} }
Punt DebrisManager::calcular_direccio_explosio(const Punt& p1, Vec2 DebrisManager::computeExplosionDirection(const Vec2& p1,
const Punt& p2, const Vec2& p2,
const Punt& centre_objecte) const { const Vec2& centre_objecte) const {
// 1. Calcular centre del segment // 1. Calcular centro del segment
float centro_seg_x = (p1.x + p2.x) / 2.0F; float centro_seg_x = (p1.x + p2.x) / 2.0F;
float centro_seg_y = (p1.y + p2.y) / 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 // 2. Calcular vector des del centro de l'objecte hacia el centro del segment
// Això garanteix que la direcció sempre apunte cap a fora (direcció radial) // Això garanteix que la direcció siempre apunte hacia fuera (direcció radial)
float dx = centro_seg_x - centre_objecte.x; float dx = centro_seg_x - centre_objecte.x;
float dy = centro_seg_y - centre_objecte.y; float dy = centro_seg_y - centre_objecte.y;
// 3. Normalitzar (obtenir vector unitari) // 3. Normalitzar (obtenir vector unitari)
float length = std::sqrt((dx * dx) + (dy * dy)); float length = std::sqrt((dx * dx) + (dy * dy));
if (length < 0.001F) { 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 = float angle_rand =
(std::rand() / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI; (std::rand() / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
return {.x = std::cos(angle_rand), .y = std::sin(angle_rand)}; 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; dx /= length;
dy /= 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 = float angle_variacio =
((std::rand() % 30) - 15) * Defaults::Math::PI / 180.0F; ((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}; return {.x = final_x, .y = final_y};
} }
void DebrisManager::reiniciar() { void DebrisManager::reset() {
for (auto& debris : debris_pool_) { for (auto& debris : debris_pool_) {
debris.actiu = false; debris.active = false;
} }
} }
int DebrisManager::get_num_actius() const { int DebrisManager::getActiveCount() const {
int count = 0; int count = 0;
for (const auto& debris : debris_pool_) { for (const auto& debris : debris_pool_) {
if (debris.actiu) { if (debris.active) {
count++; count++;
} }
} }
+37 -33
View File
@@ -1,72 +1,76 @@
// debris_manager.hpp - Gestor de fragments d'explosions // debris_manager.hpp - Gestor de fragments de explosions
// © 2025 Port a C++20 amb SDL3 // © 2025 Port a C++20 con SDL3
#pragma once #pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <array> #include <array>
#include <memory> #include <memory>
#include "core/defaults.hpp"
#include "core/graphics/shape.hpp" #include "core/graphics/shape.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "debris.hpp" #include "debris.hpp"
namespace Effects { namespace Effects {
// Gestor de fragments d'explosions // Gestor de fragments de explosions
// Manté un pool d'objectes Debris i gestiona el seu cicle de vida // Manté un pool de objectes Debris i gestiona el seu cicle de vida
class DebrisManager { class DebrisManager {
public: public:
explicit DebrisManager(SDL_Renderer* renderer); explicit DebrisManager(Rendering::Renderer* renderer);
// Crear explosió a partir d'una forma // Crear explosión a partir de una shape
// - shape: forma vectorial a explotar // - shape: shape vectorial a explode
// - centre: posició del centre de l'objecte // - centro: posición del centro de l'objecte
// - angle: orientació de l'objecte (radians) // - angle: orientació de l'objecte (radians)
// - escala: escala de l'objecte (1.0 = normal) // - scale: scale de l'objecte (1.0 = normal)
// - velocitat_base: velocitat inicial dels fragments (px/s) // - velocitat_base: velocity inicial dels fragments (px/s)
// - brightness: factor de brillantor heretat (0.0-1.0, per defecte 1.0) // - brightness: factor de brightness heretat (0.0-1.0, per defecte 1.0)
// - velocitat_objecte: velocitat de l'objecte que explota (px/s, per defecte 0) // - velocitat_objecte: velocity de l'objecte que explota (px/s, per defecte 0)
// - velocitat_angular: velocitat angular heretada (rad/s, per defecte 0) // - velocitat_angular: velocity angular heretada (rad/s, per defecte 0)
// - factor_herencia_visual: factor de herència rotació visual (0.0-1.0, per defecte 0.0) // - factor_herencia_visual: factor de herència rotación visual (0.0-1.0, per defecte 0.0)
void explotar(const std::shared_ptr<Graphics::Shape>& shape, void explode(const std::shared_ptr<Graphics::Shape>& shape,
const Punt& centre, const Vec2& centro,
float angle, float angle,
float escala, float scale,
float velocitat_base, float velocitat_base,
float brightness = 1.0F, 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 velocitat_angular = 0.0F,
float factor_herencia_visual = 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 // Actualitzar todos los fragments active
void actualitzar(float delta_time); void update(float delta_time);
// Dibuixar tots els fragments actius // Dibuixar todos los fragments active
void dibuixar() const; void draw() const;
// Reiniciar tots els fragments (neteja) // Reiniciar todos los fragments (clear)
void reiniciar(); void reset();
// Obtenir número de fragments actius // Obtenir número de fragments active
[[nodiscard]] int get_num_actius() const; [[nodiscard]] int getActiveCount() const;
private: private:
SDL_Renderer* renderer_; Rendering::Renderer* renderer_;
// Pool de fragments (màxim concurrent) // Pool de fragments (màxim concurrent)
// Un pentàgon té 5 línies, 15 enemics = 75 línies // Un pentágono té 5 línies, 15 enemigos = 75 línies
// + nau (3 línies) + bales (5 línies * 3) = 93 línies màxim // + ship (3 línies) + balas (5 línies * 3) = 93 línies màxim
// Arrodonit a 100 per seguretat // Arrodonit a 100 per seguretat
static constexpr int MAX_DEBRIS = 150; static constexpr int MAX_DEBRIS = 150;
std::array<Debris, MAX_DEBRIS> debris_pool_; std::array<Debris, MAX_DEBRIS> debris_pool_;
// Trobar primer slot inactiu // Trobar primer slot inactiu
Debris* trobar_slot_lliure(); Debris* findFreeSlot();
// Calcular direcció d'explosió (radial, des del centre cap al segment) // Calcular direcció de explosión (radial, des del centro hacia el segment)
[[nodiscard]] Punt calcular_direccio_explosio(const Punt& p1, const Punt& p2, const Punt& centre_objecte) const; [[nodiscard]] Vec2 computeExplosionDirection(const Vec2& p1, const Vec2& p2, const Vec2& centre_objecte) const;
}; };
} // namespace Effects } // namespace Effects
+33
View File
@@ -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