Compare commits
148 Commits
beta-3.0
...
v1-title-3d
| Author | SHA1 | Date | |
|---|---|---|---|
| d3076fbdec | |||
| 26c6decd74 | |||
| 54702a5afe | |||
| b45390a8d1 | |||
| 2faa3ede84 | |||
| 85e1933a83 | |||
| 07788ab3b6 | |||
| 2ed7463069 | |||
| e533387ce5 | |||
| b654fd0428 | |||
| 7a3a71e1dc | |||
| 8722a46d06 | |||
| e20bdec470 | |||
| 86708e0ed5 | |||
| 51797e0ea7 | |||
| 20f5b83649 | |||
| ffeff3d69d | |||
| a44748c0c4 | |||
| e678f8d538 | |||
| ccda7113c1 | |||
| 5c8a583e24 | |||
| 07985228b2 | |||
| dc389037f8 | |||
| f30b195778 | |||
| 95ac4606d5 | |||
| 2bc07f8e8d | |||
| ca6f863c0f | |||
| 66faa07c00 | |||
| 72158c7c3f | |||
| 8b32a0a404 | |||
| abb7b8fe8c | |||
| 51308fa25e | |||
| 74d855357d | |||
| a9593a0fd9 | |||
| dec72340de | |||
| 7646daef3d | |||
| 1c1fd1273b | |||
| e6eaf870c6 | |||
| 23eff1585c | |||
| 4d51c13e46 | |||
| 625cb19cba | |||
| ae946b578e | |||
| 8b4683b77b | |||
| 0cc1f7623a | |||
| 56ce1a3236 | |||
| 5aab26f2ca | |||
| 2869c63517 | |||
| 87b96b8226 | |||
| 7505de074c | |||
| ae1d1397b1 | |||
| 0c8a9b744e | |||
| 9b25e875f3 | |||
| e84f555a66 | |||
| 048263a1d0 | |||
| efd18ff852 | |||
| 44aa4e76e2 | |||
| e3af88ea8c | |||
| ff5dfab94d | |||
| 2cf5292b16 | |||
| 7b24bfae94 | |||
| 5cb547db0a | |||
| dc2824a095 | |||
| d169a1997c | |||
| 23bcd0816f | |||
| 93baead066 | |||
| bb21191c5b | |||
| 7139dea7f6 | |||
| 08100f60e8 | |||
| 61ae211dab | |||
| 5d1dae1d86 | |||
| 4252f3327f | |||
| 9a79fb9774 | |||
| 6629e9b9aa | |||
| afc91425bc | |||
| 6259f594c8 | |||
| ac5434fc30 | |||
| d1ca0df1ab | |||
| 9eb8c58d87 | |||
| 470d2b85a4 | |||
| 81330f8432 | |||
| 799a97930c | |||
| 1ef9ca551f | |||
| b10f2da647 | |||
| 6063309932 | |||
| 7c2499cd91 | |||
| e0f8cf78ee | |||
| 20cfadeb0b | |||
| cf4fbf7153 | |||
| 329ae7a38e | |||
| 41ce3fece5 | |||
| fdd34eb943 | |||
| d118218662 | |||
| 2f0b148380 | |||
| ecb41cbc3a | |||
| 5f6d51b6cb | |||
| aa0abd9ae1 | |||
| f777017460 | |||
| a0c1c8342f | |||
| 11e9d6569b | |||
| e4b6d2df6a | |||
| 97c98272c9 | |||
| e1d6cd1bb9 | |||
| e3b0958d10 | |||
| 88bb6afab1 | |||
| 707fd29b97 | |||
| 682c27c07c | |||
| 9e54dde490 | |||
| 15bd480d4c | |||
| bbbb8d47ae | |||
| 4e5ab6be1d | |||
| 6d0df85e5e | |||
| c80212adb9 | |||
| 1214599c4c | |||
| 424d0d2b89 | |||
| c45e524109 | |||
| efbf2457a1 | |||
| d3cb93bdba | |||
| e8c253d953 | |||
| b746578bc8 | |||
| 8c251d2246 | |||
| 89a9f06324 | |||
| 0573022b7c | |||
| 5e82dc880f | |||
| a7aecbadd1 | |||
| 6d7060ceb5 | |||
| 5c9f6e6613 | |||
| 808abb28ea | |||
| a4942fcbae | |||
| 816bc02d9d | |||
| 896a899b0f | |||
| e98b87243b | |||
| fa7da4ca58 | |||
| ba6fd00b54 | |||
| 9993b2d98c | |||
| c50ca23135 | |||
| 27242f54fe | |||
| 2fe22ff911 | |||
| 05740775c2 | |||
| 0fd9360029 | |||
| ed98ef612e | |||
| a4f6a5514f | |||
| 56533caff0 | |||
| bf83f161b0 | |||
| 7ee359b910 | |||
| 5871d29d48 | |||
| ae5cc1cfb4 | |||
| cd38101f99 | |||
| 6cf990bc1d |
@@ -9,6 +9,11 @@ Checks:
|
|||||||
- -bugprone-easily-swappable-parameters
|
- -bugprone-easily-swappable-parameters
|
||||||
- -bugprone-narrowing-conversions
|
- -bugprone-narrowing-conversions
|
||||||
- -modernize-avoid-c-arrays
|
- -modernize-avoid-c-arrays
|
||||||
|
# performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1)
|
||||||
|
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
|
||||||
|
# analiza ciertas instanciaciones de std::set. No es un falso positivo
|
||||||
|
# sobre nuestro código: el check ni siquiera llega a evaluar el patrón.
|
||||||
|
- -performance-noexcept-move-constructor
|
||||||
|
|
||||||
WarningsAsErrors: '*'
|
WarningsAsErrors: '*'
|
||||||
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
|
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ if [ ${#CPP_STAGED[@]} -eq 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2
|
echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2
|
||||||
|
# Nota: el path d'inclusió ha d'anar en relatiu. Amb path absolut, cppcheck
|
||||||
|
# falla a parsejar "enum class X : std::uint8_t" (no resol <cstdint> bé) i
|
||||||
|
# emet un syntaxError fals. Els hooks de git s'executen sempre des de la
|
||||||
|
# rel del repo, així que "source" relatiu és prou.
|
||||||
if ! cppcheck \
|
if ! cppcheck \
|
||||||
--enable=warning,style,performance,portability \
|
--enable=warning,style,performance,portability \
|
||||||
--std=c++20 \
|
--std=c++20 \
|
||||||
@@ -81,11 +85,12 @@ if ! cppcheck \
|
|||||||
--suppress='*:*source/external/*' \
|
--suppress='*:*source/external/*' \
|
||||||
--suppress='*:*source/legacy/*' \
|
--suppress='*:*source/legacy/*' \
|
||||||
--suppress=normalCheckLevelMaxBranches \
|
--suppress=normalCheckLevelMaxBranches \
|
||||||
|
--suppress=useStlAlgorithm \
|
||||||
-D_DEBUG \
|
-D_DEBUG \
|
||||||
-DLINUX_BUILD \
|
-DLINUX_BUILD \
|
||||||
--quiet \
|
--quiet \
|
||||||
--error-exitcode=1 \
|
--error-exitcode=1 \
|
||||||
-I "$REPO_ROOT/source" \
|
-I source \
|
||||||
"${CPP_STAGED[@]}"; then
|
"${CPP_STAGED[@]}"; then
|
||||||
echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2
|
echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
+2
-1
@@ -104,4 +104,5 @@ ehthumbs_vista.db
|
|||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
.cache/
|
.cache/
|
||||||
.claude/
|
.claude/lint-reports/
|
||||||
|
lint-reports/
|
||||||
|
|||||||
+47
-3
@@ -3,9 +3,7 @@ project(orni VERSION 0.7.2 LANGUAGES CXX)
|
|||||||
|
|
||||||
# Info del projecte (font de veritat per a project.h)
|
# Info del projecte (font de veritat per a project.h)
|
||||||
set(PROJECT_LONG_NAME "Orni Attack")
|
set(PROJECT_LONG_NAME "Orni Attack")
|
||||||
set(PROJECT_COPYRIGHT_ORIGINAL "© 1999 Visente i Sergi")
|
set(PROJECT_COPYRIGHT "© 2026 JailDesigner")
|
||||||
set(PROJECT_COPYRIGHT_PORT "© 2025 JailDesigner")
|
|
||||||
set(PROJECT_COPYRIGHT "${PROJECT_COPYRIGHT_ORIGINAL}, ${PROJECT_COPYRIGHT_PORT}")
|
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||||
@@ -135,6 +133,51 @@ 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 (headers C++ embedits) ---
|
||||||
|
# Compila els shaders .glsl a SPIR-V i els converteix en headers C++ embedits
|
||||||
|
# (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo:
|
||||||
|
# en macOS no cal glslc (els headers ja existeixen). En Linux/Windows glslc
|
||||||
|
# és obligatori per regenerar els headers en cada canvi del GLSL.
|
||||||
|
#
|
||||||
|
# Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/.
|
||||||
|
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders")
|
||||||
|
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/gpu/spv")
|
||||||
|
set(ALL_SHADER_HEADERS
|
||||||
|
"${HEADERS_DIR}/line_vert_spv.h"
|
||||||
|
"${HEADERS_DIR}/line_frag_spv.h"
|
||||||
|
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||||
|
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||||
|
"${HEADERS_DIR}/bloom_frag_spv.h"
|
||||||
|
)
|
||||||
|
set(ALL_SHADER_SOURCES
|
||||||
|
"${SHADERS_DIR}/line.vert.glsl"
|
||||||
|
"${SHADERS_DIR}/line.frag.glsl"
|
||||||
|
"${SHADERS_DIR}/postfx.vert.glsl"
|
||||||
|
"${SHADERS_DIR}/postfx.frag.glsl"
|
||||||
|
"${SHADERS_DIR}/bloom.frag.glsl"
|
||||||
|
)
|
||||||
|
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
|
||||||
|
if(GLSLC_EXE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${ALL_SHADER_HEADERS}
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
-D GLSLC=${GLSLC_EXE}
|
||||||
|
-D SHADERS_DIR=${SHADERS_DIR}
|
||||||
|
-D HEADERS_DIR=${HEADERS_DIR}
|
||||||
|
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
|
||||||
|
DEPENDS ${ALL_SHADER_SOURCES} ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
|
||||||
|
COMMENT "Compilant shaders GLSL → headers SPIR-V embedits"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
|
||||||
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
|
message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL")
|
||||||
|
elseif(APPLE)
|
||||||
|
message(STATUS "Shaders: glslc no trobat en macOS — s'usaran els headers SPV ja commiteats")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V (obligatori a Linux/Windows)")
|
||||||
|
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)
|
||||||
@@ -221,6 +264,7 @@ if(CPPCHECK_EXE)
|
|||||||
--suppress=*:*source/external/*
|
--suppress=*:*source/external/*
|
||||||
--suppress=*:*source/legacy/*
|
--suppress=*:*source/legacy/*
|
||||||
--suppress=normalCheckLevelMaxBranches
|
--suppress=normalCheckLevelMaxBranches
|
||||||
|
--suppress=useStlAlgorithm
|
||||||
-D_DEBUG
|
-D_DEBUG
|
||||||
-DLINUX_BUILD
|
-DLINUX_BUILD
|
||||||
--quiet
|
--quiet
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 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: separable gaussian blur de dues passes (H + V).
|
||||||
|
# Equivalent matemàtic d'un kernel 15×15 dens (225 mostres) però només cosTa
|
||||||
|
# 30 mostres per píxel. Sense moiré: sigma_px controla l'amplada del halo.
|
||||||
|
bloom:
|
||||||
|
enabled: true
|
||||||
|
intensity: 1.8 # 0..2 — cuanto del bloom se suma a la imagen
|
||||||
|
threshold: 0.20 # 0..1 — luminància mínima que aporta al bloom
|
||||||
|
sigma_px: 5.0 # sigma de la gaussiana en texels (~1.5..6 raonable;
|
||||||
|
# halo ≈ 3·sigma a cada banda. 3.5 → halo de ~10 px)
|
||||||
|
|
||||||
|
# Flicker: modulación global de brillo (efecto fósforo CRT).
|
||||||
|
# Sustituye a la antigua oscilación CPU del ColorOscillator.
|
||||||
|
# Solo afecta a `(lines + bloom)` en el shader; NO toca el fondo, así que
|
||||||
|
# los píxeles negros siguen siendo negros (no pulsan).
|
||||||
|
flicker:
|
||||||
|
enabled: true
|
||||||
|
amplitude: 0.18 # 0..1 — profundidad del flicker
|
||||||
|
frequency_hz: 6.0 # Hz — velocidad de la pulsación
|
||||||
|
|
||||||
|
# Background pulse: color de fondo oscilante (suma aditiva).
|
||||||
|
# Desactivado: fondo negro puro. Se mantienen los valores por si queremos
|
||||||
|
# reactivar más adelante un tinte verdoso muy tenue al estilo CRT.
|
||||||
|
background:
|
||||||
|
enabled: false
|
||||||
|
color_min: [0, 0, 0] # negro puro
|
||||||
|
color_max: [0, 0, 0] # negro puro
|
||||||
|
pulse_frequency_hz: 6.0 # Hz — sincronizado con flicker por defecto
|
||||||
+1
-17
@@ -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,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,5 +1,5 @@
|
|||||||
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
|
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
|
||||||
# © 2025 Port a C++20 amb SDL3
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: enemy_pinwheel
|
name: enemy_pinwheel
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
|
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
|
||||||
# © 2025 Orni Attack - Jugador 2
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: ship2
|
name: ship2
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# star.shp - Estrella per a starfield
|
# star.shp - Estrella per a starfield
|
||||||
# © 2025 Orni Attack
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: star
|
name: star
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14
-14
@@ -1,5 +1,5 @@
|
|||||||
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
|
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
|
||||||
# © 2025 Orni Attack
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
@@ -9,14 +9,14 @@ metadata:
|
|||||||
stages:
|
stages:
|
||||||
# STAGE 1: Tutorial - Only pentagons, slow speed
|
# STAGE 1: Tutorial - Only pentagons, slow speed
|
||||||
- stage_id: 1
|
- stage_id: 1
|
||||||
total_enemies: 5
|
total_enemies: 50
|
||||||
spawn_config:
|
spawn_config:
|
||||||
mode: "progressive"
|
mode: "progressive"
|
||||||
initial_delay: 2.0
|
initial_delay: 0.3
|
||||||
spawn_interval: 3.0
|
spawn_interval: 0.4
|
||||||
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
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 1999 Visente i Sergi, 2025 Port</string>
|
<string>© 2026 JailDesigner</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>SUPublicDSAKeyFile</key>
|
<key>SUPublicDSAKeyFile</key>
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Fragment shader del bloom: una passada 1D de blur gaussià separable, amb
|
||||||
|
// high-pass opcional. Es crida dues vegades per frame:
|
||||||
|
//
|
||||||
|
// Pass H: extract=1.0, direction=(1,0). Llegeix l'escena offscreen i
|
||||||
|
// emet a bloom_texture_a aplicant high-pass + gaussiana horitzontal.
|
||||||
|
// Pass V: extract=0.0, direction=(0,1). Llegeix bloom_texture_a i emet
|
||||||
|
// a bloom_texture_b amb la gaussiana vertical (sense high-pass).
|
||||||
|
//
|
||||||
|
// Resultat: equivalent matemàtic d'una convolució 2D de 15×15 mostres denses,
|
||||||
|
// però només costa 2×15 = 30 mostres per píxel. Sense moiré (samples a
|
||||||
|
// distància 1 texel, així que la gaussiana és contínua a l'escala del píxel).
|
||||||
|
//
|
||||||
|
// El paràmetre `sigma` (en texels) controla l'amplada del halo. Per a sigma=4,
|
||||||
|
// el halo cobreix ~12 texels al voltant de cada línia. Pujar sigma engreixa
|
||||||
|
// el halo; cal mantenir-lo ≤ ~5-6 perquè el rang de mostreig (±7 taps) cobreixi
|
||||||
|
// el 99% del gaussià.
|
||||||
|
//
|
||||||
|
// Recursos:
|
||||||
|
// set=2, binding=0 → sampler2D (input)
|
||||||
|
// set=3, binding=0 → uniform buffer (paràmetres)
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D src;
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform BloomUBO {
|
||||||
|
vec2 texel_size; // 1.0 / texture_size
|
||||||
|
vec2 direction; // (1,0) per pass H, (0,1) per pass V
|
||||||
|
float threshold; // luminància mínima per al high-pass
|
||||||
|
float extract; // 1.0 = aplica high-pass (pass H), 0.0 = blur pur (pass V)
|
||||||
|
float sigma; // sigma de la gaussiana en texels
|
||||||
|
float _pad;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 v_uv;
|
||||||
|
layout(location = 0) out vec4 frag;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 sum = vec3(0.0);
|
||||||
|
float total_weight = 0.0;
|
||||||
|
|
||||||
|
// 15 taps: -7..+7, espaiats 1 texel. Cobreix ±7 texels = ±~2σ per σ=3.5.
|
||||||
|
// Per σ més grans, el cua es retalla una mica però el peso del tap 7 ja és
|
||||||
|
// molt baix; visualment no es nota.
|
||||||
|
const int RADIUS = 7;
|
||||||
|
const float TWO_SIGMA_SQ_FACTOR = 2.0; // multiplicador per a 2σ² al denominador
|
||||||
|
|
||||||
|
for (int i = -RADIUS; i <= RADIUS; ++i) {
|
||||||
|
vec2 offset = ubo.direction * float(i) * ubo.texel_size;
|
||||||
|
vec3 c = texture(src, v_uv + offset).rgb;
|
||||||
|
|
||||||
|
// High-pass només a la primera passada: a la segona, c ja és el
|
||||||
|
// resultat de la H i no l'hem de tornar a filtrar.
|
||||||
|
if (ubo.extract > 0.5) {
|
||||||
|
float luma = max(c.r, max(c.g, c.b));
|
||||||
|
float high_pass = max(0.0, luma - ubo.threshold);
|
||||||
|
c *= high_pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
float fi = float(i);
|
||||||
|
float w = exp(-(fi * fi) / (TWO_SIGMA_SQ_FACTOR * ubo.sigma * ubo.sigma));
|
||||||
|
sum += c * w;
|
||||||
|
total_weight += w;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total_weight > 0.0) {
|
||||||
|
sum /= total_weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
frag = vec4(sum, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Fragment shader per a línies vectorials.
|
||||||
|
//
|
||||||
|
// Antialias geomètric: rebem `frag_edge_dist` interpolat (±1 als laterals del
|
||||||
|
// quad, 0 a l'eix central). Apliquem un smoothstep d'1 píxel d'amplada perquè
|
||||||
|
// el gruix nominal (els |edge_dist| < threshold) quedi totalment opac i només
|
||||||
|
// el píxel extruit als laterals faci la transició suau.
|
||||||
|
//
|
||||||
|
// La línia ja ve extruïda amb thickness + 1px a CPU; el threshold equival a
|
||||||
|
// (thickness)/(thickness+1), però no el coneixem aquí per vèrtex. En el cas
|
||||||
|
// general (línies fines), fade lineal entre 0.0 i 1.0 dóna prou bon resultat
|
||||||
|
// visualment sense necessitat d'un uniform per línia.
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 frag_color;
|
||||||
|
layout(location = 1) in float frag_edge_dist;
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// |edge_dist|=0 → totalment opac; |edge_dist|=1 → totalment transparent.
|
||||||
|
// smoothstep dóna un fade Hermite C¹ que evita banding.
|
||||||
|
float d = abs(frag_edge_dist);
|
||||||
|
float alpha = 1.0 - smoothstep(0.7, 1.0, d);
|
||||||
|
out_color = vec4(frag_color.rgb, frag_color.a * alpha);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#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 = 2) in float in_edge_dist; // ±1 als laterals, 0 al centre
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 frag_color;
|
||||||
|
layout(location = 1) out float frag_edge_dist;
|
||||||
|
|
||||||
|
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;
|
||||||
|
frag_edge_dist = in_edge_dist;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Fragment shader del pase final de composite.
|
||||||
|
// Llegeix dos samplers: l'escena vectorial i el bloom ja pre-calculat (resultat
|
||||||
|
// del separable blur de dues passes a bloom.frag.glsl). Aplica:
|
||||||
|
// 1. Mescla del bloom amb la intensitat configurada.
|
||||||
|
// 2. Flicker: multiplicador global de brillo modulat per temps.
|
||||||
|
// 3. Background pulse: color de fons additiu que oscil·la entre min/max.
|
||||||
|
//
|
||||||
|
// L'arquitectura anterior tenia el bloom inline (kernel 7×7 single-pass), que
|
||||||
|
// produïa moiré per radis grans. Ara el bloom és pre-computed via separable
|
||||||
|
// gaussian (equivalent a kernel 15×15 dens) i aquí només cal samplejar-lo.
|
||||||
|
//
|
||||||
|
// Resource sets (SDL_gpu):
|
||||||
|
// set=2, binding=0 → sampler2D (escena offscreen)
|
||||||
|
// set=2, binding=1 → sampler2D (bloom pre-calculat)
|
||||||
|
// set=3, binding=0 → uniform buffer (paràmetres del postpro)
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||||
|
layout(set = 2, binding = 1) uniform sampler2D bloom_tex;
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform PostFxUBO {
|
||||||
|
float time;
|
||||||
|
float bloom_intensity;
|
||||||
|
float flicker_amplitude;
|
||||||
|
float flicker_frequency_hz;
|
||||||
|
|
||||||
|
float background_pulse_freq_hz;
|
||||||
|
float _pad_a;
|
||||||
|
float _pad_b;
|
||||||
|
float _pad_c;
|
||||||
|
|
||||||
|
vec4 background_min; // RGB en [0..1], A=1
|
||||||
|
vec4 background_max; // RGB en [0..1], A=1
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 v_uv;
|
||||||
|
layout(location = 0) out vec4 frag;
|
||||||
|
|
||||||
|
const float TAU = 6.28318530718;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 src = texture(scene, v_uv).rgb;
|
||||||
|
vec3 bloom = texture(bloom_tex, v_uv).rgb * ubo.bloom_intensity;
|
||||||
|
|
||||||
|
// === FLICKER ===
|
||||||
|
// Multiplicador global de brillo. Oscil·la entre (1.0 - amplitude) i 1.0.
|
||||||
|
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 ===
|
||||||
|
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Ó (preserve-core) ===
|
||||||
|
// Bloom additiu però atenuat per (1 - luma_src): no contribueix als píxels
|
||||||
|
// on la línia ja és brillant (manté el color del core sense rentar-lo cap a
|
||||||
|
// blanc) i contribueix al màxim als píxels foscos del voltant (halo intens).
|
||||||
|
// El flicker només multiplica (línies + bloom); el fons va a banda perquè
|
||||||
|
// els píxels foscos no han de pulsar.
|
||||||
|
float src_luma = max(src.r, max(src.g, src.b));
|
||||||
|
vec3 bloom_contribution = bloom * (1.0 - src_luma);
|
||||||
|
vec3 lines_and_glow = (src + bloom_contribution) * flicker;
|
||||||
|
frag = vec4(background + lines_and_glow, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Vertex shader del pase de postprocesado.
|
||||||
|
// Emite un solo triángulo que cubre toda la pantalla (técnica del "fullscreen
|
||||||
|
// triangle"): tres vértices en (-1,-1), (3,-1), (-1,3) → la región visible
|
||||||
|
// [-1..1]² queda completamente cubierta y el clip recorta el resto. No hace
|
||||||
|
// falta vertex buffer; el draw es DrawPrimitives(vertex_count=3).
|
||||||
|
|
||||||
|
layout(location = 0) out vec2 v_uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 positions[3] = vec2[3](
|
||||||
|
vec2(-1.0, -1.0),
|
||||||
|
vec2( 3.0, -1.0),
|
||||||
|
vec2(-1.0, 3.0)
|
||||||
|
);
|
||||||
|
// UV.y invertida para compensar la diferencia entre la convención de
|
||||||
|
// clip-space del line shader (ndc.y flipeado, GL-style) y la convención
|
||||||
|
// de muestreo de SDL_gpu/Vulkan (origen de textura en top-left). Sin esta
|
||||||
|
// inversión, el offscreen se ve cabeza-abajo en el composite.
|
||||||
|
vec2 uvs[3] = vec2[3](
|
||||||
|
vec2(0.0, 1.0),
|
||||||
|
vec2(2.0, 1.0),
|
||||||
|
vec2(0.0, -1.0)
|
||||||
|
);
|
||||||
|
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||||
|
v_uv = uvs[gl_VertexIndex];
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Deshabilitar clang-tidy para este directorio (código externo: jail_audio.hpp)
|
|
||||||
# Los demás archivos de este directorio (audio.cpp, audio_cache.cpp) también se benefician
|
|
||||||
# de no ser modificados porque dependen íntimamente de la API de jail_audio.hpp
|
|
||||||
|
|
||||||
Checks: '-*'
|
|
||||||
+217
-109
@@ -1,183 +1,291 @@
|
|||||||
#include "audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
|
|
||||||
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
|
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
|
||||||
|
|
||||||
#include <algorithm> // Para clamp
|
#include <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);
|
||||||
}
|
}
|
||||||
+142
-77
@@ -1,97 +1,162 @@
|
|||||||
#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
|
||||||
|
|
||||||
// --- 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 ConfigYaml::*. Això deixa audio.cpp independent
|
||||||
|
// del layout d'Options i permet substituir la font de configuración.
|
||||||
|
//
|
||||||
|
// Els volums es manegen internament como a float 0.0–1.0; la capa de
|
||||||
|
// presentació (menús, notificacions) usa las helpers toPercent/fromPercent
|
||||||
|
// per mostrar 0–100 a l'usuari.
|
||||||
class Audio {
|
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 la música
|
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
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// audio_adapter.cpp - Implementación de AudioResource para orni_attack
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
auto musicCache() -> std::unordered_map<std::string, std::unique_ptr<Ja::Music>>& {
|
||||||
|
static std::unordered_map<std::string, std::unique_ptr<Ja::Music>> cache_;
|
||||||
|
return cache_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto soundCache() -> std::unordered_map<std::string, std::unique_ptr<Ja::Sound>>& {
|
||||||
|
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"
|
||||||
|
auto normalizeMusicPath(const std::string& name) -> std::string {
|
||||||
|
return (name.starts_with("music/")) ? name : "music/" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto normalizeSoundPath(const std::string& name) -> std::string {
|
||||||
|
return (name.starts_with("sounds/")) ? name : "sounds/" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace AudioResource {
|
||||||
|
|
||||||
|
auto getMusic(const std::string& name) -> Ja::Music* {
|
||||||
|
auto& cache = musicCache();
|
||||||
|
if (auto it = cache.find(name); it != cache.end()) {
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string PATH = normalizeMusicPath(name);
|
||||||
|
auto bytes = Resource::Helper::loadFile(PATH);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
std::cerr << "[AudioResource] no se ha podido cargar música: " << PATH << "\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ja::Music* raw = Ja::loadMusic(bytes.data(), static_cast<std::uint32_t>(bytes.size()), name.c_str());
|
||||||
|
if (raw == nullptr) {
|
||||||
|
std::cerr << "[AudioResource] decodificación de música falló: " << PATH << "\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.emplace(name, std::unique_ptr<Ja::Music>(raw));
|
||||||
|
std::cout << "[AudioResource] música cargada: " << PATH << "\n";
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto getSound(const std::string& name) -> Ja::Sound* {
|
||||||
|
auto& cache = soundCache();
|
||||||
|
if (auto it = cache.find(name); it != cache.end()) {
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string PATH = normalizeSoundPath(name);
|
||||||
|
auto bytes = Resource::Helper::loadFile(PATH);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
std::cerr << "[AudioResource] no se ha podido cargar sonido: " << PATH << "\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ja::Sound* raw = Ja::loadSound(bytes.data(), static_cast<std::uint32_t>(bytes.size()));
|
||||||
|
if (raw == nullptr) {
|
||||||
|
std::cerr << "[AudioResource] decodificación de sonido falló: " << PATH << "\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.emplace(name, std::unique_ptr<Ja::Sound>(raw));
|
||||||
|
std::cout << "[AudioResource] sonido cargado: " << PATH << "\n";
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace AudioResource
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// --- Audio Resource Adapter ---
|
||||||
|
// Este archivo exposa una interfície comuna a Audio per obtenir Ja::Music* /
|
||||||
|
// Ja::Sound* per nom. Cada projecte la implementa en audio_adapter.cpp delegant
|
||||||
|
// al seu singleton de recursos (Resource::Cache::get(), ...). Así audio.hpp
|
||||||
|
// i audio.cpp es poden compartir entre projectes.
|
||||||
|
|
||||||
|
#include <string> // Para string
|
||||||
|
|
||||||
|
namespace Ja {
|
||||||
|
struct Music;
|
||||||
|
struct Sound;
|
||||||
|
} // namespace Ja
|
||||||
|
|
||||||
|
namespace AudioResource {
|
||||||
|
auto getMusic(const std::string& name) -> Ja::Music*;
|
||||||
|
auto getSound(const std::string& name) -> Ja::Sound*;
|
||||||
|
} // namespace AudioResource
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// audio_cache.cpp - Implementació del caché de sons i música
|
|
||||||
// © 2025 Port a C++20 amb SDL3
|
|
||||||
|
|
||||||
#include "core/audio/audio_cache.hpp"
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
|
||||||
|
|
||||||
// Inicialització de variables estàtiques
|
|
||||||
std::unordered_map<std::string, JA_Sound_t*> AudioCache::sounds_;
|
|
||||||
std::unordered_map<std::string, JA_Music_t*> AudioCache::musics_;
|
|
||||||
std::string AudioCache::sounds_base_path_ = "data/sounds/";
|
|
||||||
std::string AudioCache::music_base_path_ = "data/music/";
|
|
||||||
|
|
||||||
JA_Sound_t* AudioCache::getSound(const std::string& name) {
|
|
||||||
// Cache hit
|
|
||||||
auto it = sounds_.find(name);
|
|
||||||
if (it != sounds_.end()) {
|
|
||||||
std::cout << "[AudioCache] Sound cache hit: " << name << std::endl;
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize path: "laser_shoot.wav" → "sounds/laser_shoot.wav"
|
|
||||||
std::string normalized = name;
|
|
||||||
if (normalized.find("sounds/") != 0) {
|
|
||||||
normalized = "sounds/" + normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load from resource system
|
|
||||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
|
||||||
if (data.empty()) {
|
|
||||||
std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << normalized << std::endl;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load sound from memory
|
|
||||||
JA_Sound_t* sound = JA_LoadSound(data.data(), static_cast<uint32_t>(data.size()));
|
|
||||||
if (sound == nullptr) {
|
|
||||||
std::cerr << "[AudioCache] Error: no s'ha pogut decodificar " << normalized
|
|
||||||
<< std::endl;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "[AudioCache] Sound loaded: " << normalized << std::endl;
|
|
||||||
sounds_[name] = sound;
|
|
||||||
return sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
JA_Music_t* AudioCache::getMusic(const std::string& name) {
|
|
||||||
// Cache hit
|
|
||||||
auto it = musics_.find(name);
|
|
||||||
if (it != musics_.end()) {
|
|
||||||
std::cout << "[AudioCache] Music cache hit: " << name << std::endl;
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize path: "title.ogg" → "music/title.ogg"
|
|
||||||
std::string normalized = name;
|
|
||||||
if (normalized.find("music/") != 0) {
|
|
||||||
normalized = "music/" + normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load from resource system
|
|
||||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
|
||||||
if (data.empty()) {
|
|
||||||
std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << normalized << std::endl;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load music from memory
|
|
||||||
JA_Music_t* music = JA_LoadMusic(data.data(), static_cast<uint32_t>(data.size()));
|
|
||||||
if (music == nullptr) {
|
|
||||||
std::cerr << "[AudioCache] Error: no s'ha pogut decodificar " << normalized
|
|
||||||
<< std::endl;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "[AudioCache] Music loaded: " << normalized << std::endl;
|
|
||||||
musics_[name] = music;
|
|
||||||
return music;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioCache::clear() {
|
|
||||||
std::cout << "[AudioCache] Clearing cache (" << sounds_.size() << " sounds, "
|
|
||||||
<< musics_.size() << " music)" << std::endl;
|
|
||||||
|
|
||||||
// Liberar memoria de sonidos
|
|
||||||
for (auto& [name, sound] : sounds_) {
|
|
||||||
if (sound && sound->buffer) {
|
|
||||||
SDL_free(sound->buffer);
|
|
||||||
}
|
|
||||||
delete sound;
|
|
||||||
}
|
|
||||||
sounds_.clear();
|
|
||||||
|
|
||||||
// Liberar memoria de música
|
|
||||||
for (auto& [name, music] : musics_) {
|
|
||||||
if (music && music->buffer) {
|
|
||||||
SDL_free(music->buffer);
|
|
||||||
}
|
|
||||||
if (music && music->filename) {
|
|
||||||
free(music->filename);
|
|
||||||
}
|
|
||||||
delete music;
|
|
||||||
}
|
|
||||||
musics_.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t AudioCache::getSoundCacheSize() { return sounds_.size(); }
|
|
||||||
|
|
||||||
size_t AudioCache::getMusicCacheSize() { return musics_.size(); }
|
|
||||||
|
|
||||||
std::string AudioCache::resolveSoundPath(const std::string& name) {
|
|
||||||
// Si es un path absoluto (comienza con '/'), usarlo directamente
|
|
||||||
if (!name.empty() && name[0] == '/') {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si ya contiene el prefix base_path, usarlo directamente
|
|
||||||
if (name.find(sounds_base_path_) == 0) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caso contrario, añadir base_path
|
|
||||||
return sounds_base_path_ + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string AudioCache::resolveMusicPath(const std::string& name) {
|
|
||||||
// Si es un path absoluto (comienza con '/'), usarlo directamente
|
|
||||||
if (!name.empty() && name[0] == '/') {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si ya contiene el prefix base_path, usarlo directamente
|
|
||||||
if (name.find(music_base_path_) == 0) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caso contrario, añadir base_path
|
|
||||||
return music_base_path_ + name;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// audio_cache.hpp - Caché simplificado de sonidos y música
|
|
||||||
// © 2025 Port a C++20 amb SDL3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include "core/audio/jail_audio.hpp"
|
|
||||||
|
|
||||||
// Caché estático de sonidos y música
|
|
||||||
// Patrón inspirado en Graphics::ShapeLoader
|
|
||||||
class AudioCache {
|
|
||||||
public:
|
|
||||||
// No instanciable (todo estático)
|
|
||||||
AudioCache() = delete;
|
|
||||||
|
|
||||||
// Obtener sonido (carga bajo demanda)
|
|
||||||
// Retorna puntero (nullptr si error)
|
|
||||||
static JA_Sound_t* getSound(const std::string& name);
|
|
||||||
|
|
||||||
// Obtener música (carga bajo demanda)
|
|
||||||
// Retorna puntero (nullptr si error)
|
|
||||||
static JA_Music_t* getMusic(const std::string& name);
|
|
||||||
|
|
||||||
// Limpiar caché (útil para debug/recarga)
|
|
||||||
static void clear();
|
|
||||||
|
|
||||||
// Estadísticas (debug)
|
|
||||||
static size_t getSoundCacheSize();
|
|
||||||
static size_t getMusicCacheSize();
|
|
||||||
|
|
||||||
private:
|
|
||||||
static std::unordered_map<std::string, JA_Sound_t*> sounds_;
|
|
||||||
static std::unordered_map<std::string, JA_Music_t*> musics_;
|
|
||||||
static std::string sounds_base_path_; // "data/sounds/"
|
|
||||||
static std::string music_base_path_; // "data/music/"
|
|
||||||
|
|
||||||
// Helpers privados
|
|
||||||
static std::string resolveSoundPath(const std::string& name);
|
|
||||||
static std::string resolveMusicPath(const std::string& name);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
#include "core/audio/audio_effects.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "core/audio/jail_audio.hpp"
|
||||||
|
|
||||||
|
namespace AudioEffects {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// --- Caps de cua ---
|
||||||
|
constexpr float ECHO_TAIL_MS = 800.0F;
|
||||||
|
constexpr float REVERB_TAIL_MS = 1500.0F;
|
||||||
|
|
||||||
|
// --- Constants Freeverb ---
|
||||||
|
// Delays de comb i allpass tunats para 44.1 kHz; los reescalem per
|
||||||
|
// freqüència real de la font.
|
||||||
|
constexpr int COMB_REFERENCE_RATE = 44100;
|
||||||
|
constexpr std::array<int, 8> COMB_DELAYS_L = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617};
|
||||||
|
constexpr std::array<int, 4> ALLPASS_DELAYS_L = {556, 441, 341, 225};
|
||||||
|
constexpr int STEREO_SPREAD = 23;
|
||||||
|
|
||||||
|
// Mapeig de Schroeder/Dattorro/Freeverb estàndard.
|
||||||
|
constexpr float FIXED_GAIN = 0.015F;
|
||||||
|
constexpr float SCALE_ROOM = 0.28F;
|
||||||
|
constexpr float OFFSET_ROOM = 0.7F;
|
||||||
|
constexpr float SCALE_DAMP = 0.4F;
|
||||||
|
|
||||||
|
// --- Decodificació a float -1..1 ---
|
||||||
|
// Suporta U8/S16, mono/estèreo. Mono es duplica a L i R (la cadena
|
||||||
|
// d'efectes treballa siempre con dos canals per simplicitat).
|
||||||
|
auto decodeToStereoFloat(const Ja::Sound& src, std::vector<float>& left, std::vector<float>& right) -> bool {
|
||||||
|
const auto& spec = src.spec;
|
||||||
|
const Uint8* buf = src.buffer.get();
|
||||||
|
if (buf == nullptr || src.length == 0) { return false; }
|
||||||
|
|
||||||
|
int bytes_per_sample = 0;
|
||||||
|
if (spec.format == SDL_AUDIO_S16) {
|
||||||
|
bytes_per_sample = 2;
|
||||||
|
} else if (spec.format == SDL_AUDIO_U8) {
|
||||||
|
bytes_per_sample = 1;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[AudioEffects] formato de sonido no soportado (solo U8 o S16)\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (spec.channels < 1 || spec.channels > 2) {
|
||||||
|
std::cerr << "[AudioEffects] el sonido debe ser mono o estéreo\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t TOTAL_FRAMES = src.length / static_cast<std::size_t>(bytes_per_sample * spec.channels);
|
||||||
|
left.resize(TOTAL_FRAMES);
|
||||||
|
right.resize(TOTAL_FRAMES);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < TOTAL_FRAMES; ++i) {
|
||||||
|
float sample_l = 0.0F;
|
||||||
|
float sample_r = 0.0F;
|
||||||
|
if (spec.format == SDL_AUDIO_S16) {
|
||||||
|
const auto* p = reinterpret_cast<const std::int16_t*>(buf + (i * spec.channels * 2));
|
||||||
|
sample_l = static_cast<float>(p[0]) / 32768.0F;
|
||||||
|
sample_r = (spec.channels == 2) ? static_cast<float>(p[1]) / 32768.0F : sample_l;
|
||||||
|
} else { // U8
|
||||||
|
const Uint8* p = buf + (i * spec.channels);
|
||||||
|
sample_l = (static_cast<float>(p[0]) - 128.0F) / 128.0F;
|
||||||
|
sample_r = (spec.channels == 2) ? (static_cast<float>(p[1]) - 128.0F) / 128.0F : sample_l;
|
||||||
|
}
|
||||||
|
left[i] = sample_l;
|
||||||
|
right[i] = sample_r;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empaqueta dos canals float (-1..1) a S16 entrellaçat.
|
||||||
|
void encodeStereoS16(const std::vector<float>& left, const std::vector<float>& right, std::vector<std::uint8_t>& out) {
|
||||||
|
const std::size_t LEN = left.size();
|
||||||
|
out.resize(LEN * 2 * sizeof(std::int16_t));
|
||||||
|
auto* dst = reinterpret_cast<std::int16_t*>(out.data());
|
||||||
|
for (std::size_t i = 0; i < LEN; ++i) {
|
||||||
|
const float L = std::clamp(left[i], -1.0F, 1.0F);
|
||||||
|
const float R = std::clamp(right[i], -1.0F, 1.0F);
|
||||||
|
dst[(i * 2) + 0] = static_cast<std::int16_t>(std::lround(L * 32767.0F));
|
||||||
|
dst[(i * 2) + 1] = static_cast<std::int16_t>(std::lround(R * 32767.0F));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reescala un delay de la taula de Freeverb para la freqüència real.
|
||||||
|
auto scaledDelay(int reference_delay, int rate) -> int {
|
||||||
|
const long SCALED = std::lround(static_cast<double>(reference_delay) * static_cast<double>(rate) / static_cast<double>(COMB_REFERENCE_RATE));
|
||||||
|
return std::max(1, static_cast<int>(SCALED));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Filtres bàsics ---
|
||||||
|
struct Comb {
|
||||||
|
std::vector<float> buf;
|
||||||
|
std::size_t idx{0};
|
||||||
|
float feedback{0.0F};
|
||||||
|
float damp1{0.0F};
|
||||||
|
float damp2{1.0F};
|
||||||
|
float store{0.0F};
|
||||||
|
|
||||||
|
void init(int delay, float fb, float damping) {
|
||||||
|
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||||
|
idx = 0;
|
||||||
|
feedback = fb;
|
||||||
|
damp1 = damping;
|
||||||
|
damp2 = 1.0F - damping;
|
||||||
|
store = 0.0F;
|
||||||
|
}
|
||||||
|
auto tick(float in) -> float {
|
||||||
|
const float OUT = buf[idx];
|
||||||
|
store = (OUT * damp2) + (store * damp1);
|
||||||
|
buf[idx] = in + (store * feedback);
|
||||||
|
idx = (idx + 1) % buf.size();
|
||||||
|
return OUT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Allpass {
|
||||||
|
std::vector<float> buf;
|
||||||
|
std::size_t idx{0};
|
||||||
|
|
||||||
|
void init(int delay) {
|
||||||
|
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||||
|
idx = 0;
|
||||||
|
}
|
||||||
|
auto tick(float in) -> float {
|
||||||
|
const float BUFOUT = buf[idx];
|
||||||
|
const float OUT = -in + BUFOUT;
|
||||||
|
buf[idx] = in + (BUFOUT * 0.5F);
|
||||||
|
idx = (idx + 1) % buf.size();
|
||||||
|
return OUT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound> {
|
||||||
|
std::vector<float> left;
|
||||||
|
std::vector<float> right;
|
||||||
|
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||||
|
|
||||||
|
const int RATE = src.spec.freq;
|
||||||
|
const int DELAY_SAMPLES = std::max(1, static_cast<int>(std::lround(params.delay_ms * 0.001F * static_cast<float>(RATE))));
|
||||||
|
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(ECHO_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||||
|
|
||||||
|
const float FEEDBACK = std::clamp(params.feedback, 0.0F, 0.95F);
|
||||||
|
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||||
|
const float DRY = 1.0F - WET;
|
||||||
|
|
||||||
|
const std::size_t INPUT_LEN = left.size();
|
||||||
|
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||||
|
|
||||||
|
std::vector<float> ring_l(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||||
|
std::vector<float> ring_r(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||||
|
std::size_t cursor = 0;
|
||||||
|
|
||||||
|
std::vector<float> out_l(TOTAL_LEN);
|
||||||
|
std::vector<float> out_r(TOTAL_LEN);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||||
|
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||||
|
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||||
|
|
||||||
|
const float DELAYED_L = ring_l[cursor];
|
||||||
|
const float DELAYED_R = ring_r[cursor];
|
||||||
|
|
||||||
|
out_l[i] = (DRY * IN_L) + (WET * DELAYED_L);
|
||||||
|
out_r[i] = (DRY * IN_R) + (WET * DELAYED_R);
|
||||||
|
|
||||||
|
ring_l[cursor] = IN_L + (DELAYED_L * FEEDBACK);
|
||||||
|
ring_r[cursor] = IN_R + (DELAYED_R * FEEDBACK);
|
||||||
|
cursor = (cursor + 1) % static_cast<std::size_t>(DELAY_SAMPLES);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessedSound result;
|
||||||
|
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||||
|
encodeStereoS16(out_l, out_r, result.bytes);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound> {
|
||||||
|
std::vector<float> left;
|
||||||
|
std::vector<float> right;
|
||||||
|
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||||
|
|
||||||
|
const int RATE = src.spec.freq;
|
||||||
|
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(REVERB_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||||
|
|
||||||
|
const float ROOM_SIZE = std::clamp(params.room_size, 0.0F, 1.0F);
|
||||||
|
const float DAMPING = std::clamp(params.damping, 0.0F, 1.0F);
|
||||||
|
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||||
|
const float DRY = 1.0F - WET;
|
||||||
|
|
||||||
|
const float FEEDBACK = (ROOM_SIZE * SCALE_ROOM) + OFFSET_ROOM; // 0.7..0.98
|
||||||
|
const float DAMP1 = DAMPING * SCALE_DAMP; // 0..0.4
|
||||||
|
|
||||||
|
// Inicialitza los 8 comb filters per cada canal i los 4 allpass.
|
||||||
|
std::array<Comb, 8> comb_l;
|
||||||
|
std::array<Comb, 8> comb_r;
|
||||||
|
for (std::size_t i = 0; i < COMB_DELAYS_L.size(); ++i) {
|
||||||
|
comb_l[i].init(scaledDelay(COMB_DELAYS_L[i], RATE), FEEDBACK, DAMP1);
|
||||||
|
comb_r[i].init(scaledDelay(COMB_DELAYS_L[i] + STEREO_SPREAD, RATE), FEEDBACK, DAMP1);
|
||||||
|
}
|
||||||
|
std::array<Allpass, 4> allpass_l;
|
||||||
|
std::array<Allpass, 4> allpass_r;
|
||||||
|
for (std::size_t i = 0; i < ALLPASS_DELAYS_L.size(); ++i) {
|
||||||
|
allpass_l[i].init(scaledDelay(ALLPASS_DELAYS_L[i], RATE));
|
||||||
|
allpass_r[i].init(scaledDelay(ALLPASS_DELAYS_L[i] + STEREO_SPREAD, RATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t INPUT_LEN = left.size();
|
||||||
|
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||||
|
std::vector<float> out_l(TOTAL_LEN);
|
||||||
|
std::vector<float> out_r(TOTAL_LEN);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||||
|
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||||
|
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||||
|
const float MONO_INPUT = (IN_L + IN_R) * FIXED_GAIN;
|
||||||
|
|
||||||
|
// 8 comb filters en paral·lel, sumats.
|
||||||
|
float wet_l = 0.0F;
|
||||||
|
float wet_r = 0.0F;
|
||||||
|
for (std::size_t k = 0; k < comb_l.size(); ++k) {
|
||||||
|
wet_l += comb_l[k].tick(MONO_INPUT);
|
||||||
|
wet_r += comb_r[k].tick(MONO_INPUT);
|
||||||
|
}
|
||||||
|
// 4 allpass en sèrie.
|
||||||
|
for (std::size_t k = 0; k < allpass_l.size(); ++k) {
|
||||||
|
wet_l = allpass_l[k].tick(wet_l);
|
||||||
|
wet_r = allpass_r[k].tick(wet_r);
|
||||||
|
}
|
||||||
|
|
||||||
|
out_l[i] = (DRY * IN_L) + (WET * wet_l);
|
||||||
|
out_r[i] = (DRY * IN_R) + (WET * wet_r);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessedSound result;
|
||||||
|
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||||
|
encodeStereoS16(out_l, out_r, result.bytes);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace AudioEffects
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Forward-declaració per no incloure jail_audio.hpp (cicle d'inclusió: este
|
||||||
|
// header viu sota los params declarats a jail_audio.hpp, i alhora jail_audio
|
||||||
|
// usa applyEcho/applyReverb).
|
||||||
|
namespace Ja {
|
||||||
|
struct Sound;
|
||||||
|
struct EchoParams;
|
||||||
|
struct ReverbParams;
|
||||||
|
} // namespace Ja
|
||||||
|
|
||||||
|
// Processadors d'efectes para sons puntuals. Reben un Ja::Sound (qualsevol
|
||||||
|
// format suportat pel decodificador WAV: U8/S16, mono o estèreo) i tornen un
|
||||||
|
// buffer PCM en S16 + el seu spec, llest per empenyer a un SDL_AudioStream.
|
||||||
|
//
|
||||||
|
// El buffer de sortida inclou la cua (decay) generada per l'efecte: per al
|
||||||
|
// reverb, hasta a 1500 ms; para l'eco, hasta a 800 ms. Aquests caps eviten
|
||||||
|
// allargar indefinidament la reproducció cuando los parámetros reinjecten mucho.
|
||||||
|
//
|
||||||
|
// Si el format del so d'origen no es pot processar, retornen std::nullopt
|
||||||
|
// (el caller ha de fer fallback a reproducció seca).
|
||||||
|
namespace AudioEffects {
|
||||||
|
|
||||||
|
struct ProcessedSound {
|
||||||
|
std::vector<std::uint8_t> bytes; // PCM S16 entrellaçat (LRLRLR... si stereo)
|
||||||
|
SDL_AudioSpec spec; // Format/canals/freqüència del buffer
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound>;
|
||||||
|
[[nodiscard]] auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound>;
|
||||||
|
|
||||||
|
} // namespace AudioEffects
|
||||||
@@ -0,0 +1,645 @@
|
|||||||
|
#include "core/audio/jail_audio.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/audio/audio_effects.hpp"
|
||||||
|
|
||||||
|
// Solo declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
|
||||||
|
// d'implementació. Les definicions las aporta source/external/stb_vorbis_impl.cpp
|
||||||
|
// (TU aïllat porque clang-analyzer no dispari fals positius al nostre codi).
|
||||||
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
|
// clang-format off
|
||||||
|
// NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis es single-file: la macro de dalt limita este TU a solo-declaracions; la implementació viu a external/stb_vorbis_impl.cpp.
|
||||||
|
#include "external/stb_vorbis.c"
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
namespace Ja {
|
||||||
|
|
||||||
|
// --- Streaming internals (file-scope constants) ---
|
||||||
|
namespace {
|
||||||
|
// Bytes-per-sample per canal (siempre s16)
|
||||||
|
constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
|
||||||
|
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
||||||
|
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
||||||
|
constexpr int MUSIC_CHUNK_SHORTS = 8192;
|
||||||
|
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||||
|
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
||||||
|
constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// --- Engine::active_ storage ---
|
||||||
|
Engine* Engine::active_ = nullptr;
|
||||||
|
|
||||||
|
auto Engine::active() noexcept -> Engine* { return active_; }
|
||||||
|
|
||||||
|
// --- Ctor/Dtor ---
|
||||||
|
|
||||||
|
Engine::Engine(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||||
|
assert(active_ == nullptr && "Ja::Engine: més d'una instància activa no está suportat");
|
||||||
|
active_ = this;
|
||||||
|
|
||||||
|
audio_spec_ = {.format = format, .channels = num_channels, .freq = freq};
|
||||||
|
sdl_audio_device_ = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec_);
|
||||||
|
if (sdl_audio_device_ == 0) { std::fprintf(stderr, "Ja::Engine: Failed to initialize SDL audio!\n"); }
|
||||||
|
for (auto& channel : channels_) { channel.state = ChannelState::FREE; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Engine::~Engine() {
|
||||||
|
if (outgoing_music_.stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||||
|
outgoing_music_.stream = nullptr;
|
||||||
|
}
|
||||||
|
if (sdl_audio_device_ != 0) { SDL_CloseAudioDevice(sdl_audio_device_); }
|
||||||
|
sdl_audio_device_ = 0;
|
||||||
|
|
||||||
|
if (active_ == this) { active_ = nullptr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers stateless (no toquen membres d'Engine) ---
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto feedMusicChunk(Music* music) -> int {
|
||||||
|
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }
|
||||||
|
|
||||||
|
short chunk[MUSIC_CHUNK_SHORTS];
|
||||||
|
const int NUM_CHANNELS = music->spec.channels;
|
||||||
|
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
|
||||||
|
music->vorbis,
|
||||||
|
NUM_CHANNELS,
|
||||||
|
static_cast<short*>(chunk),
|
||||||
|
MUSIC_CHUNK_SHORTS);
|
||||||
|
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
|
||||||
|
|
||||||
|
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
SDL_PutAudioStreamData(music->stream, static_cast<const void*>(chunk), BYTES);
|
||||||
|
return SAMPLES_PER_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pumpMusic(Music* music) {
|
||||||
|
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
|
||||||
|
|
||||||
|
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||||
|
|
||||||
|
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
|
||||||
|
const int DECODED = feedMusicChunk(music);
|
||||||
|
if (DECODED > 0) { continue; }
|
||||||
|
|
||||||
|
// EOF: si queden loops, rebobinar; si no, tallar y deixar drenar.
|
||||||
|
if (music->times != 0) {
|
||||||
|
stb_vorbis_seek_start(music->vorbis);
|
||||||
|
if (music->times > 0) { music->times--; }
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void preFillOutgoing(Music* music, const int duration_ms) {
|
||||||
|
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
|
||||||
|
|
||||||
|
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
|
||||||
|
|
||||||
|
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
|
||||||
|
const int DECODED = feedMusicChunk(music);
|
||||||
|
if (DECODED <= 0) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única
|
||||||
|
// font de la corba del fade: si es vol canviar a logarítmica/quadràtica,
|
||||||
|
// s'edita aquí i afecta fade-in i fade-out alhora.
|
||||||
|
auto fadeProgress(const FadeState& fade) -> float {
|
||||||
|
if (fade.duration_ms <= 0) { return 1.0F; }
|
||||||
|
const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time;
|
||||||
|
if (ELAPSED >= static_cast<Uint64>(fade.duration_ms)) { return 1.0F; }
|
||||||
|
return static_cast<float>(ELAPSED) / static_cast<float>(fade.duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Engine::updateOutgoingFade() {
|
||||||
|
if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }
|
||||||
|
|
||||||
|
// Mentre la fosa está activa, mantenim el stream con una reserva
|
||||||
|
// de samples per davant del cursor (mismo patró que pumpMusic
|
||||||
|
// para el current_music_). Así el stream no es buida ni cuando SDL
|
||||||
|
// drena més ràpid del previst en haver sounds bound a la misma
|
||||||
|
// device. Si l'OGG arriba a EOF, rebobina (la fosa pot ser més
|
||||||
|
// llarga que la pista).
|
||||||
|
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||||
|
const Music& music = *outgoing_music_.music;
|
||||||
|
const int BYTES_PER_SECOND = music.spec.freq * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
const int LOW_WATER = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||||
|
while (SDL_GetAudioStreamAvailable(outgoing_music_.stream) < LOW_WATER) {
|
||||||
|
short chunk[MUSIC_CHUNK_SHORTS];
|
||||||
|
const int SAMPLES = stb_vorbis_get_samples_short_interleaved(
|
||||||
|
music.vorbis,
|
||||||
|
music.spec.channels,
|
||||||
|
static_cast<short*>(chunk),
|
||||||
|
MUSIC_CHUNK_SHORTS);
|
||||||
|
if (SAMPLES <= 0) {
|
||||||
|
stb_vorbis_seek_start(music.vorbis);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int BYTES = SAMPLES * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
SDL_PutAudioStreamData(outgoing_music_.stream, static_cast<const void*>(chunk), BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const float PROGRESS = fadeProgress(outgoing_music_.fade);
|
||||||
|
if (PROGRESS >= 1.0F) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||||
|
outgoing_music_.stream = nullptr;
|
||||||
|
outgoing_music_.fade.active = false;
|
||||||
|
// Deixem el Vorbis del Music original en un estat conegut per
|
||||||
|
// a la pròxima reproducció. (playMusic también fa seek_start,
|
||||||
|
// pero fer-ho ací evita estats intermedis si algú consulta.)
|
||||||
|
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||||
|
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
|
||||||
|
}
|
||||||
|
outgoing_music_.music = nullptr;
|
||||||
|
} else {
|
||||||
|
SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::updateIncomingFade() {
|
||||||
|
if (!incoming_fade_.active) { return; }
|
||||||
|
|
||||||
|
const float PROGRESS = fadeProgress(incoming_fade_);
|
||||||
|
if (PROGRESS >= 1.0F) {
|
||||||
|
incoming_fade_.active = false;
|
||||||
|
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||||
|
} else {
|
||||||
|
SDL_SetAudioStreamGain(current_music_->stream, music_volume_ * PROGRESS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::updateCurrentMusic() {
|
||||||
|
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
updateIncomingFade();
|
||||||
|
|
||||||
|
pumpMusic(current_music_);
|
||||||
|
if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) {
|
||||||
|
// La pista ha acabat de drenar naturalment. L'aturem primer (deixa
|
||||||
|
// l'engine en estat consistent) i entonces invoquem el callback;
|
||||||
|
// así un eventual playMusic des del callback comença net.
|
||||||
|
stopMusic();
|
||||||
|
if (on_music_ended_) { on_music_ended_(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::updateSoundChannels() {
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||||
|
if (channels_[i].state != ChannelState::PLAYING) { continue; }
|
||||||
|
|
||||||
|
if (channels_[i].times != 0) {
|
||||||
|
if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(channels_[i].stream)) < (channels_[i].sound->length / 2)) {
|
||||||
|
SDL_PutAudioStreamData(channels_[i].stream, channels_[i].sound->buffer.get(), channels_[i].sound->length);
|
||||||
|
if (channels_[i].times > 0) { channels_[i].times--; }
|
||||||
|
}
|
||||||
|
} else if (SDL_GetAudioStreamAvailable(channels_[i].stream) == 0) {
|
||||||
|
stopChannel(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::stealCurrentIntoOutgoing(const int duration_ms) {
|
||||||
|
if (outgoing_music_.stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||||
|
outgoing_music_.stream = nullptr;
|
||||||
|
outgoing_music_.fade.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING || current_music_->stream == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preFillOutgoing(current_music_, duration_ms);
|
||||||
|
|
||||||
|
outgoing_music_.stream = current_music_->stream;
|
||||||
|
// Guardem la referència al Music porque updateOutgoingFade puga
|
||||||
|
// seguir bombant Vorbis sin al stream durante tota la fosa. NO fem
|
||||||
|
// seek_start ací: la decompressió ha de continuar des d'on estava
|
||||||
|
// porque el so siga continu. El seek_start es farà cuando la fosa
|
||||||
|
// acabe (o cuando playMusic la interrompi via stopMusic).
|
||||||
|
outgoing_music_.music = current_music_;
|
||||||
|
outgoing_music_.fade = {
|
||||||
|
.active = true,
|
||||||
|
.start_time = SDL_GetTicks(),
|
||||||
|
.duration_ms = duration_ms,
|
||||||
|
.initial_volume = music_volume_,
|
||||||
|
};
|
||||||
|
current_music_->stream = nullptr;
|
||||||
|
current_music_->state = MusicState::STOPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Fn>
|
||||||
|
void Engine::forEachTargetChannel(const int channel, Fn&& fn) {
|
||||||
|
if (channel == -1) {
|
||||||
|
for (auto& ch : channels_) { fn(ch); }
|
||||||
|
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
fn(channels_[channel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Engine public API ---
|
||||||
|
|
||||||
|
void Engine::update() {
|
||||||
|
updateOutgoingFade();
|
||||||
|
updateCurrentMusic();
|
||||||
|
updateSoundChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::playMusic(Music* music, const int loop) {
|
||||||
|
if (music == nullptr || music->vorbis == nullptr) { return; }
|
||||||
|
|
||||||
|
stopMusic();
|
||||||
|
|
||||||
|
current_music_ = music;
|
||||||
|
current_music_->state = MusicState::PLAYING;
|
||||||
|
current_music_->times = loop;
|
||||||
|
|
||||||
|
stb_vorbis_seek_start(current_music_->vorbis);
|
||||||
|
|
||||||
|
current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_);
|
||||||
|
if (current_music_->stream == nullptr) {
|
||||||
|
std::fprintf(stderr, "Ja::Engine::playMusic: Failed to create audio stream!\n");
|
||||||
|
current_music_->state = MusicState::STOPPED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||||
|
|
||||||
|
pumpMusic(current_music_);
|
||||||
|
|
||||||
|
if (!SDL_BindAudioStream(sdl_audio_device_, current_music_->stream)) {
|
||||||
|
std::fprintf(stderr, "Ja::Engine::playMusic: SDL_BindAudioStream failed!\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::setMusicSpeed(float ratio) {
|
||||||
|
if (current_music_ == nullptr || current_music_->stream == nullptr) { return; }
|
||||||
|
SDL_SetAudioStreamFrequencyRatio(current_music_->stream, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::pauseMusic() {
|
||||||
|
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
current_music_->state = MusicState::PAUSED;
|
||||||
|
SDL_UnbindAudioStream(current_music_->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::resumeMusic() {
|
||||||
|
if (current_music_ == nullptr || current_music_->state != MusicState::PAUSED) { return; }
|
||||||
|
|
||||||
|
current_music_->state = MusicState::PLAYING;
|
||||||
|
SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::stopMusic() {
|
||||||
|
if (outgoing_music_.stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||||
|
outgoing_music_.stream = nullptr;
|
||||||
|
outgoing_music_.fade.active = false;
|
||||||
|
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||||
|
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
|
||||||
|
}
|
||||||
|
outgoing_music_.music = nullptr;
|
||||||
|
}
|
||||||
|
incoming_fade_.active = false;
|
||||||
|
|
||||||
|
if (current_music_ == nullptr || current_music_->state == MusicState::INVALID || current_music_->state == MusicState::STOPPED) { return; }
|
||||||
|
|
||||||
|
current_music_->state = MusicState::STOPPED;
|
||||||
|
if (current_music_->stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(current_music_->stream);
|
||||||
|
current_music_->stream = nullptr;
|
||||||
|
}
|
||||||
|
if (current_music_->vorbis != nullptr) {
|
||||||
|
stb_vorbis_seek_start(current_music_->vorbis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::fadeOutMusic(const int milliseconds) {
|
||||||
|
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
stealCurrentIntoOutgoing(milliseconds);
|
||||||
|
incoming_fade_.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::crossfadeMusic(Music* music, const int crossfade_ms, const int loop) {
|
||||||
|
if (music == nullptr || music->vorbis == nullptr) { return; }
|
||||||
|
|
||||||
|
stealCurrentIntoOutgoing(crossfade_ms);
|
||||||
|
|
||||||
|
current_music_ = music;
|
||||||
|
current_music_->state = MusicState::PLAYING;
|
||||||
|
current_music_->times = loop;
|
||||||
|
|
||||||
|
stb_vorbis_seek_start(current_music_->vorbis);
|
||||||
|
current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_);
|
||||||
|
if (current_music_->stream == nullptr) {
|
||||||
|
std::fprintf(stderr, "Ja::Engine::crossfadeMusic: Failed to create audio stream!\n");
|
||||||
|
current_music_->state = MusicState::STOPPED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_SetAudioStreamGain(current_music_->stream, 0.0F);
|
||||||
|
pumpMusic(current_music_);
|
||||||
|
SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
|
||||||
|
|
||||||
|
incoming_fade_ = {
|
||||||
|
.active = true,
|
||||||
|
.start_time = SDL_GetTicks(),
|
||||||
|
.duration_ms = crossfade_ms,
|
||||||
|
.initial_volume = 0.0F,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::getMusicState() const -> MusicState {
|
||||||
|
if (current_music_ == nullptr) { return MusicState::INVALID; }
|
||||||
|
return current_music_->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::setMusicVolume(float volume) -> float {
|
||||||
|
music_volume_ = SDL_clamp(volume, 0.0F, 1.0F);
|
||||||
|
if (current_music_ != nullptr && current_music_->stream != nullptr) {
|
||||||
|
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||||
|
}
|
||||||
|
return music_volume_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::setOnMusicEnded(std::function<void()> callback) {
|
||||||
|
on_music_ended_ = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::onMusicDeleted(const Music* music) {
|
||||||
|
if (music == nullptr) { return; }
|
||||||
|
if (current_music_ == music) {
|
||||||
|
stopMusic();
|
||||||
|
current_music_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sound ---
|
||||||
|
|
||||||
|
auto Engine::playSound(Sound* sound, const int loop, const int group) -> int {
|
||||||
|
if (sound == nullptr) { return -1; }
|
||||||
|
|
||||||
|
int channel = 0;
|
||||||
|
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { channel++; }
|
||||||
|
if (channel == MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
// No hay canal libre, reemplazamos el primero
|
||||||
|
channel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return playSoundOnChannel(sound, channel, loop, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int {
|
||||||
|
if (sound == nullptr) { return -1; }
|
||||||
|
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; }
|
||||||
|
|
||||||
|
stopChannel(channel);
|
||||||
|
|
||||||
|
channels_[channel].sound = sound;
|
||||||
|
channels_[channel].times = loop;
|
||||||
|
channels_[channel].pos = 0;
|
||||||
|
channels_[channel].group = group;
|
||||||
|
channels_[channel].state = ChannelState::PLAYING;
|
||||||
|
channels_[channel].stream = SDL_CreateAudioStream(&channels_[channel].sound->spec, &audio_spec_);
|
||||||
|
|
||||||
|
if (channels_[channel].stream == nullptr) {
|
||||||
|
std::fprintf(stderr, "Ja::Engine::playSoundOnChannel: Failed to create audio stream!\n");
|
||||||
|
channels_[channel].state = ChannelState::FREE;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_PutAudioStreamData(channels_[channel].stream, channels_[channel].sound->buffer.get(), channels_[channel].sound->length);
|
||||||
|
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[group]);
|
||||||
|
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::setChannelSpeed(const int channel, const float ratio) {
|
||||||
|
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return; }
|
||||||
|
if (channels_[channel].stream == nullptr) { return; }
|
||||||
|
SDL_SetAudioStreamFrequencyRatio(channels_[channel].stream, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::pauseChannel(const int channel) {
|
||||||
|
forEachTargetChannel(channel, [](Channel& ch) {
|
||||||
|
if (ch.state == ChannelState::PLAYING) {
|
||||||
|
ch.state = ChannelState::PAUSED;
|
||||||
|
SDL_UnbindAudioStream(ch.stream);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::resumeChannel(const int channel) {
|
||||||
|
const SDL_AudioDeviceID DEVICE = sdl_audio_device_;
|
||||||
|
forEachTargetChannel(channel, [DEVICE](Channel& ch) {
|
||||||
|
if (ch.state == ChannelState::PAUSED) {
|
||||||
|
ch.state = ChannelState::PLAYING;
|
||||||
|
SDL_BindAudioStream(DEVICE, ch.stream);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::stopChannel(const int channel) {
|
||||||
|
forEachTargetChannel(channel, [this](Channel& ch) {
|
||||||
|
if (ch.state != ChannelState::FREE) {
|
||||||
|
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
||||||
|
ch.stream = nullptr;
|
||||||
|
ch.state = ChannelState::FREE;
|
||||||
|
ch.pos = 0;
|
||||||
|
ch.sound = nullptr;
|
||||||
|
if (ch.has_effect) {
|
||||||
|
ch.has_effect = false;
|
||||||
|
if (effect_channels_active_ > 0) { --effect_channels_active_; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::setSoundVolume(float volume, const int group) -> float {
|
||||||
|
const float V = SDL_clamp(volume, 0.0F, 1.0F);
|
||||||
|
|
||||||
|
if (group == -1) {
|
||||||
|
std::ranges::fill(sound_volume_, V);
|
||||||
|
} else if (group >= 0 && group < MAX_GROUPS) {
|
||||||
|
sound_volume_[group] = V;
|
||||||
|
} else {
|
||||||
|
return V;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& ch : channels_) {
|
||||||
|
if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) {
|
||||||
|
if (group == -1 || ch.group == group) {
|
||||||
|
if (ch.stream != nullptr) {
|
||||||
|
SDL_SetAudioStreamGain(ch.stream, sound_volume_[ch.group]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return V;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::onSoundDeleted(const Sound* sound) {
|
||||||
|
if (sound == nullptr) { return; }
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||||
|
if (channels_[i].sound == sound) { stopChannel(i); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, const int group) -> int {
|
||||||
|
// El sin de canals con efecte es valida antes de reservar slot —
|
||||||
|
// así evitem crear y destruir un stream solo per descartar el play.
|
||||||
|
if (effect_channels_active_ >= MAX_EFFECT_CHANNELS) { return -1; }
|
||||||
|
|
||||||
|
int channel = 0;
|
||||||
|
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { ++channel; }
|
||||||
|
if (channel == MAX_SIMULTANEOUS_CHANNELS) { channel = 0; }
|
||||||
|
|
||||||
|
stopChannel(channel);
|
||||||
|
|
||||||
|
// El stream es crea contra l'spec del buffer processat (S16, ...)
|
||||||
|
// porque SDL faci el resampling sin a audio_spec_ del device.
|
||||||
|
channels_[channel].stream = SDL_CreateAudioStream(&spec, &audio_spec_);
|
||||||
|
if (channels_[channel].stream == nullptr) {
|
||||||
|
std::fprintf(stderr, "Ja::Engine::playProcessedOnFreeChannel: Failed to create audio stream!\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
channels_[channel].sound = nullptr; // El buffer no es propietat de sin Ja::Sound.
|
||||||
|
channels_[channel].times = 0;
|
||||||
|
channels_[channel].pos = 0;
|
||||||
|
const int CLAMPED_GROUP = (group >= 0 && group < MAX_GROUPS) ? group : 0;
|
||||||
|
channels_[channel].group = CLAMPED_GROUP;
|
||||||
|
channels_[channel].state = ChannelState::PLAYING;
|
||||||
|
channels_[channel].has_effect = true;
|
||||||
|
++effect_channels_active_;
|
||||||
|
|
||||||
|
SDL_PutAudioStreamData(channels_[channel].stream, bytes.data(), static_cast<int>(bytes.size()));
|
||||||
|
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[CLAMPED_GROUP]);
|
||||||
|
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& params, const int group) -> int {
|
||||||
|
if (sound == nullptr) { return -1; }
|
||||||
|
auto processed = AudioEffects::applyEcho(*sound, params);
|
||||||
|
if (!processed) { return -1; }
|
||||||
|
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& params, const int group) -> int {
|
||||||
|
if (sound == nullptr) { return -1; }
|
||||||
|
auto processed = AudioEffects::applyReverb(*sound, params);
|
||||||
|
if (!processed) { return -1; }
|
||||||
|
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Factories y destructors (permanents) ---
|
||||||
|
|
||||||
|
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||||
|
if (buffer == nullptr || length == 0) { return nullptr; }
|
||||||
|
|
||||||
|
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
|
||||||
|
// como a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||||
|
// persistent al buffer; como que ací no el resize'jem, el .data() es
|
||||||
|
// estable durante tot el cicle de vida del music.
|
||||||
|
auto music = std::make_unique<Music>();
|
||||||
|
music->ogg_data.assign(buffer, buffer + length);
|
||||||
|
|
||||||
|
int vorbis_error = 0;
|
||||||
|
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||||
|
static_cast<int>(length),
|
||||||
|
&vorbis_error,
|
||||||
|
nullptr);
|
||||||
|
if (music->vorbis == nullptr) {
|
||||||
|
std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
|
||||||
|
music->spec.channels = static_cast<int>(INFO.channels);
|
||||||
|
music->spec.freq = static_cast<int>(INFO.sample_rate);
|
||||||
|
music->spec.format = SDL_AUDIO_S16;
|
||||||
|
// Pre-cálculo de la duración en ms a partir del header. stb_vorbis ya
|
||||||
|
// ha decodificat la informació necessària a `stb_vorbis_open_memory`;
|
||||||
|
// esta consulta no descodifica àudio, solo llig el comptador
|
||||||
|
// de samples. Si el sample_rate fos 0 (header malmès) deixem
|
||||||
|
// duration_ms a 0.
|
||||||
|
if (INFO.sample_rate > 0) {
|
||||||
|
const auto SAMPLES = stb_vorbis_stream_length_in_samples(music->vorbis);
|
||||||
|
music->duration_ms = static_cast<int>((static_cast<std::uint64_t>(SAMPLES) * 1000ULL) / INFO.sample_rate);
|
||||||
|
}
|
||||||
|
music->state = MusicState::STOPPED;
|
||||||
|
|
||||||
|
return music.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload con filename. Resource::Cache l'usa per registrar el path dins
|
||||||
|
// del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar
|
||||||
|
// el nom después d'un playMusic(Ja::Music*, ...) — veure PATCH-02.
|
||||||
|
auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* {
|
||||||
|
Music* music = loadMusic(buffer, length);
|
||||||
|
if (music != nullptr && filename != nullptr) { music->filename = filename; }
|
||||||
|
return music;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteMusic(Music* music) {
|
||||||
|
if (music == nullptr) { return; }
|
||||||
|
// Notifiquem el motor actiu porque pari la pista si es la current_music.
|
||||||
|
// Si no hay motor (shutdown-order invertit), passem: los recursos
|
||||||
|
// propis del Music es lliberen igualment a sota.
|
||||||
|
if (Engine* eng = Engine::active()) { eng->onMusicDeleted(music); }
|
||||||
|
|
||||||
|
if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
|
||||||
|
if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); }
|
||||||
|
delete music;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
|
||||||
|
auto sound = std::make_unique<Sound>();
|
||||||
|
Uint8* raw = nullptr;
|
||||||
|
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
|
||||||
|
std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||||
|
return sound.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteSound(Sound* sound) {
|
||||||
|
if (sound == nullptr) { return; }
|
||||||
|
if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); }
|
||||||
|
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
|
||||||
|
delete sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Ja
|
||||||
|
|
||||||
|
// --- stb_vorbis macro leak cleanup ---
|
||||||
|
// stb_vorbis.c filtra noms curts (L, C, R i PLAYBACK_*) al TU que el compila.
|
||||||
|
// Xocarien con parámetros de plantilla d'altres headers si estas definicions
|
||||||
|
// s'escapessin. Els netegem al final del TU per tancar la porta.
|
||||||
|
// clang-format off
|
||||||
|
#undef L
|
||||||
|
#undef C
|
||||||
|
#undef R
|
||||||
|
#undef PLAYBACK_MONO
|
||||||
|
#undef PLAYBACK_LEFT
|
||||||
|
#undef PLAYBACK_RIGHT
|
||||||
|
// clang-format on
|
||||||
+242
-466
@@ -2,481 +2,257 @@
|
|||||||
|
|
||||||
// --- Includes ---
|
// --- 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(¤t_music->spec, &JA_audioSpec);
|
|
||||||
if (!current_music->stream) { // Comprobar creación de stream
|
|
||||||
SDL_Log("Failed to create audio stream!");
|
|
||||||
current_music->state = JA_MUSIC_STOPPED;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
|
|
||||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
|
||||||
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
|
|
||||||
if (!music) music = current_music;
|
|
||||||
if (!music) return nullptr; // Añadida comprobación
|
|
||||||
return music->filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_PauseMusic() {
|
|
||||||
if (!JA_musicEnabled) return;
|
|
||||||
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
|
|
||||||
|
|
||||||
current_music->state = JA_MUSIC_PAUSED;
|
|
||||||
SDL_UnbindAudioStream(current_music->stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_ResumeMusic() {
|
|
||||||
if (!JA_musicEnabled) return;
|
|
||||||
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada
|
|
||||||
|
|
||||||
current_music->state = JA_MUSIC_PLAYING;
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_StopMusic() {
|
|
||||||
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
|
|
||||||
|
|
||||||
current_music->pos = 0;
|
|
||||||
current_music->state = JA_MUSIC_STOPPED;
|
|
||||||
if (current_music->stream) {
|
|
||||||
SDL_DestroyAudioStream(current_music->stream);
|
|
||||||
current_music->stream = nullptr;
|
|
||||||
}
|
|
||||||
// No liberamos filename aquí, se debería liberar en JA_DeleteMusic
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_FadeOutMusic(const int milliseconds) {
|
|
||||||
if (!JA_musicEnabled) return;
|
|
||||||
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
|
|
||||||
|
|
||||||
fading = true;
|
|
||||||
fade_start_time = SDL_GetTicks();
|
|
||||||
fade_duration = milliseconds;
|
|
||||||
fade_initial_volume = JA_musicVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline JA_Music_state JA_GetMusicState() {
|
|
||||||
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
|
|
||||||
if (!current_music) return JA_MUSIC_INVALID;
|
|
||||||
|
|
||||||
return current_music->state;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_DeleteMusic(JA_Music_t* music) {
|
|
||||||
if (!music) return;
|
|
||||||
if (current_music == music) {
|
|
||||||
JA_StopMusic();
|
|
||||||
current_music = nullptr;
|
|
||||||
}
|
|
||||||
SDL_free(music->buffer);
|
|
||||||
if (music->stream) SDL_DestroyAudioStream(music->stream);
|
|
||||||
free(music->filename); // filename se libera aquí
|
|
||||||
delete music;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float JA_SetMusicVolume(float volume) {
|
|
||||||
JA_musicVolume = SDL_clamp(volume, 0.0F, 1.0F);
|
|
||||||
if (current_music && current_music->stream) {
|
|
||||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
|
||||||
}
|
|
||||||
return JA_musicVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_SetMusicPosition(float value) {
|
|
||||||
if (!current_music) return;
|
|
||||||
current_music->pos = value * current_music->spec.freq;
|
|
||||||
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
|
|
||||||
// el streaming. El streaming siempre parece empezar desde el principio.
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float JA_GetMusicPosition() {
|
|
||||||
if (!current_music) return 0;
|
|
||||||
return float(current_music->pos) / float(current_music->spec.freq);
|
|
||||||
// Nota: Ver `JA_SetMusicPosition`
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_EnableMusic(const bool value) {
|
|
||||||
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
|
|
||||||
|
|
||||||
JA_musicEnabled = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Sound Functions ---
|
|
||||||
|
|
||||||
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
|
|
||||||
JA_Sound_t* sound = new JA_Sound_t();
|
|
||||||
sound->buffer = buffer;
|
|
||||||
sound->length = length;
|
|
||||||
// Nota: spec se queda con los valores por defecto.
|
|
||||||
return sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
|
||||||
JA_Sound_t* sound = new JA_Sound_t();
|
|
||||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
|
|
||||||
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
|
|
||||||
delete sound;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline JA_Sound_t* JA_LoadSound(const char* filename) {
|
|
||||||
JA_Sound_t* sound = new JA_Sound_t();
|
|
||||||
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
|
|
||||||
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
|
|
||||||
delete sound;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
|
|
||||||
if (!JA_soundEnabled || !sound) return -1;
|
|
||||||
|
|
||||||
int channel = 0;
|
|
||||||
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
|
|
||||||
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
|
|
||||||
// No hay canal libre, reemplazamos el primero
|
|
||||||
channel = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JA_PlaySoundOnChannel(sound, channel, loop, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
|
|
||||||
if (!JA_soundEnabled || !sound) return -1;
|
|
||||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
|
|
||||||
|
|
||||||
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso
|
|
||||||
|
|
||||||
channels[channel].sound = sound;
|
|
||||||
channels[channel].times = loop;
|
|
||||||
channels[channel].pos = 0;
|
|
||||||
channels[channel].group = group; // Asignar grupo
|
|
||||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
|
||||||
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
|
||||||
|
|
||||||
if (!channels[channel].stream) {
|
|
||||||
SDL_Log("Failed to create audio stream for sound!");
|
|
||||||
channels[channel].state = JA_CHANNEL_FREE;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
|
|
||||||
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
|
||||||
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_DeleteSound(JA_Sound_t* sound) {
|
|
||||||
if (!sound) return;
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
|
||||||
if (channels[i].sound == sound) JA_StopChannel(i);
|
|
||||||
}
|
|
||||||
SDL_free(sound->buffer);
|
|
||||||
delete sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_PauseChannel(const int channel) {
|
|
||||||
if (!JA_soundEnabled) return;
|
|
||||||
|
|
||||||
if (channel == -1) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
|
||||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
|
||||||
channels[i].state = JA_CHANNEL_PAUSED;
|
|
||||||
SDL_UnbindAudioStream(channels[i].stream);
|
|
||||||
}
|
|
||||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
|
||||||
if (channels[channel].state == JA_CHANNEL_PLAYING) {
|
|
||||||
channels[channel].state = JA_CHANNEL_PAUSED;
|
|
||||||
SDL_UnbindAudioStream(channels[channel].stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_ResumeChannel(const int channel) {
|
|
||||||
if (!JA_soundEnabled) return;
|
|
||||||
|
|
||||||
if (channel == -1) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
|
||||||
if (channels[i].state == JA_CHANNEL_PAUSED) {
|
|
||||||
channels[i].state = JA_CHANNEL_PLAYING;
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
|
|
||||||
}
|
|
||||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
|
||||||
if (channels[channel].state == JA_CHANNEL_PAUSED) {
|
|
||||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
|
||||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_StopChannel(const int channel) {
|
|
||||||
if (channel == -1) {
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
|
||||||
if (channels[i].state != JA_CHANNEL_FREE) {
|
|
||||||
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
|
|
||||||
channels[i].stream = nullptr;
|
|
||||||
channels[i].state = JA_CHANNEL_FREE;
|
|
||||||
channels[i].pos = 0;
|
|
||||||
channels[i].sound = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
|
||||||
if (channels[channel].state != JA_CHANNEL_FREE) {
|
|
||||||
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
|
|
||||||
channels[channel].stream = nullptr;
|
|
||||||
channels[channel].state = JA_CHANNEL_FREE;
|
|
||||||
channels[channel].pos = 0;
|
|
||||||
channels[channel].sound = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline JA_Channel_state JA_GetChannelState(const int channel) {
|
|
||||||
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
|
|
||||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
|
|
||||||
|
|
||||||
return channels[channel].state;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos
|
|
||||||
{
|
|
||||||
const float v = SDL_clamp(volume, 0.0F, 1.0F);
|
|
||||||
|
|
||||||
if (group == -1) {
|
|
||||||
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
|
|
||||||
JA_soundVolume[i] = v;
|
|
||||||
}
|
|
||||||
} else if (group >= 0 && group < JA_MAX_GROUPS) {
|
|
||||||
JA_soundVolume[group] = v;
|
|
||||||
} else {
|
|
||||||
return v; // Grupo inválido
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aplicar volumen a canales activos
|
|
||||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
|
||||||
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
|
|
||||||
if (group == -1 || channels[i].group == group) {
|
|
||||||
if (channels[i].stream) {
|
|
||||||
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void JA_EnableSound(const bool value) {
|
|
||||||
if (!value) {
|
|
||||||
JA_StopChannel(-1); // Detener todos los canales
|
|
||||||
}
|
|
||||||
JA_soundEnabled = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float JA_SetVolume(float volume) {
|
|
||||||
float v = JA_SetMusicVolume(volume);
|
|
||||||
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#include "core/audio/sound_effects_config.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Lector de camp con fallback: deixa el destí intacte si la clau no
|
||||||
|
// existeix (los defaults dels Ja::*Params s'inicialitzen al ctor del
|
||||||
|
// struct, así que el comportament es "preset parcial = preset complet
|
||||||
|
// con defaults per als camps que falten").
|
||||||
|
template <typename T>
|
||||||
|
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||||
|
if (node.contains(key)) { dst = node[key].get_value<T>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
|
||||||
|
static SoundEffectsConfig instance_;
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoundEffectsConfig::load(const std::string& file_path) {
|
||||||
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
std::cerr << "[SoundEffectsConfig] no se ha podido abrir " << file_path
|
||||||
|
<< " — sin presets de efecto disponibles\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
|
|
||||||
|
if (yaml.contains("echo") && yaml["echo"].is_mapping()) {
|
||||||
|
for (auto it = yaml["echo"].begin(); it != yaml["echo"].end(); ++it) {
|
||||||
|
const auto NAME = it.key().get_value<std::string>();
|
||||||
|
const auto& node = it.value();
|
||||||
|
Ja::EchoParams params{};
|
||||||
|
readField(node, "delay_ms", params.delay_ms);
|
||||||
|
readField(node, "feedback", params.feedback);
|
||||||
|
readField(node, "wet", params.wet);
|
||||||
|
echoes_[NAME] = params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("reverb") && yaml["reverb"].is_mapping()) {
|
||||||
|
for (auto it = yaml["reverb"].begin(); it != yaml["reverb"].end(); ++it) {
|
||||||
|
const auto NAME = it.key().get_value<std::string>();
|
||||||
|
const auto& node = it.value();
|
||||||
|
Ja::ReverbParams params{};
|
||||||
|
readField(node, "room_size", params.room_size);
|
||||||
|
readField(node, "damping", params.damping);
|
||||||
|
readField(node, "wet", params.wet);
|
||||||
|
reverbs_[NAME] = params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[SoundEffectsConfig] " << echoes_.size() << " preset(s) de echo y "
|
||||||
|
<< reverbs_.size() << " de reverb desde " << file_path << "\n";
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[SoundEffectsConfig] error parseando " << file_path << ": " << e.what() << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SoundEffectsConfig::findEcho(const std::string& name) const -> const Ja::EchoParams* {
|
||||||
|
const auto IT = echoes_.find(name);
|
||||||
|
return (IT == echoes_.end()) ? nullptr : &IT->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SoundEffectsConfig::findReverb(const std::string& name) const -> const Ja::ReverbParams* {
|
||||||
|
const auto IT = reverbs_.find(name);
|
||||||
|
return (IT == reverbs_.end()) ? nullptr : &IT->second;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "core/audio/jail_audio.hpp" // Para Ja::EchoParams / Ja::ReverbParams
|
||||||
|
|
||||||
|
// Catàleg de presets d'efectes carregat des de data/config/sounds.yaml. La capa
|
||||||
|
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
|
||||||
|
// preset no existeix, el so es reprodueix sec con un avís a stderr.
|
||||||
|
//
|
||||||
|
// Patró Meyers idèntic a UiConfig/Locale: un sol load() a l'arrencada, sense
|
||||||
|
// hot-reload. Si el archivo no existeix, el catàleg queda buit (sin preset
|
||||||
|
// disponible) i tots los playSoundWith* es comporten como playSound dry.
|
||||||
|
class SoundEffectsConfig {
|
||||||
|
public:
|
||||||
|
static auto get() -> SoundEffectsConfig&;
|
||||||
|
|
||||||
|
SoundEffectsConfig(const SoundEffectsConfig&) = delete;
|
||||||
|
SoundEffectsConfig(SoundEffectsConfig&&) = delete;
|
||||||
|
auto operator=(const SoundEffectsConfig&) -> SoundEffectsConfig& = delete;
|
||||||
|
auto operator=(SoundEffectsConfig&&) -> SoundEffectsConfig& = delete;
|
||||||
|
|
||||||
|
void load(const std::string& file_path);
|
||||||
|
|
||||||
|
// Retorna nullptr si el preset no existeix.
|
||||||
|
[[nodiscard]] auto findEcho(const std::string& name) const -> const Ja::EchoParams*;
|
||||||
|
[[nodiscard]] auto findReverb(const std::string& name) const -> const Ja::ReverbParams*;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SoundEffectsConfig() = default;
|
||||||
|
~SoundEffectsConfig() = default;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, Ja::EchoParams> echoes_;
|
||||||
|
std::unordered_map<std::string, Ja::ReverbParams> reverbs_;
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// engine_config.hpp - Configuració runtime del motor (window, render, input)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Struct POD que conté la configuració runtime que els sistemes de `core/`
|
||||||
|
// llegeixen i muten. La capa de persistència (YAML) viu a `game/config_yaml.cpp`,
|
||||||
|
// que omple aquesta struct a init() i loadFromFile().
|
||||||
|
//
|
||||||
|
// Es passa per referència (mutable quan cal) al constructor dels sistemes
|
||||||
|
// que la necessiten, mantenint `core/` agnòstic a `game/`.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Config {
|
||||||
|
|
||||||
|
struct WindowConfig {
|
||||||
|
int width{1280};
|
||||||
|
int height{720};
|
||||||
|
bool fullscreen{false};
|
||||||
|
float zoom_factor{1.0F}; // Zoom level (0.5x to max_zoom)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderingConfig {
|
||||||
|
int vsync{1}; // 0=disabled, 1=enabled
|
||||||
|
int antialias{1}; // 0=disabled, 1=enabled (AA geomètric a les línies, toggle F5)
|
||||||
|
// Resolució del render target offscreen (independent del tamany lògic
|
||||||
|
// 1280×720 del joc). Aquesta és la resolució real on rasteritzen les
|
||||||
|
// línies abans de l'escala final a la swapchain; pujar-la millora
|
||||||
|
// la nitidesa en finestres grans i fullscreen. Llista tancada de
|
||||||
|
// presets 16:9 — veure Defaults::Rendering::RESOLUTION_PRESETS.
|
||||||
|
int render_width{1280};
|
||||||
|
int render_height{720};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KeyboardBindings {
|
||||||
|
SDL_Scancode key_left{SDL_SCANCODE_LEFT};
|
||||||
|
SDL_Scancode key_right{SDL_SCANCODE_RIGHT};
|
||||||
|
SDL_Scancode key_thrust{SDL_SCANCODE_UP};
|
||||||
|
SDL_Scancode key_shoot{SDL_SCANCODE_SPACE};
|
||||||
|
SDL_Scancode key_start{SDL_SCANCODE_1};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GamepadBindings {
|
||||||
|
int button_left{SDL_GAMEPAD_BUTTON_DPAD_LEFT};
|
||||||
|
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
|
||||||
|
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
|
||||||
|
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlayerBindings {
|
||||||
|
KeyboardBindings keyboard{};
|
||||||
|
GamepadBindings gamepad{};
|
||||||
|
std::string gamepad_name; // Empty = auto-assign by index
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EngineConfig {
|
||||||
|
WindowConfig window{};
|
||||||
|
RenderingConfig rendering{};
|
||||||
|
PlayerBindings player1{};
|
||||||
|
PlayerBindings player2{};
|
||||||
|
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
|
||||||
|
GamepadBindings gamepad_controls{};
|
||||||
|
bool console{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Capa de persistència delegada cap a l'EngineConfig. Permet al Director
|
||||||
|
// orquestrar init/load/save sense conèixer cap esquema concret (YAML,
|
||||||
|
// SQLite, ...) ni la capa que el conté (`game/config_yaml.cpp`).
|
||||||
|
struct ConfigPersistence {
|
||||||
|
std::function<void()> init_defaults; // Restaura valors per defecte
|
||||||
|
std::function<void(const std::string& path)> set_path; // Indica on guardar
|
||||||
|
std::function<bool()> load; // Llegeix path → EngineConfig
|
||||||
|
std::function<bool()> save; // Escriu EngineConfig → path
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Config
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// 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 (...) { // @INTENTIONAL
|
||||||
|
// Mantiene los defaults si algún elemento del RGB no es entero parseable
|
||||||
|
// (el YAML viene de archivo, así que es razonable degradar a los defaults
|
||||||
|
// en vez de propagar la excepción y abortar el load del postpro 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);
|
||||||
|
// sigma_px és el paràmetre canònic des del separable blur; acceptem
|
||||||
|
// també `radius_px` com a alias per a configs antigues (s'interpreta
|
||||||
|
// com sigma directament — els valors útils estan al mateix rang ~2-5).
|
||||||
|
readField(node, "sigma_px", params.bloom_sigma_px);
|
||||||
|
readField(node, "radius_px", params.bloom_sigma_px);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("flicker") && yaml["flicker"].is_mapping()) {
|
||||||
|
const auto& node = yaml["flicker"];
|
||||||
|
readField(node, "enabled", params.flicker_enabled);
|
||||||
|
readField(node, "amplitude", params.flicker_amplitude);
|
||||||
|
readField(node, "frequency_hz", params.flicker_frequency_hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("background") && yaml["background"].is_mapping()) {
|
||||||
|
const auto& node = yaml["background"];
|
||||||
|
readField(node, "enabled", params.background_enabled);
|
||||||
|
readRgb255(node, "color_min", params.background_min_r, params.background_min_g, params.background_min_b);
|
||||||
|
readRgb255(node, "color_max", params.background_max_r, params.background_max_g, params.background_max_b);
|
||||||
|
readField(node, "pulse_frequency_hz", params.background_pulse_freq_hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[PostFxConfig] Cargado " << path
|
||||||
|
<< " (bloom=" << (params.bloom_enabled ? "on" : "off")
|
||||||
|
<< " intensity=" << params.bloom_intensity
|
||||||
|
<< ", flicker=" << (params.flicker_enabled ? "on" : "off")
|
||||||
|
<< " amp=" << params.flicker_amplitude
|
||||||
|
<< ", bg=" << (params.background_enabled ? "on" : "off")
|
||||||
|
<< ")\n";
|
||||||
|
} catch (const fkyaml::exception& e) {
|
||||||
|
std::cerr << "[PostFxConfig] Error parseando " << path << ": " << e.what()
|
||||||
|
<< " — usando defaults built-in\n";
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Config::PostFx
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// postfx_config.hpp - Carga de los parámetros del shader de postpro desde YAML.
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// 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
|
||||||
+31
-530
@@ -1,532 +1,33 @@
|
|||||||
|
// defaults.hpp - Umbrella header que reuneix totes les constants del joc.
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// El contingut viu ara a source/core/defaults/*.hpp (un fitxer per
|
||||||
|
// namespace). Es manté aquest umbrella per no haver de tocar els 22
|
||||||
|
// includers existents. Codi nou pot incloure directament el subfitxer
|
||||||
|
// concret per millorar el temps de compilació incremental.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
#include <cmath>
|
// IWYU pragma: begin_exports
|
||||||
#include <cstdint>
|
#include "core/defaults/audio.hpp"
|
||||||
#include <numbers>
|
#include "core/defaults/border.hpp"
|
||||||
|
#include "core/defaults/brightness.hpp"
|
||||||
namespace Defaults {
|
#include "core/defaults/controls.hpp"
|
||||||
// Configuración de ventana
|
#include "core/defaults/effects.hpp"
|
||||||
namespace Window {
|
#include "core/defaults/enemies.hpp"
|
||||||
constexpr int WIDTH = 640;
|
#include "core/defaults/entities.hpp"
|
||||||
constexpr int HEIGHT = 480;
|
#include "core/defaults/floating_score.hpp"
|
||||||
constexpr int MIN_WIDTH = 320; // Mínimo: mitad del original
|
#include "core/defaults/game.hpp"
|
||||||
constexpr int MIN_HEIGHT = 240;
|
#include "core/defaults/hud.hpp"
|
||||||
// Zoom system
|
#include "core/defaults/math.hpp"
|
||||||
constexpr float BASE_ZOOM = 1.0F; // 640x480 baseline
|
#include "core/defaults/notifier.hpp"
|
||||||
constexpr float MIN_ZOOM = 0.5F; // 320x240 minimum
|
#include "core/defaults/palette.hpp"
|
||||||
constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2)
|
#include "core/defaults/physics.hpp"
|
||||||
constexpr bool FULLSCREEN = true; // Pantalla completa activadapor defecto
|
#include "core/defaults/playfield.hpp"
|
||||||
} // namespace Window
|
#include "core/defaults/rendering.hpp"
|
||||||
|
#include "core/defaults/ship.hpp"
|
||||||
// Dimensions base del joc (coordenades lògiques)
|
#include "core/defaults/title.hpp"
|
||||||
namespace Game {
|
#include "core/defaults/trail.hpp"
|
||||||
constexpr int WIDTH = 640;
|
#include "core/defaults/window.hpp"
|
||||||
constexpr int HEIGHT = 480;
|
#include "core/defaults/zones.hpp"
|
||||||
} // namespace Game
|
// IWYU pragma: end_exports
|
||||||
|
|
||||||
// Zones del joc (SDL_FRect amb càlculs automàtics basat en percentatges)
|
|
||||||
namespace Zones {
|
|
||||||
// --- CONFIGURACIÓ DE PORCENTATGES ---
|
|
||||||
// Totes les zones definides com a percentatges de Game::WIDTH (640) i Game::HEIGHT (480)
|
|
||||||
|
|
||||||
// Percentatges d'alçada (divisió vertical)
|
|
||||||
constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior
|
|
||||||
constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central
|
|
||||||
constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior
|
|
||||||
|
|
||||||
// Padding horizontal per a PLAYAREA (dins de MAIN_PLAYAREA)
|
|
||||||
constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat
|
|
||||||
|
|
||||||
// --- CÀLCULS AUTOMÀTICS DE PÍXELS ---
|
|
||||||
// Càlculs automàtics a partir dels percentatges
|
|
||||||
|
|
||||||
// Alçades
|
|
||||||
constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT;
|
|
||||||
constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT;
|
|
||||||
constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT;
|
|
||||||
|
|
||||||
// Posicions Y
|
|
||||||
constexpr float SCOREBOARD_TOP_Y = 0.0F;
|
|
||||||
constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H;
|
|
||||||
constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H;
|
|
||||||
|
|
||||||
// Padding horizontal de PLAYAREA
|
|
||||||
constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT;
|
|
||||||
|
|
||||||
// --- ZONES FINALS (SDL_FRect) ---
|
|
||||||
|
|
||||||
// Marcador superior (reservat per a futur ús)
|
|
||||||
// Ocupa: 10% superior (0-48px)
|
|
||||||
constexpr SDL_FRect SCOREBOARD_TOP = {
|
|
||||||
0.0F, // x = 0.0
|
|
||||||
SCOREBOARD_TOP_Y, // y = 0.0
|
|
||||||
static_cast<float>(Game::WIDTH), // w = 640.0
|
|
||||||
SCOREBOARD_TOP_H // h = 48.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Àrea de joc principal (contenidor del 80% central, sense padding)
|
|
||||||
// Ocupa: 10-90% (48-432px), ample complet
|
|
||||||
constexpr SDL_FRect MAIN_PLAYAREA = {
|
|
||||||
0.0F, // x = 0.0
|
|
||||||
MAIN_PLAYAREA_Y, // y = 48.0
|
|
||||||
static_cast<float>(Game::WIDTH), // w = 640.0
|
|
||||||
MAIN_PLAYAREA_H // h = 384.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zona de joc real (amb padding horizontal del 5%)
|
|
||||||
// Ocupa: dins de MAIN_PLAYAREA, amb marges laterals
|
|
||||||
// S'utilitza per a límits del joc, col·lisions, spawn
|
|
||||||
constexpr SDL_FRect PLAYAREA = {
|
|
||||||
PLAYAREA_PADDING_H, // x = 32.0
|
|
||||||
MAIN_PLAYAREA_Y, // y = 48.0 (igual que MAIN_PLAYAREA)
|
|
||||||
Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // w = 576.0
|
|
||||||
MAIN_PLAYAREA_H // h = 384.0 (igual que MAIN_PLAYAREA)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Marcador inferior (marcador actual)
|
|
||||||
// Ocupa: 10% inferior (432-480px)
|
|
||||||
constexpr SDL_FRect SCOREBOARD = {
|
|
||||||
0.0F, // x = 0.0
|
|
||||||
SCOREBOARD_BOTTOM_Y, // y = 432.0
|
|
||||||
static_cast<float>(Game::WIDTH), // w = 640.0
|
|
||||||
SCOREBOARD_BOTTOM_H // h = 48.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Padding horizontal del marcador (per alinear zones esquerra/dreta amb PLAYAREA)
|
|
||||||
constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f;
|
|
||||||
} // namespace Zones
|
|
||||||
|
|
||||||
// Objetos del juego
|
|
||||||
namespace Entities {
|
|
||||||
constexpr int MAX_ORNIS = 15;
|
|
||||||
constexpr int MAX_BALES = 3;
|
|
||||||
constexpr int MAX_IPUNTS = 30;
|
|
||||||
|
|
||||||
constexpr float SHIP_RADIUS = 12.0F;
|
|
||||||
constexpr float ENEMY_RADIUS = 20.0F;
|
|
||||||
constexpr float BULLET_RADIUS = 3.0F;
|
|
||||||
} // namespace Entities
|
|
||||||
|
|
||||||
// Ship (nave del jugador)
|
|
||||||
namespace Ship {
|
|
||||||
// Invulnerabilidad post-respawn
|
|
||||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
|
|
||||||
|
|
||||||
// Parpadeo visual durante invulnerabilidad
|
|
||||||
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
|
|
||||||
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
|
|
||||||
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
|
|
||||||
} // namespace Ship
|
|
||||||
|
|
||||||
// Game rules (lives, respawn, game over)
|
|
||||||
namespace Game {
|
|
||||||
constexpr int STARTING_LIVES = 3; // Initial lives
|
|
||||||
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
|
|
||||||
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
|
|
||||||
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous)
|
|
||||||
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
|
|
||||||
|
|
||||||
// Friendly fire system
|
|
||||||
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
|
||||||
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
|
||||||
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
|
|
||||||
|
|
||||||
// Transición LEVEL_START (mensajes aleatorios PRE-level)
|
|
||||||
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
|
||||||
constexpr float LEVEL_START_TYPING_RATIO = 0.3F; // 30% escribiendo, 70% mostrando
|
|
||||||
|
|
||||||
// Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!")
|
|
||||||
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
|
|
||||||
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0F; // 0.0 = sin typewriter (directo)
|
|
||||||
|
|
||||||
// Transición INIT_HUD (animación inicial del HUD)
|
|
||||||
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
|
|
||||||
|
|
||||||
// Ratios de animación (inicio y fin como porcentajes del tiempo total)
|
|
||||||
// RECT (rectángulo de marges)
|
|
||||||
constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F;
|
|
||||||
constexpr float INIT_HUD_RECT_RATIO_END = 0.85F;
|
|
||||||
|
|
||||||
// SCORE (marcador de puntuación)
|
|
||||||
constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F;
|
|
||||||
constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F;
|
|
||||||
|
|
||||||
// SHIP1 (nave jugador 1)
|
|
||||||
constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F;
|
|
||||||
constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F;
|
|
||||||
|
|
||||||
// SHIP2 (nave jugador 2)
|
|
||||||
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
|
|
||||||
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F;
|
|
||||||
|
|
||||||
// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego)
|
|
||||||
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA
|
|
||||||
|
|
||||||
// Spawn positions (distribución horizontal para 2 jugadores)
|
|
||||||
constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda
|
|
||||||
constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda
|
|
||||||
constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba
|
|
||||||
|
|
||||||
// Continue system behavior
|
|
||||||
constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9
|
|
||||||
constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick
|
|
||||||
constexpr int MAX_CONTINUES = 3; // Maximum continues per game
|
|
||||||
constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues
|
|
||||||
|
|
||||||
// Continue screen visual configuration
|
|
||||||
namespace ContinueScreen {
|
|
||||||
// "CONTINUE" text
|
|
||||||
constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size
|
|
||||||
constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA
|
|
||||||
|
|
||||||
// Countdown number (9, 8, 7...)
|
|
||||||
constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large)
|
|
||||||
constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA
|
|
||||||
|
|
||||||
// "CONTINUES LEFT: X" text
|
|
||||||
constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small)
|
|
||||||
constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA
|
|
||||||
} // namespace ContinueScreen
|
|
||||||
|
|
||||||
// Game Over screen visual configuration
|
|
||||||
namespace GameOverScreen {
|
|
||||||
constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size
|
|
||||||
constexpr float TEXT_SPACING = 4.0F; // Character spacing
|
|
||||||
} // namespace GameOverScreen
|
|
||||||
|
|
||||||
// Stage message configuration (LEVEL_START, LEVEL_COMPLETED)
|
|
||||||
constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA
|
|
||||||
constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width
|
|
||||||
} // namespace Game
|
|
||||||
|
|
||||||
// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp)
|
|
||||||
namespace Physics {
|
|
||||||
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s)
|
|
||||||
constexpr float ACCELERATION = 400.0F; // px/s²
|
|
||||||
constexpr float MAX_VELOCITY = 120.0F; // px/s
|
|
||||||
constexpr float FRICTION = 20.0F; // px/s²
|
|
||||||
constexpr float ENEMY_SPEED = 2.0F; // unidades/frame
|
|
||||||
constexpr float BULLET_SPEED = 6.0F; // unidades/frame
|
|
||||||
constexpr float VELOCITY_SCALE = 20.0F; // factor conversión frame→tiempo
|
|
||||||
|
|
||||||
// Explosions (debris physics)
|
|
||||||
namespace Debris {
|
|
||||||
constexpr float VELOCITAT_BASE = 80.0F; // Velocitat inicial (px/s)
|
|
||||||
constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s)
|
|
||||||
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
|
||||||
constexpr float ROTACIO_MIN = 0.1F; // Rotació mínima (rad/s ~5.7°/s)
|
|
||||||
constexpr float ROTACIO_MAX = 0.3F; // Rotació màxima (rad/s ~17.2°/s)
|
|
||||||
constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris
|
|
||||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION)
|
|
||||||
constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s)
|
|
||||||
|
|
||||||
// Herència de velocitat angular (trayectorias curvas)
|
|
||||||
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
|
||||||
constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
|
||||||
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
|
||||||
|
|
||||||
// Angular velocity cap for trajectory inheritance
|
|
||||||
// Excess above this threshold is converted to tangential linear velocity
|
|
||||||
// Prevents "vortex trap" problem with high-rotation enemies
|
|
||||||
constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
|
||||||
} // namespace Debris
|
|
||||||
} // namespace Physics
|
|
||||||
|
|
||||||
// Matemáticas
|
|
||||||
namespace Math {
|
|
||||||
constexpr float PI = std::numbers::pi_v<float>;
|
|
||||||
} // namespace Math
|
|
||||||
|
|
||||||
// Colores (oscilación para efecto CRT)
|
|
||||||
namespace Color {
|
|
||||||
// Frecuencia de oscilación
|
|
||||||
constexpr float FREQUENCY = 6.0F; // 1 Hz (1 ciclo/segundo)
|
|
||||||
|
|
||||||
// Color de líneas (efecto fósforo verde CRT)
|
|
||||||
constexpr uint8_t LINE_MIN_R = 100; // Verde oscuro
|
|
||||||
constexpr uint8_t LINE_MIN_G = 200;
|
|
||||||
constexpr uint8_t LINE_MIN_B = 100;
|
|
||||||
|
|
||||||
constexpr uint8_t LINE_MAX_R = 100; // Verde brillante
|
|
||||||
constexpr uint8_t LINE_MAX_G = 255;
|
|
||||||
constexpr uint8_t LINE_MAX_B = 100;
|
|
||||||
|
|
||||||
// Color de fondo (pulso sutil verde oscuro)
|
|
||||||
constexpr uint8_t BACKGROUND_MIN_R = 0; // Negro
|
|
||||||
constexpr uint8_t BACKGROUND_MIN_G = 5;
|
|
||||||
constexpr uint8_t BACKGROUND_MIN_B = 0;
|
|
||||||
|
|
||||||
constexpr uint8_t BACKGROUND_MAX_R = 0; // Verde muy oscuro
|
|
||||||
constexpr uint8_t BACKGROUND_MAX_G = 15;
|
|
||||||
constexpr uint8_t BACKGROUND_MAX_B = 0;
|
|
||||||
} // namespace Color
|
|
||||||
|
|
||||||
// Brillantor (control de intensitat per cada tipus d'entitat)
|
|
||||||
namespace Brightness {
|
|
||||||
// Brillantor estàtica per entitats de joc (0.0-1.0)
|
|
||||||
constexpr float NAU = 1.0F; // Màxima visibilitat (jugador)
|
|
||||||
constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys)
|
|
||||||
constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat)
|
|
||||||
|
|
||||||
// Starfield: gradient segons distància al centre
|
|
||||||
// distancia_centre: 0.0 (centre) → 1.0 (vora pantalla)
|
|
||||||
// brightness = MIN + (MAX - MIN) * distancia_centre
|
|
||||||
constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centre)
|
|
||||||
constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla)
|
|
||||||
} // namespace Brightness
|
|
||||||
|
|
||||||
// Renderització (V-Sync i altres opcions de render)
|
|
||||||
namespace Rendering {
|
|
||||||
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
|
|
||||||
} // namespace Rendering
|
|
||||||
|
|
||||||
// Audio (sistema de so i música)
|
|
||||||
namespace Audio {
|
|
||||||
constexpr float VOLUME = 1.0F; // Volumen maestro (0.0 a 1.0)
|
|
||||||
constexpr bool ENABLED = true; // Audio habilitado por defecto
|
|
||||||
} // namespace Audio
|
|
||||||
|
|
||||||
// Música (pistas de fondo)
|
|
||||||
namespace Music {
|
|
||||||
constexpr float VOLUME = 0.8F; // Volumen música
|
|
||||||
constexpr bool ENABLED = true; // Música habilitada
|
|
||||||
constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego
|
|
||||||
constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo
|
|
||||||
constexpr int FADE_DURATION_MS = 1000; // Fade out duration
|
|
||||||
} // namespace Music
|
|
||||||
|
|
||||||
// Efectes de so (sons puntuals)
|
|
||||||
namespace Sound {
|
|
||||||
constexpr float VOLUME = 1.0F; // Volumen efectos
|
|
||||||
constexpr bool ENABLED = true; // Sonidos habilitados
|
|
||||||
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
|
|
||||||
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
|
|
||||||
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
|
|
||||||
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
|
|
||||||
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
|
|
||||||
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
|
||||||
constexpr const char* LOGO = "effects/logo.wav"; // Logo
|
|
||||||
constexpr const char* START = "effects/start.wav"; // El jugador pulsa START
|
|
||||||
constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
|
|
||||||
} // namespace Sound
|
|
||||||
|
|
||||||
// Controls (mapeo de teclas para los jugadores)
|
|
||||||
namespace Controls {
|
|
||||||
namespace P1 {
|
|
||||||
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT;
|
|
||||||
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT;
|
|
||||||
constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP;
|
|
||||||
constexpr SDL_Keycode SHOOT = SDLK_SPACE;
|
|
||||||
} // namespace P1
|
|
||||||
|
|
||||||
namespace P2 {
|
|
||||||
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D;
|
|
||||||
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A;
|
|
||||||
constexpr SDL_Scancode THRUST = SDL_SCANCODE_W;
|
|
||||||
constexpr SDL_Keycode SHOOT = SDLK_LSHIFT;
|
|
||||||
} // namespace P2
|
|
||||||
} // namespace Controls
|
|
||||||
|
|
||||||
// Enemy type configuration (tipus d'enemics)
|
|
||||||
namespace Enemies {
|
|
||||||
// Pentagon (esquivador - zigzag evasion)
|
|
||||||
namespace Pentagon {
|
|
||||||
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
|
|
||||||
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
|
|
||||||
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad)
|
|
||||||
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
|
|
||||||
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
|
|
||||||
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
|
|
||||||
} // namespace Pentagon
|
|
||||||
|
|
||||||
// Quadrat (perseguidor - tracks player)
|
|
||||||
namespace Quadrat {
|
|
||||||
constexpr float VELOCITAT = 40.0F; // px/s (medium speed)
|
|
||||||
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
|
|
||||||
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
|
|
||||||
constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%]
|
|
||||||
constexpr float DROTACIO_MAX = 1.5F; // [+50%]
|
|
||||||
constexpr const char* SHAPE_FILE = "enemy_square.shp";
|
|
||||||
} // namespace Quadrat
|
|
||||||
|
|
||||||
// Molinillo (agressiu - fast straight lines, proximity spin-up)
|
|
||||||
namespace Molinillo {
|
|
||||||
constexpr float VELOCITAT = 50.0F; // px/s (fastest)
|
|
||||||
constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change)
|
|
||||||
constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments
|
|
||||||
constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%]
|
|
||||||
constexpr float DROTACIO_MAX = 6.0F; // [+50%]
|
|
||||||
constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
|
|
||||||
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
|
|
||||||
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
|
|
||||||
} // namespace Molinillo
|
|
||||||
|
|
||||||
// Animation parameters (shared)
|
|
||||||
namespace Animation {
|
|
||||||
// Palpitation
|
|
||||||
constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second
|
|
||||||
constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds)
|
|
||||||
constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds)
|
|
||||||
constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation
|
|
||||||
constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation
|
|
||||||
constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz)
|
|
||||||
constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz)
|
|
||||||
|
|
||||||
// Rotation acceleration
|
|
||||||
constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
|
|
||||||
constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time
|
|
||||||
constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time
|
|
||||||
constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
|
|
||||||
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
|
|
||||||
} // namespace Animation
|
|
||||||
|
|
||||||
// Spawn safety and invulnerability system
|
|
||||||
namespace Spawn {
|
|
||||||
// Safe spawn distance from player
|
|
||||||
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
|
|
||||||
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
|
|
||||||
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
|
|
||||||
|
|
||||||
// Invulnerability system
|
|
||||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
|
|
||||||
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim
|
|
||||||
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC)
|
|
||||||
constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible
|
|
||||||
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
|
|
||||||
} // namespace Spawn
|
|
||||||
|
|
||||||
// Scoring system (puntuació per tipus d'enemic)
|
|
||||||
namespace Scoring {
|
|
||||||
constexpr int PENTAGON_SCORE = 100; // Pentàgon (esquivador, 35 px/s)
|
|
||||||
constexpr int QUADRAT_SCORE = 150; // Quadrat (perseguidor, 40 px/s)
|
|
||||||
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
|
|
||||||
} // namespace Scoring
|
|
||||||
|
|
||||||
} // namespace Enemies
|
|
||||||
|
|
||||||
// Title scene ship animations (naus 3D flotants a l'escena de títol)
|
|
||||||
namespace Title {
|
|
||||||
namespace Ships {
|
|
||||||
// ============================================================
|
|
||||||
// PARÀMETRES BASE (ajustar aquí per experimentar)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// 1. Escala global de les naus
|
|
||||||
constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp)
|
|
||||||
|
|
||||||
// 2. Altura vertical (cercanía al centro)
|
|
||||||
// Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla)
|
|
||||||
constexpr float TARGET_Y_RATIO = 0.15625F;
|
|
||||||
|
|
||||||
// 3. Radio orbital (distancia radial desde centro en coordenadas polares)
|
|
||||||
constexpr float CLOCK_RADIUS = 150.0F; // Distància des del centre
|
|
||||||
|
|
||||||
// 4. Ángulos de posición (clock positions en coordenadas polares)
|
|
||||||
// En coordenades de pantalla: 0° = dreta, 90° = baix, 180° = esquerra, 270° = dalt
|
|
||||||
constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left)
|
|
||||||
constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right)
|
|
||||||
|
|
||||||
// 5. Radio máximo de la forma de la nave (para calcular offset automáticamente)
|
|
||||||
constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp
|
|
||||||
|
|
||||||
// 6. Margen de seguridad para offset de entrada
|
|
||||||
constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado)
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// VALORS DERIVATS (calculats automàticament - NO modificar)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Centre de la pantalla (punt de referència)
|
|
||||||
constexpr float CENTER_X = Game::WIDTH / 2.0F; // 320.0f
|
|
||||||
constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // 240.0f
|
|
||||||
|
|
||||||
// Posicions target (calculades dinàmicament des dels paràmetres base)
|
|
||||||
// Nota: std::cos/sin no són constexpr en C++20, però funcionen en runtime
|
|
||||||
// Les funcions inline són optimitzades pel compilador (zero overhead)
|
|
||||||
inline float P1_TARGET_X() {
|
|
||||||
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE));
|
|
||||||
}
|
|
||||||
inline float P1_TARGET_Y() {
|
|
||||||
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
|
|
||||||
}
|
|
||||||
inline float P2_TARGET_X() {
|
|
||||||
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE));
|
|
||||||
}
|
|
||||||
inline float P2_TARGET_Y() {
|
|
||||||
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escales d'animació (relatives a SHIP_BASE_SCALE)
|
|
||||||
constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més gran
|
|
||||||
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotant: escala base
|
|
||||||
|
|
||||||
// Offset d'entrada (ajustat automàticament a l'escala)
|
|
||||||
// Fórmula: (radi màxim de la nau * escala d'entrada) + marge
|
|
||||||
constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
|
|
||||||
|
|
||||||
// Punt de fuga (centre per a l'animació de sortida)
|
|
||||||
constexpr float VANISHING_POINT_X = CENTER_X; // 320.0f
|
|
||||||
constexpr float VANISHING_POINT_Y = CENTER_Y; // 240.0f
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// ANIMACIONS (durades, oscil·lacions, delays)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Durades d'animació
|
|
||||||
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
|
|
||||||
constexpr float EXIT_DURATION = 1.0F; // Sortida (segons)
|
|
||||||
|
|
||||||
// Flotació (oscil·lació reduïda i diferenciada per nau)
|
|
||||||
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
|
|
||||||
constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels)
|
|
||||||
|
|
||||||
// Freqüències base
|
|
||||||
constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz
|
|
||||||
constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz
|
|
||||||
constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°)
|
|
||||||
|
|
||||||
// Delays d'entrada (per a entrada escalonada)
|
|
||||||
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
|
|
||||||
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s després
|
|
||||||
|
|
||||||
// Delay global abans d'iniciar l'animació d'entrada al estat MAIN
|
|
||||||
constexpr float ENTRANCE_DELAY = 5.0F; // Temps d'espera abans que les naus entrin
|
|
||||||
|
|
||||||
// Multiplicadors de freqüència per a cada nau (variació sutil ±12%)
|
|
||||||
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
|
|
||||||
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
|
|
||||||
|
|
||||||
} // namespace Ships
|
|
||||||
|
|
||||||
namespace Layout {
|
|
||||||
// Posicions verticals (anclatges des del TOP de pantalla lògica, 0.0-1.0)
|
|
||||||
constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
|
|
||||||
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY"
|
|
||||||
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
|
|
||||||
|
|
||||||
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
|
|
||||||
constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px)
|
|
||||||
constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px)
|
|
||||||
|
|
||||||
// Factors d'escala
|
|
||||||
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
|
|
||||||
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
|
|
||||||
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
|
|
||||||
|
|
||||||
// Espaiat entre caràcters (usat per VectorText)
|
|
||||||
constexpr float TEXT_SPACING = 2.0F;
|
|
||||||
} // namespace Layout
|
|
||||||
} // namespace Title
|
|
||||||
|
|
||||||
// Floating score numbers (números flotants de puntuació)
|
|
||||||
namespace FloatingScore {
|
|
||||||
constexpr float LIFETIME = 2.0F; // Duració màxima (segons)
|
|
||||||
constexpr float VELOCITY_Y = -30.0F; // Velocitat vertical (px/s, negatiu = amunt)
|
|
||||||
constexpr float VELOCITY_X = 0.0F; // Velocitat horizontal (px/s)
|
|
||||||
constexpr float SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador)
|
|
||||||
constexpr float SPACING = 0.0F; // Espaiat entre caràcters
|
|
||||||
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
|
|
||||||
} // namespace FloatingScore
|
|
||||||
|
|
||||||
} // namespace Defaults
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// audio.hpp - Configuració d'audio (sistema), pistes de música i efectes
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
// Audio (sistema de sonido y música) — usado por Audio::Config en init()
|
||||||
|
namespace Defaults::Audio {
|
||||||
|
|
||||||
|
constexpr bool ENABLED = true; // Audio habilitado por defecto
|
||||||
|
constexpr float VOLUME = 1.0F; // Volumen maestro (0..1) — 100%
|
||||||
|
constexpr bool MUSIC_ENABLED = true; // Música habilitada
|
||||||
|
constexpr float MUSIC_VOLUME = 1.0F; // Volumen música (0..1) — 100%
|
||||||
|
constexpr bool SOUND_ENABLED = true; // Efectos habilitados
|
||||||
|
constexpr float SOUND_VOLUME = 0.25F; // Volumen efectos (0..1) — 25%
|
||||||
|
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 Defaults::Audio
|
||||||
|
|
||||||
|
// Música (pistas de fondo)
|
||||||
|
namespace Defaults::Music {
|
||||||
|
|
||||||
|
constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego
|
||||||
|
constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo
|
||||||
|
constexpr int FADE_DURATION_MS = 1000; // Fade out duration
|
||||||
|
|
||||||
|
} // namespace Defaults::Music
|
||||||
|
|
||||||
|
// Efectes de so (sons puntuals)
|
||||||
|
namespace Defaults::Sound {
|
||||||
|
|
||||||
|
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
|
||||||
|
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
|
||||||
|
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
|
||||||
|
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
|
||||||
|
constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT)
|
||||||
|
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
|
||||||
|
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
||||||
|
constexpr const char* LOGO = "effects/logo.wav"; // Logo
|
||||||
|
constexpr const char* START = "effects/start.wav"; // El player pulsa START
|
||||||
|
constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
|
||||||
|
|
||||||
|
} // namespace Defaults::Sound
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// border.hpp - Configuració del border del playfield (estàtic + reaccions)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Border {
|
||||||
|
|
||||||
|
// Desplaçament del border per impactes
|
||||||
|
constexpr float MAX_DISPLACEMENT_PX = 6.0F; // tope màxim de separació respecte la posició natural
|
||||||
|
constexpr float DISPLACEMENT_RECOVERY_PER_S = 30.0F; // px/s tornant cap a 0 (ease lineal)
|
||||||
|
|
||||||
|
// Flash al impacte. Intensitat proporcional al desplaçament:
|
||||||
|
// max displacement → color = FLASH_COLOR pur
|
||||||
|
// 0 displacement → color = oscil·lador (base verd)
|
||||||
|
// La línia es dibuixa amb el color resultant del lerp; no hi ha sobreposició.
|
||||||
|
constexpr bool FLASH_ENABLED = true;
|
||||||
|
constexpr unsigned char FLASH_COLOR_R = 180;
|
||||||
|
constexpr unsigned char FLASH_COLOR_G = 255;
|
||||||
|
constexpr unsigned char FLASH_COLOR_B = 180;
|
||||||
|
|
||||||
|
// Conversió velocitat d'impacte → strength del bump
|
||||||
|
constexpr float BUMP_VELOCITY_REFERENCE = 120.0F; // px/s donen strength 1.0
|
||||||
|
constexpr float BUMP_MIN_VELOCITY = 20.0F; // sota d'açò no genera bump (filtrar fregaments)
|
||||||
|
|
||||||
|
// Bump generat per explosions properes a la paret.
|
||||||
|
constexpr float EXPLOSION_FALLOFF_PX = 80.0F; // més enllà d'aquesta distància, sense bump
|
||||||
|
constexpr float EXPLOSION_BASE_STRENGTH = 0.7F; // strength màxim (a 0 px de la paret)
|
||||||
|
|
||||||
|
} // namespace Defaults::Border
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// brightness.hpp - Control d'intensitat per tipus d'entitat i starfield
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// La antigua oscilación CPU (namespace Color) se ha migrado al shader de
|
||||||
|
// postpro. Los parámetros de flicker / background pulse viven ahora en
|
||||||
|
// data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl.
|
||||||
|
|
||||||
|
namespace Defaults::Brightness {
|
||||||
|
|
||||||
|
// Brillantor estàtica per entidades de juego (0.0-1.0)
|
||||||
|
constexpr float NAU = 1.0F; // Màxima visibilitat (player)
|
||||||
|
constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys)
|
||||||
|
constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat)
|
||||||
|
|
||||||
|
// Starfield: gradient segons distancia al centro
|
||||||
|
// distancia_centre: 0.0 (centro) → 1.0 (vora pantalla)
|
||||||
|
// brightness = MIN + (MAX - MIN) * distancia_centre
|
||||||
|
constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centro)
|
||||||
|
constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla)
|
||||||
|
|
||||||
|
} // namespace Defaults::Brightness
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// controls.hpp - Mapeig de tecles per defecte dels jugadors
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Defaults::Controls {
|
||||||
|
|
||||||
|
namespace P1 {
|
||||||
|
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT;
|
||||||
|
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT;
|
||||||
|
constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP;
|
||||||
|
constexpr SDL_Keycode SHOOT = SDLK_SPACE;
|
||||||
|
} // namespace P1
|
||||||
|
|
||||||
|
namespace P2 {
|
||||||
|
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D;
|
||||||
|
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A;
|
||||||
|
constexpr SDL_Scancode THRUST = SDL_SCANCODE_W;
|
||||||
|
constexpr SDL_Keycode SHOOT = SDLK_LSHIFT;
|
||||||
|
} // namespace P2
|
||||||
|
|
||||||
|
} // namespace Defaults::Controls
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// effects.hpp - Constants per a efectes visuals (fireworks, etc.)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Defaults::FX::Firework {
|
||||||
|
|
||||||
|
// Color per defecte. La caller pot fer override (p.ex. heretar del pare),
|
||||||
|
// però per defecte no l'heretem — feel més neutre/lluminós.
|
||||||
|
constexpr SDL_Color DEFAULT_COLOR = {.r = 255, .g = 255, .b = 255, .a = 255};
|
||||||
|
|
||||||
|
// Velocitat inicial radial al spawn (px/s) i variació entre punts.
|
||||||
|
constexpr float SPEED = 250.0F;
|
||||||
|
constexpr float SPEED_VARIATION = 30.0F; // ±
|
||||||
|
|
||||||
|
// Quantitat de línies per burst (per defecte).
|
||||||
|
constexpr int N_POINTS = 100;
|
||||||
|
|
||||||
|
// Distribució angular: jitter aleatori sobre el repartiment uniforme.
|
||||||
|
constexpr float ANGULAR_JITTER_DEG = 12.0F;
|
||||||
|
|
||||||
|
// Fase 1 (creixement): la línia neix amb longitud 0 i creix fins a max.
|
||||||
|
constexpr float GROW_DURATION = 0.08F; // s
|
||||||
|
constexpr float MAX_LENGTH = 25.0F; // px
|
||||||
|
|
||||||
|
// Fricció lineal (px/s²). Negativa per frenar.
|
||||||
|
constexpr float FRICTION = -180.0F;
|
||||||
|
|
||||||
|
// Llindar de mort: per sota d'aquesta longitud (px) o brillor, la
|
||||||
|
// partícula es marca inactiva.
|
||||||
|
constexpr float MIN_LENGTH = 0.5F;
|
||||||
|
constexpr float MIN_BRIGHTNESS = 0.02F;
|
||||||
|
|
||||||
|
// Brillor inicial per defecte.
|
||||||
|
constexpr float INITIAL_BRIGHTNESS = 1.0F;
|
||||||
|
|
||||||
|
// Restitució en rebot contra els límits del PLAYAREA (mateix patró que debris).
|
||||||
|
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||||
|
|
||||||
|
// Mida del pool. 8 punts × ~25 bursts simultanis.
|
||||||
|
constexpr int POOL_SIZE = 2000;
|
||||||
|
|
||||||
|
} // namespace Defaults::FX::Firework
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Cuadrado/Molinillo), spawn i scoring
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/defaults/entities.hpp"
|
||||||
|
|
||||||
|
namespace Defaults::Enemies {
|
||||||
|
|
||||||
|
// Cuerpo físico común (valores por defecto del constructor)
|
||||||
|
namespace Body {
|
||||||
|
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
|
||||||
|
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
|
||||||
|
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
|
||||||
|
constexpr float ANGULAR_DAMPING = 0.0F;
|
||||||
|
} // namespace Body
|
||||||
|
|
||||||
|
// Pentagon (esquivador - zigzag evasion)
|
||||||
|
namespace Pentagon {
|
||||||
|
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
|
||||||
|
constexpr float MASS = 5.0F; // Masa estándar
|
||||||
|
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
|
||||||
|
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad)
|
||||||
|
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
|
||||||
|
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
|
||||||
|
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
|
||||||
|
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
|
||||||
|
} // namespace Pentagon
|
||||||
|
|
||||||
|
// Cuadrado (perseguidor - tracks player)
|
||||||
|
namespace Cuadrado {
|
||||||
|
constexpr float VELOCITAT = 40.0F; // px/s (medium speed)
|
||||||
|
constexpr float MASS = 8.0F; // Más pesado, "tanque"
|
||||||
|
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
|
||||||
|
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
|
||||||
|
constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%]
|
||||||
|
constexpr float DROTACIO_MAX = 1.5F; // [+50%]
|
||||||
|
constexpr const char* SHAPE_FILE = "enemy_square.shp";
|
||||||
|
} // namespace Cuadrado
|
||||||
|
|
||||||
|
// Molinillo (agressiu - fast straight lines, proximity spin-up)
|
||||||
|
namespace Molinillo {
|
||||||
|
constexpr float VELOCITAT = 50.0F; // px/s (fastest)
|
||||||
|
constexpr float MASS = 4.0F; // Más liviano, ágil
|
||||||
|
constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change)
|
||||||
|
constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments
|
||||||
|
constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%]
|
||||||
|
constexpr float DROTACIO_MAX = 6.0F; // [+50%]
|
||||||
|
constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
|
||||||
|
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
|
||||||
|
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
|
||||||
|
} // namespace Molinillo
|
||||||
|
|
||||||
|
// Animation parameters (shared)
|
||||||
|
namespace Animation {
|
||||||
|
// Palpitation
|
||||||
|
constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second
|
||||||
|
constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds)
|
||||||
|
constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds)
|
||||||
|
constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation
|
||||||
|
constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation
|
||||||
|
constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz)
|
||||||
|
constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz)
|
||||||
|
|
||||||
|
// Rotation acceleration
|
||||||
|
constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
|
||||||
|
constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time
|
||||||
|
constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time
|
||||||
|
constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
|
||||||
|
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
|
||||||
|
} // namespace Animation
|
||||||
|
|
||||||
|
// Wounded state (entre primer impacto y explosión)
|
||||||
|
namespace Wounded {
|
||||||
|
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
|
||||||
|
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
|
||||||
|
} // namespace Wounded
|
||||||
|
|
||||||
|
// Spawn safety and invulnerability system
|
||||||
|
namespace Spawn {
|
||||||
|
// Safe spawn distance from player
|
||||||
|
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
|
||||||
|
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
|
||||||
|
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
|
||||||
|
|
||||||
|
// Invulnerability system
|
||||||
|
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
|
||||||
|
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim
|
||||||
|
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC)
|
||||||
|
constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible
|
||||||
|
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
|
||||||
|
} // namespace Spawn
|
||||||
|
|
||||||
|
// Scoring system (puntuación per type de enemy)
|
||||||
|
namespace Scoring {
|
||||||
|
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
|
||||||
|
constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s)
|
||||||
|
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
|
||||||
|
} // namespace Scoring
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// entities.hpp - Configuració d'objectes del joc (límits i radis de col·lisió)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Entities {
|
||||||
|
|
||||||
|
constexpr int MAX_ORNIS = 15;
|
||||||
|
constexpr int MAX_BALES = 50;
|
||||||
|
|
||||||
|
constexpr float SHIP_RADIUS = 12.0F;
|
||||||
|
constexpr float ENEMY_RADIUS = 20.0F;
|
||||||
|
constexpr float BULLET_RADIUS = 3.0F;
|
||||||
|
|
||||||
|
} // namespace Defaults::Entities
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// floating_score.hpp - Números flotants de puntuació
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::FloatingScore {
|
||||||
|
|
||||||
|
constexpr float LIFETIME = 2.0F; // Duració màxima (segons)
|
||||||
|
constexpr float VELOCITY_Y = -30.0F; // Velocidad vertical (px/s, negatiu = amunt)
|
||||||
|
constexpr float VELOCITY_X = 0.0F; // Velocidad horizontal (px/s)
|
||||||
|
constexpr float SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador)
|
||||||
|
constexpr float SPACING = 0.0F; // Espaiat entre caràcters
|
||||||
|
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
|
||||||
|
|
||||||
|
} // namespace Defaults::FloatingScore
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// game.hpp - Dimensions del joc i regles de partida (vides, durades, colisions)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Game {
|
||||||
|
|
||||||
|
// Dimensiones base del juego (coordenadas lógicas, 16:9)
|
||||||
|
constexpr int WIDTH = 1280;
|
||||||
|
constexpr int HEIGHT = 720;
|
||||||
|
|
||||||
|
// Regles de partida
|
||||||
|
constexpr int STARTING_LIVES = 3; // Initial lives
|
||||||
|
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
|
||||||
|
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
|
||||||
|
|
||||||
|
// Valores centinela del temporitzador de mort per-jugador.
|
||||||
|
constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu
|
||||||
|
constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla)
|
||||||
|
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous)
|
||||||
|
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
|
||||||
|
|
||||||
|
// Friendly fire system
|
||||||
|
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||||
|
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
||||||
|
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
|
||||||
|
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
|
||||||
|
|
||||||
|
// Transición LEVEL_START (mensajes aleatorios PRE-level)
|
||||||
|
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
||||||
|
constexpr float LEVEL_START_TYPING_RATIO = 0.3F; // 30% escribiendo, 70% mostrando
|
||||||
|
|
||||||
|
// Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!")
|
||||||
|
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
|
||||||
|
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.05F; // ~150ms de typewriter (escan ràpid però visible)
|
||||||
|
|
||||||
|
// Transición INIT_HUD (animación inicial del HUD)
|
||||||
|
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
|
||||||
|
|
||||||
|
// Ratios de animación (inicio y fin como porcentajes del tiempo total)
|
||||||
|
// RECT (rectángulo de márgenes)
|
||||||
|
constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F;
|
||||||
|
constexpr float INIT_HUD_RECT_RATIO_END = 0.85F;
|
||||||
|
|
||||||
|
// SCORE (marcador de puntuación)
|
||||||
|
constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F;
|
||||||
|
constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F;
|
||||||
|
|
||||||
|
// SHIP1 (nave player 1)
|
||||||
|
constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F;
|
||||||
|
constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F;
|
||||||
|
|
||||||
|
// SHIP2 (nave player 2)
|
||||||
|
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
|
||||||
|
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F;
|
||||||
|
|
||||||
|
// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego)
|
||||||
|
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA
|
||||||
|
|
||||||
|
// Spawn positions (distribución horizontal para 2 jugadores)
|
||||||
|
constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda
|
||||||
|
constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda
|
||||||
|
constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba
|
||||||
|
|
||||||
|
// Continue system behavior
|
||||||
|
constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9
|
||||||
|
constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick
|
||||||
|
constexpr int MAX_CONTINUES = 3; // Maximum continues per game
|
||||||
|
constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues
|
||||||
|
|
||||||
|
// Continue screen visual configuration
|
||||||
|
namespace ContinueScreen {
|
||||||
|
// "CONTINUE" text
|
||||||
|
constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size
|
||||||
|
constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA
|
||||||
|
|
||||||
|
// Countdown number (9, 8, 7...)
|
||||||
|
constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large)
|
||||||
|
constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA
|
||||||
|
|
||||||
|
// "CONTINUES LEFT: X" text
|
||||||
|
constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small)
|
||||||
|
constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA
|
||||||
|
} // namespace ContinueScreen
|
||||||
|
|
||||||
|
// Game Over screen visual configuration
|
||||||
|
namespace GameOverScreen {
|
||||||
|
constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size
|
||||||
|
constexpr float TEXT_SPACING = 4.0F; // Character spacing
|
||||||
|
} // namespace GameOverScreen
|
||||||
|
|
||||||
|
// Stage message configuration (LEVEL_START, LEVEL_COMPLETED)
|
||||||
|
constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA
|
||||||
|
constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width
|
||||||
|
|
||||||
|
} // namespace Defaults::Game
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// hud.hpp - Configuració visual del HUD (marcador, etc.)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Defaults::Hud {
|
||||||
|
|
||||||
|
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
|
||||||
|
// y por la animación de entrada en init_hud_animator.
|
||||||
|
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
|
||||||
|
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
|
||||||
|
|
||||||
|
// Animación de entrada del HUD (init_hud_animator).
|
||||||
|
namespace InitAnim {
|
||||||
|
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
|
||||||
|
constexpr float SHIP_SPAWN_Y_OFFSET = 50.0F;
|
||||||
|
|
||||||
|
// Bordes: ratios de las tres fases (top → laterales → bottom).
|
||||||
|
constexpr float BORDER_PHASE_1_END = 0.33F; // Fin de la fase top
|
||||||
|
constexpr float BORDER_PHASE_2_END = 0.66F; // Fin de la fase laterales
|
||||||
|
} // namespace InitAnim
|
||||||
|
|
||||||
|
// Indicadores ("tips") sobre los enemigos enganchados a la nave.
|
||||||
|
// Offset local al frame de la nave (apunta hacia delante, eje Y negativo).
|
||||||
|
namespace Tips {
|
||||||
|
constexpr float LOCAL_X = 0.0F;
|
||||||
|
constexpr float LOCAL_Y = -12.0F;
|
||||||
|
} // namespace Tips
|
||||||
|
|
||||||
|
// Overlay de debug (FPS, métriques) en coordenades lògiques (1280×720).
|
||||||
|
namespace DebugOverlay {
|
||||||
|
constexpr float X = 30.0F;
|
||||||
|
constexpr float Y_FPS = 24.0F;
|
||||||
|
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
|
||||||
|
constexpr float TEXT_SCALE = 0.4F;
|
||||||
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
|
constexpr float BRIGHTNESS = 1.0F;
|
||||||
|
constexpr float FPS_UPDATE_INTERVAL = 0.5F; // Cadencia d'actualització del FPS visible
|
||||||
|
constexpr SDL_Color COLOR = {.r = 255, .g = 215, .b = 0, .a = 255}; // #FFD700 — daurat
|
||||||
|
} // namespace DebugOverlay
|
||||||
|
|
||||||
|
} // namespace Defaults::Hud
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// math.hpp - Constants matemàtiques
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <numbers>
|
||||||
|
|
||||||
|
namespace Defaults::Math {
|
||||||
|
|
||||||
|
constexpr float PI = std::numbers::pi_v<float>;
|
||||||
|
|
||||||
|
} // namespace Defaults::Math
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// notifier.hpp - Configuració del cuadre de notificacions toast (System::Notifier)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Defaults::Notifier {
|
||||||
|
|
||||||
|
// Geometria del cuadre en coordenades lògiques (1280×720).
|
||||||
|
constexpr float CANVAS_WIDTH = 1280.0F;
|
||||||
|
constexpr float MARGIN_TOP = 40.0F;
|
||||||
|
constexpr float PADDING_H = 16.0F;
|
||||||
|
constexpr float PADDING_V = 10.0F;
|
||||||
|
constexpr float BORDER_THICKNESS = 2.0F;
|
||||||
|
constexpr float TEXT_SCALE = 0.55F;
|
||||||
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
|
constexpr float BORDER_BRIGHTNESS = 1.0F;
|
||||||
|
|
||||||
|
// Cinemàtica del slide.
|
||||||
|
constexpr float SLIDE_DURATION_S = 0.30F;
|
||||||
|
|
||||||
|
// Presets per als atajos semàntics.
|
||||||
|
constexpr SDL_Color COLOR_INFO{.r = 80, .g = 230, .b = 255, .a = 255};
|
||||||
|
constexpr SDL_Color COLOR_WARN{.r = 255, .g = 180, .b = 40, .a = 255};
|
||||||
|
constexpr SDL_Color COLOR_EXIT{.r = 255, .g = 80, .b = 80, .a = 255};
|
||||||
|
constexpr float DURATION_INFO = 2.0F;
|
||||||
|
constexpr float DURATION_WARN = 3.0F;
|
||||||
|
constexpr float DURATION_EXIT = 3.0F;
|
||||||
|
|
||||||
|
} // namespace Defaults::Notifier
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// palette.hpp - Paleta semàntica per tipus d'entitat
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
// 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 Defaults::Palette {
|
||||||
|
|
||||||
|
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
|
||||||
|
// brillantor perceptual sota el bloom (sense alterar la identitat de color).
|
||||||
|
// El canal dominant es manté a 255 a cada color per maximitzar la saturació
|
||||||
|
// visible quan el halo s'expandeix.
|
||||||
|
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
||||||
|
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
|
||||||
|
constexpr SDL_Color PENTAGON = {.r = 155, .g = 195, .b = 255, .a = 255}; // Azul "esquivador"
|
||||||
|
constexpr SDL_Color QUADRAT = {.r = 255, .g = 140, .b = 140, .a = 255}; // Rojo "tank"
|
||||||
|
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 160, .b = 255, .a = 255}; // Magenta agresivo
|
||||||
|
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
|
||||||
|
|
||||||
|
} // namespace Defaults::Palette
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// physics.hpp - Constants de física del control de la nau i debris d'explosió
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Physics {
|
||||||
|
|
||||||
|
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s)
|
||||||
|
constexpr float ACCELERATION = 400.0F; // px/s²
|
||||||
|
constexpr float MAX_VELOCITY = 180.0F; // px/s
|
||||||
|
constexpr float FRICTION = 20.0F; // px/s²
|
||||||
|
|
||||||
|
// Bullet — impacto físico contra enemigo (impulse mass-aware).
|
||||||
|
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
|
||||||
|
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
|
||||||
|
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
|
||||||
|
namespace Bullet {
|
||||||
|
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
|
||||||
|
} // namespace Bullet
|
||||||
|
|
||||||
|
// Explosions (debris physics)
|
||||||
|
namespace Debris {
|
||||||
|
constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||||
|
constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s)
|
||||||
|
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
||||||
|
constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||||
|
constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
||||||
|
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
||||||
|
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
||||||
|
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
||||||
|
|
||||||
|
// Política de mort: passat el min_lifetime, el fragment mor quan la
|
||||||
|
// seva velocity cau per sota d'aquest llindar. Així els fragments
|
||||||
|
// ràpids no "popen" en moviment.
|
||||||
|
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
|
||||||
|
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
|
||||||
|
|
||||||
|
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
|
||||||
|
// 0.7 = 70% de l'energia conservada al rebot.
|
||||||
|
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||||
|
|
||||||
|
// Herència de velocity angular (trayectorias curvas)
|
||||||
|
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||||
|
constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
||||||
|
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
||||||
|
|
||||||
|
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
||||||
|
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
||||||
|
|
||||||
|
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
|
||||||
|
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||||
|
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
||||||
|
|
||||||
|
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
||||||
|
// que es passen com a paràmetres opcionals a explode()).
|
||||||
|
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
|
||||||
|
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
|
||||||
|
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
|
||||||
|
|
||||||
|
// Angular velocity sin for trajectory inheritance
|
||||||
|
// Excess above this threshold is converted to tangential linear velocity
|
||||||
|
// Prevents "vortex trap" problem with high-rotation enemies
|
||||||
|
constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
||||||
|
} // namespace Debris
|
||||||
|
|
||||||
|
} // namespace Defaults::Physics
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// playfield.hpp - Configuració del fons del playfield (graella, sub-graella, animació)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Playfield {
|
||||||
|
|
||||||
|
// Estructura de la graella (cel·les omplen tota la PLAYAREA)
|
||||||
|
constexpr int COLUMNS = 16; // cell_w = PLAYAREA.w / 16
|
||||||
|
constexpr int ROWS = 8; // cell_h = PLAYAREA.h / 8
|
||||||
|
constexpr int SUBDIVISIONS = 5; // cada cel·la principal es divideix en N subcel·les
|
||||||
|
|
||||||
|
// Brillo respecte al color global (border = 1.0)
|
||||||
|
constexpr float GRID_BRIGHTNESS = 0.15F;
|
||||||
|
constexpr float SUBGRID_BRIGHTNESS = 0.05F;
|
||||||
|
|
||||||
|
// Animació de creació amb timer intern del Playfield.
|
||||||
|
// L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en
|
||||||
|
// LINE_GROWTH_DURATION_S; els spawns es distribueixen amb sweep des del
|
||||||
|
// centre perquè verticals i horitzontals propaguen cap als extrems.
|
||||||
|
constexpr float LINE_GROWTH_DURATION_S = 0.4F;
|
||||||
|
constexpr float TOTAL_ANIMATION_DURATION_S = 3.0F; // = Defaults::Game::INIT_HUD_DURATION
|
||||||
|
|
||||||
|
// Cap brillant de la línia mentre creix (extrem que avança).
|
||||||
|
constexpr float HEAD_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant
|
||||||
|
constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border)
|
||||||
|
|
||||||
|
// Orbit (oscil·lació transversal de la línia quan la nau hi passa a prop).
|
||||||
|
constexpr float ORBIT_AMPLITUDE_MAX_PX = 3.0F; // desplaçament transversal màxim
|
||||||
|
constexpr float ORBIT_DECAY_PER_S = 4.0F; // decaiment de l'amplitud (px/s)
|
||||||
|
constexpr float ORBIT_FREQ_HZ = 8.0F; // freqüència del sin
|
||||||
|
constexpr float ORBIT_PROXIMITY_PX = 12.0F; // distància max de la línia per excitar-la
|
||||||
|
constexpr float ORBIT_SHIP_SPEED_THRESHOLD = 60.0F; // velocitat mínima per excitar (px/s)
|
||||||
|
|
||||||
|
// Pulse (reacció a fireworks: punt brillant que es propaga al llarg de la
|
||||||
|
// línia a partir del punt de spawn).
|
||||||
|
constexpr int MAX_PULSES_PER_LINE = 2;
|
||||||
|
constexpr float PULSE_LIFETIME_S = 1.0F; // temps total fins desaparèixer
|
||||||
|
constexpr float PULSE_SPREAD_PER_S = 300.0F; // px/s de propagació (cap a cada extrem)
|
||||||
|
constexpr unsigned char PULSE_COLOR_R = 180;
|
||||||
|
constexpr unsigned char PULSE_COLOR_G = 230;
|
||||||
|
constexpr unsigned char PULSE_COLOR_B = 255;
|
||||||
|
|
||||||
|
} // namespace Defaults::Playfield
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// rendering.hpp - Opcions de renderització
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace Defaults::Rendering {
|
||||||
|
|
||||||
|
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
|
||||||
|
constexpr int ANTIALIAS_DEFAULT = 1; // 0=disabled, 1=enabled (AA geomètric a les línies)
|
||||||
|
|
||||||
|
// Grosor global per defecte de les línies. 1.5 dóna línia visible i crujent;
|
||||||
|
// 1.0 es veu massa fi en pantalles grans. Configurable via setLineThickness.
|
||||||
|
constexpr float LINE_THICKNESS_DEFAULT = 1.5F;
|
||||||
|
|
||||||
|
// Resolució del render target offscreen. El tamany lògic del joc roman a
|
||||||
|
// 1280×720 (coordenades dels objectes); aquesta és la resolució física a
|
||||||
|
// la qual es rasteritzen les línies abans de la composició final.
|
||||||
|
struct ResolutionPreset {
|
||||||
|
int w;
|
||||||
|
int h;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::array<ResolutionPreset, 5> RESOLUTION_PRESETS{{
|
||||||
|
{.w = 1280, .h = 720}, // HD 720p (default)
|
||||||
|
{.w = 1600, .h = 900}, // HD+ 900p
|
||||||
|
{.w = 1920, .h = 1080}, // Full HD 1080p
|
||||||
|
{.w = 2560, .h = 1440}, // QHD 1440p
|
||||||
|
{.w = 3840, .h = 2160} // 4K UHD 2160p
|
||||||
|
}};
|
||||||
|
|
||||||
|
constexpr int RENDER_WIDTH_DEFAULT = 1280;
|
||||||
|
constexpr int RENDER_HEIGHT_DEFAULT = 720;
|
||||||
|
|
||||||
|
constexpr auto isValidRenderResolution(int w, int h) -> bool {
|
||||||
|
return std::ranges::any_of(RESOLUTION_PRESETS,
|
||||||
|
[w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; });
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Defaults::Rendering
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Ship {
|
||||||
|
|
||||||
|
// Invulnerabilidad post-respawn
|
||||||
|
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
|
||||||
|
|
||||||
|
// Parpadeo visual durante invulnerabilidad
|
||||||
|
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
|
||||||
|
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
|
||||||
|
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
|
||||||
|
|
||||||
|
// Cuerpo físico
|
||||||
|
constexpr float MASS = 10.0F; // Masa de referencia para choques
|
||||||
|
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
|
||||||
|
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
|
||||||
|
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
|
||||||
|
|
||||||
|
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
|
||||||
|
// Mantiene la sensación del Pascal original.
|
||||||
|
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
|
||||||
|
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
|
||||||
|
|
||||||
|
} // namespace Defaults::Ship
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
// title.hpp - Animacions de naves i layout de l'escena de títol
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "core/defaults/game.hpp"
|
||||||
|
#include "core/defaults/math.hpp"
|
||||||
|
|
||||||
|
// Title scene ship animations (naves 3D flotantes a l'escena de título)
|
||||||
|
namespace Defaults::Title {
|
||||||
|
|
||||||
|
namespace Ships {
|
||||||
|
// ============================================================
|
||||||
|
// PARÀMETRES BASE (ajustar aquí per experimentar)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// 1. Escala global de las naves
|
||||||
|
constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp)
|
||||||
|
|
||||||
|
// 2. Altura vertical (cercanía al centro)
|
||||||
|
// Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla)
|
||||||
|
constexpr float TARGET_Y_RATIO = 0.15625F;
|
||||||
|
|
||||||
|
// 3. Radio orbital (distance radial desde centro en coordenadas polares)
|
||||||
|
constexpr float CLOCK_RADIUS = 150.0F; // Distancia des del centro
|
||||||
|
|
||||||
|
// 4. Ángulos de posición (clock positions en coordenadas polares)
|
||||||
|
// En coordenadas de pantalla: 0° = derecha, 90° = baix, 180° = izquierda, 270° = dalt
|
||||||
|
constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left)
|
||||||
|
constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right)
|
||||||
|
|
||||||
|
// 5. Radio máximo de la shape de la nave (para calcular offset automáticamente)
|
||||||
|
constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp
|
||||||
|
|
||||||
|
// 6. Margen de seguridad para offset de entrada
|
||||||
|
constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// VALORS DERIVATS (calculats automáticoament - NO modificar)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Centro de la pantalla (point de referència)
|
||||||
|
constexpr float CENTER_X = Game::WIDTH / 2.0F; // auto-derivado de Game::WIDTH
|
||||||
|
constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // auto-derivado de Game::HEIGHT
|
||||||
|
|
||||||
|
// Posicions target (calculades dinàmicament des dels parámetros base)
|
||||||
|
// Nota: std::cos/sin no són constexpr en C++20, pero funcionen en runtime
|
||||||
|
// Les funciones inline són optimitzades por el compilador (zero overhead)
|
||||||
|
inline auto p1TargetX() -> float {
|
||||||
|
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE));
|
||||||
|
}
|
||||||
|
inline auto p1TargetY() -> float {
|
||||||
|
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
|
||||||
|
}
|
||||||
|
inline auto p2TargetX() -> float {
|
||||||
|
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE));
|
||||||
|
}
|
||||||
|
inline auto p2TargetY() -> float {
|
||||||
|
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escales de animación (relatives a SHIP_BASE_SCALE)
|
||||||
|
constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més grande
|
||||||
|
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base
|
||||||
|
|
||||||
|
// Offset de entrada (ajustat automáticoament a l'scale)
|
||||||
|
// Fórmula: (radi màxim de la ship * scale de entrada) + margen
|
||||||
|
constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
|
||||||
|
|
||||||
|
// Vec2 de fuga (centro para l'animación de salida)
|
||||||
|
constexpr float VANISHING_POINT_X = CENTER_X; // auto-derivado de Game::WIDTH
|
||||||
|
constexpr float VANISHING_POINT_Y = CENTER_Y; // auto-derivado de Game::HEIGHT
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ANIMACIONS (durades, oscil·lacions, delays)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Durades de animación
|
||||||
|
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
|
||||||
|
constexpr float EXIT_DURATION = 1.0F; // Salida (segons)
|
||||||
|
|
||||||
|
// Flotació (oscil·lació reduïda y diferenciada per ship)
|
||||||
|
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
|
||||||
|
constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels)
|
||||||
|
|
||||||
|
// Freqüències base
|
||||||
|
constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz
|
||||||
|
constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz
|
||||||
|
constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°)
|
||||||
|
|
||||||
|
// Delays de entrada (per a entrada escalonada)
|
||||||
|
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
|
||||||
|
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después
|
||||||
|
|
||||||
|
// Delay global antes de start l'animación de entrada al state MAIN
|
||||||
|
constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin
|
||||||
|
|
||||||
|
// Multiplicadors de freqüència para cada ship (variació sutil ±12%)
|
||||||
|
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
|
||||||
|
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
|
||||||
|
|
||||||
|
} // namespace Ships
|
||||||
|
|
||||||
|
namespace Layout {
|
||||||
|
// Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0)
|
||||||
|
constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
|
||||||
|
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY"
|
||||||
|
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
|
||||||
|
|
||||||
|
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
|
||||||
|
constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px)
|
||||||
|
constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px)
|
||||||
|
|
||||||
|
// Factors de scale
|
||||||
|
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
|
||||||
|
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
|
||||||
|
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
|
||||||
|
constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright
|
||||||
|
|
||||||
|
// Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT).
|
||||||
|
constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F;
|
||||||
|
|
||||||
|
// Espaiat entre caràcters (usado per VectorText)
|
||||||
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
|
} // namespace Layout
|
||||||
|
|
||||||
|
} // namespace Defaults::Title
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// trail.hpp - Configuració de l'estela de partícules de la nau
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Trail {
|
||||||
|
|
||||||
|
constexpr int POOL_SIZE = 200;
|
||||||
|
|
||||||
|
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180)
|
||||||
|
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
|
||||||
|
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
|
||||||
|
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
|
||||||
|
constexpr float REAR_OFFSET_PX = 12.0F; // distància darrere center_ (cua)
|
||||||
|
|
||||||
|
constexpr float LIFETIME_BASE_S = 1.3F;
|
||||||
|
constexpr float LIFETIME_JITTER_S = 0.3F;
|
||||||
|
|
||||||
|
constexpr float SCALE_MIN = 0.7F; // × estrella starfield (3 px punta)
|
||||||
|
constexpr float SCALE_MAX = 1.2F;
|
||||||
|
|
||||||
|
constexpr float OSCILLATION_AMP_PX = 1.8F;
|
||||||
|
constexpr float OSCILLATION_FREQ_HZ = 6.0F;
|
||||||
|
|
||||||
|
constexpr float PULSE_FREQ_HZ = 2.5F;
|
||||||
|
|
||||||
|
// Colors del pulse (interpolats sinusoïdalment per partícula)
|
||||||
|
// P1: groc viu ↔ daurat clàssic
|
||||||
|
constexpr unsigned char COLOR_A_R = 255;
|
||||||
|
constexpr unsigned char COLOR_A_G = 255;
|
||||||
|
constexpr unsigned char COLOR_A_B = 0; // #FFFF00
|
||||||
|
constexpr unsigned char COLOR_B_R = 218;
|
||||||
|
constexpr unsigned char COLOR_B_G = 165;
|
||||||
|
constexpr unsigned char COLOR_B_B = 32; // #DAA520
|
||||||
|
|
||||||
|
// P2: roig viu ↔ rosa
|
||||||
|
constexpr unsigned char COLOR_P2_A_R = 255;
|
||||||
|
constexpr unsigned char COLOR_P2_A_G = 31;
|
||||||
|
constexpr unsigned char COLOR_P2_A_B = 31; // #FF1F1F
|
||||||
|
constexpr unsigned char COLOR_P2_B_R = 255;
|
||||||
|
constexpr unsigned char COLOR_P2_B_G = 105;
|
||||||
|
constexpr unsigned char COLOR_P2_B_B = 180; // #FF69B4
|
||||||
|
|
||||||
|
} // namespace Defaults::Trail
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// window.hpp - Configuració de la finestra (mida, fullscreen, zoom)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Window {
|
||||||
|
|
||||||
|
constexpr int WIDTH = 1280;
|
||||||
|
constexpr int HEIGHT = 720;
|
||||||
|
constexpr int MIN_WIDTH = 640; // Mínimo: mitad del baseline (16:9)
|
||||||
|
constexpr int MIN_HEIGHT = 360;
|
||||||
|
// Zoom system
|
||||||
|
constexpr float BASE_ZOOM = 1.0F; // 1280x720 baseline (16:9)
|
||||||
|
constexpr float MIN_ZOOM = 0.5F; // 640x360 minimum
|
||||||
|
constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2)
|
||||||
|
constexpr bool FULLSCREEN = true; // Pantalla completa activada por defecto
|
||||||
|
|
||||||
|
} // namespace Defaults::Window
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// zones.hpp - Zones de l'àrea de joc (SDL_FRect amb càlculs automàtics per percentatges)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include "core/defaults/game.hpp"
|
||||||
|
|
||||||
|
namespace Defaults::Zones {
|
||||||
|
|
||||||
|
// --- CONFIGURACIÓ DE PORCENTATGES ---
|
||||||
|
// Todas las zones definides como a porcentajes de Game::WIDTH (640) i Game::HEIGHT (480)
|
||||||
|
|
||||||
|
// Percentatges de height (divisió vertical)
|
||||||
|
constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior
|
||||||
|
constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central
|
||||||
|
constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior
|
||||||
|
|
||||||
|
// Padding horizontal para PLAYAREA (dentro de MAIN_PLAYAREA)
|
||||||
|
constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat
|
||||||
|
|
||||||
|
// --- CÀLCULS AUTOMÀTICS DE PÍXELS ---
|
||||||
|
// Cálculos automáticos a partir dels porcentajes
|
||||||
|
|
||||||
|
// Alçades
|
||||||
|
constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT;
|
||||||
|
constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT;
|
||||||
|
constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT;
|
||||||
|
|
||||||
|
// Posicions Y
|
||||||
|
constexpr float SCOREBOARD_TOP_Y = 0.0F;
|
||||||
|
constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H;
|
||||||
|
constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H;
|
||||||
|
|
||||||
|
// Padding horizontal de PLAYAREA
|
||||||
|
constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT;
|
||||||
|
|
||||||
|
// --- ZONES FINALS (SDL_FRect) ---
|
||||||
|
|
||||||
|
// Marcador superior (reservado para futuro uso)
|
||||||
|
// Ocupa el 2% superior
|
||||||
|
constexpr SDL_FRect SCOREBOARD_TOP = {
|
||||||
|
0.0F, // x = 0.0
|
||||||
|
SCOREBOARD_TOP_Y, // y = 0.0
|
||||||
|
static_cast<float>(Game::WIDTH), // ancho completo
|
||||||
|
SCOREBOARD_TOP_H // alto
|
||||||
|
};
|
||||||
|
|
||||||
|
// Área de juego principal (contenedor del 80% central, sin padding)
|
||||||
|
// Ocupa el 88% central, ancho completo
|
||||||
|
constexpr SDL_FRect MAIN_PLAYAREA = {
|
||||||
|
0.0F, // x = 0.0
|
||||||
|
MAIN_PLAYAREA_Y, // debajo del scoreboard superior
|
||||||
|
static_cast<float>(Game::WIDTH), // ancho completo
|
||||||
|
MAIN_PLAYAREA_H // alto
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zona de juego real (con padding horizontal del 5%)
|
||||||
|
// Ocupa: dentro de MAIN_PLAYAREA, con márgenes laterales
|
||||||
|
// Se utiliza para límites del juego, colisiones, spawn
|
||||||
|
constexpr SDL_FRect PLAYAREA = {
|
||||||
|
PLAYAREA_PADDING_H, // padding horizontal
|
||||||
|
MAIN_PLAYAREA_Y, // debajo del scoreboard superior (igual que MAIN_PLAYAREA)
|
||||||
|
Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // ancho con padding
|
||||||
|
MAIN_PLAYAREA_H // alto (igual que MAIN_PLAYAREA)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Marcador inferior (marcador actual)
|
||||||
|
// Ocupa el 10% inferior
|
||||||
|
constexpr SDL_FRect SCOREBOARD = {
|
||||||
|
0.0F, // x = 0.0
|
||||||
|
SCOREBOARD_BOTTOM_Y, // fondo
|
||||||
|
static_cast<float>(Game::WIDTH), // ancho completo
|
||||||
|
SCOREBOARD_BOTTOM_H // alto
|
||||||
|
};
|
||||||
|
|
||||||
|
// Padding horizontal del marcador (para alinear zonas izquierda/derecha con PLAYAREA)
|
||||||
|
constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f;
|
||||||
|
|
||||||
|
} // namespace Defaults::Zones
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// entitat.hpp - Classe base abstracta per a totes les entitats del joc
|
|
||||||
// © 2025 Orni Attack - Arquitectura d'entitats
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "core/graphics/shape.hpp"
|
|
||||||
#include "core/types.hpp"
|
|
||||||
|
|
||||||
namespace Entities {
|
|
||||||
|
|
||||||
class Entitat {
|
|
||||||
public:
|
|
||||||
virtual ~Entitat() = default;
|
|
||||||
|
|
||||||
// Interfície principal (virtual pur)
|
|
||||||
virtual void inicialitzar() = 0;
|
|
||||||
virtual void actualitzar(float delta_time) = 0;
|
|
||||||
virtual void dibuixar() const = 0;
|
|
||||||
[[nodiscard]] virtual bool esta_actiu() const = 0;
|
|
||||||
|
|
||||||
// Interfície de col·lisió (override opcional)
|
|
||||||
[[nodiscard]] virtual float get_collision_radius() const { return 0.0F; }
|
|
||||||
[[nodiscard]] virtual bool es_collidable() const { return false; }
|
|
||||||
|
|
||||||
// Getters comuns (inline, sense overhead)
|
|
||||||
[[nodiscard]] const Punt& get_centre() const { return centre_; }
|
|
||||||
[[nodiscard]] float get_angle() const { return angle_; }
|
|
||||||
[[nodiscard]] float get_brightness() const { return brightness_; }
|
|
||||||
[[nodiscard]] const std::shared_ptr<Graphics::Shape>& get_forma() const { return forma_; }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
// Estat comú (accés directe, sense overhead)
|
|
||||||
SDL_Renderer* renderer_;
|
|
||||||
std::shared_ptr<Graphics::Shape> forma_;
|
|
||||||
Punt centre_;
|
|
||||||
float angle_{0.0F};
|
|
||||||
float brightness_{1.0F};
|
|
||||||
|
|
||||||
// Constructor protegit (classe abstracta)
|
|
||||||
Entitat(SDL_Renderer* renderer = nullptr)
|
|
||||||
: renderer_(renderer),
|
|
||||||
centre_({.x = 0.0F, .y = 0.0F}) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Entities
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// entity.hpp - Clase base abstracta para todas las entidades del juego
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// 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 auto isActive() const -> bool = 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 auto getCollisionRadius() const -> float { return 0.0F; }
|
||||||
|
[[nodiscard]] virtual auto isCollidable() const -> bool { return false; }
|
||||||
|
|
||||||
|
// Getters comunes (inline, sin overhead)
|
||||||
|
[[nodiscard]] auto getCenter() const -> const Vec2& { return center_; }
|
||||||
|
[[nodiscard]] auto getAngle() const -> float { return angle_; }
|
||||||
|
[[nodiscard]] auto getBrightness() const -> float { return brightness_; }
|
||||||
|
[[nodiscard]] auto getShape() const -> const std::shared_ptr<Graphics::Shape>& { 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
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// border.cpp - Implementació del border del playfield
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/graphics/border.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/rendering/line_renderer.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
Border::Border(Rendering::Renderer* renderer)
|
||||||
|
: renderer_(renderer) {}
|
||||||
|
|
||||||
|
void Border::update(float delta_time) {
|
||||||
|
for (auto& side : sides_) {
|
||||||
|
// Desplaçament decau cap a 0 amb ritme constant (lineal).
|
||||||
|
const float DEC = Defaults::Border::DISPLACEMENT_RECOVERY_PER_S * delta_time;
|
||||||
|
side.displacement_px = std::max(0.0F, side.displacement_px - DEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Border::bumpAt(Vec2 contact_point, float strength) {
|
||||||
|
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||||
|
const std::array<float, SIDE_COUNT> DISTANCES = {
|
||||||
|
/* TOP */ std::abs(contact_point.y - zona.y),
|
||||||
|
/* RIGHT */ std::abs((zona.x + zona.w) - contact_point.x),
|
||||||
|
/* BOTTOM */ std::abs((zona.y + zona.h) - contact_point.y),
|
||||||
|
/* LEFT */ std::abs(contact_point.x - zona.x)};
|
||||||
|
|
||||||
|
int closest_idx = 0;
|
||||||
|
float closest_dist = DISTANCES[0];
|
||||||
|
for (int i = 1; i < SIDE_COUNT; i++) {
|
||||||
|
if (DISTANCES[i] < closest_dist) {
|
||||||
|
closest_dist = DISTANCES[i];
|
||||||
|
closest_idx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyBump(closest_idx, strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Border::applyBump(int side_idx, float strength) {
|
||||||
|
const float S = std::clamp(strength, 0.0F, 1.0F);
|
||||||
|
SideState& side = sides_[static_cast<std::size_t>(side_idx)];
|
||||||
|
side.displacement_px = std::min(
|
||||||
|
Defaults::Border::MAX_DISPLACEMENT_PX,
|
||||||
|
side.displacement_px + (S * Defaults::Border::MAX_DISPLACEMENT_PX));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Lerp de l'oscil·lador (color base actual) cap a un color "flash" en
|
||||||
|
// funció de f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer
|
||||||
|
// l'use directament (sense barrejar amb el global).
|
||||||
|
auto lerpColor(SDL_Color flash, float f) -> SDL_Color {
|
||||||
|
const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
|
||||||
|
const SDL_Color BASE = Rendering::getLineColor();
|
||||||
|
const auto LERP_U8 = [&](unsigned char a, unsigned char b) {
|
||||||
|
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
||||||
|
return static_cast<unsigned char>(OUT);
|
||||||
|
};
|
||||||
|
return SDL_Color{
|
||||||
|
.r = LERP_U8(BASE.r, flash.r),
|
||||||
|
.g = LERP_U8(BASE.g, flash.g),
|
||||||
|
.b = LERP_U8(BASE.b, flash.b),
|
||||||
|
.a = 255};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Border::draw() const {
|
||||||
|
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||||
|
const int X1 = static_cast<int>(zona.x);
|
||||||
|
const int Y1 = static_cast<int>(zona.y);
|
||||||
|
const int X2 = static_cast<int>(zona.x + zona.w);
|
||||||
|
const int Y2 = static_cast<int>(zona.y + zona.h);
|
||||||
|
|
||||||
|
const int OFF_TOP = static_cast<int>(sides_[SIDE_TOP].displacement_px);
|
||||||
|
const int OFF_RIGHT = static_cast<int>(sides_[SIDE_RIGHT].displacement_px);
|
||||||
|
const int OFF_BOTTOM = static_cast<int>(sides_[SIDE_BOTTOM].displacement_px);
|
||||||
|
const int OFF_LEFT = static_cast<int>(sides_[SIDE_LEFT].displacement_px);
|
||||||
|
|
||||||
|
// Color per costat: lerp(oscil·lador → flash) en funció del desplaçament.
|
||||||
|
const SDL_Color FLASH = {
|
||||||
|
.r = Defaults::Border::FLASH_COLOR_R,
|
||||||
|
.g = Defaults::Border::FLASH_COLOR_G,
|
||||||
|
.b = Defaults::Border::FLASH_COLOR_B,
|
||||||
|
.a = 255};
|
||||||
|
const float MAX_D = Defaults::Border::MAX_DISPLACEMENT_PX;
|
||||||
|
const bool DO_FLASH = Defaults::Border::FLASH_ENABLED;
|
||||||
|
|
||||||
|
const SDL_Color C_TOP = DO_FLASH ? lerpColor(FLASH, sides_[SIDE_TOP].displacement_px / MAX_D) : SDL_Color{};
|
||||||
|
const SDL_Color C_RIGHT = DO_FLASH ? lerpColor(FLASH, sides_[SIDE_RIGHT].displacement_px / MAX_D) : SDL_Color{};
|
||||||
|
const SDL_Color C_BOTTOM = DO_FLASH ? lerpColor(FLASH, sides_[SIDE_BOTTOM].displacement_px / MAX_D) : SDL_Color{};
|
||||||
|
const SDL_Color C_LEFT = DO_FLASH ? lerpColor(FLASH, sides_[SIDE_LEFT].displacement_px / MAX_D) : SDL_Color{};
|
||||||
|
|
||||||
|
// Una sola línia per costat (brillo 1.0). Si DO_FLASH = false → alpha = 0 → usa
|
||||||
|
// el color global de l'oscil·lador.
|
||||||
|
Rendering::linea(renderer_, X1, Y1 - OFF_TOP, X2, Y1 - OFF_TOP, 1.0F, 0.0F, C_TOP);
|
||||||
|
Rendering::linea(renderer_, X2 + OFF_RIGHT, Y1, X2 + OFF_RIGHT, Y2, 1.0F, 0.0F, C_RIGHT);
|
||||||
|
Rendering::linea(renderer_, X1, Y2 + OFF_BOTTOM, X2, Y2 + OFF_BOTTOM, 1.0F, 0.0F, C_BOTTOM);
|
||||||
|
Rendering::linea(renderer_, X1 - OFF_LEFT, Y1, X1 - OFF_LEFT, Y2, 1.0F, 0.0F, C_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// border.hpp - Border del playfield amb estat (desplaçaments i flash per impactes)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Substitueix el `drawMargins()` inline de GameScene. Cada un dels 4 costats
|
||||||
|
// té estat propi (desplaçament perpendicular outward + intensitat de flash blanc)
|
||||||
|
// que decau cap a 0. Esdeveniments externs (col·lisions contra els bounds, etc.)
|
||||||
|
// criden `bumpAt()` per generar reaccions.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
class Border {
|
||||||
|
public:
|
||||||
|
explicit Border(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
|
// Decae desplaçaments i flash cap a 0.
|
||||||
|
void update(float delta_time);
|
||||||
|
|
||||||
|
// Dibuixa els 4 costats amb el seu estat actual.
|
||||||
|
void draw() const;
|
||||||
|
|
||||||
|
// Aplica un bump al costat més proper al punt de contacte.
|
||||||
|
// strength ∈ [0, 1]; valors superiors es retallen.
|
||||||
|
void bumpAt(Vec2 contact_point, float strength);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum : std::uint8_t {
|
||||||
|
SIDE_TOP = 0,
|
||||||
|
SIDE_RIGHT = 1,
|
||||||
|
SIDE_BOTTOM = 2,
|
||||||
|
SIDE_LEFT = 3,
|
||||||
|
SIDE_COUNT = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SideState {
|
||||||
|
float displacement_px{0.0F}; // outward (sempre ≥ 0); el flash es deriva d'aquí
|
||||||
|
};
|
||||||
|
|
||||||
|
void applyBump(int side_idx, float strength);
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
std::array<SideState, SIDE_COUNT> sides_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// camera3d.cpp - Implementació de la càmera 3D amb projecció en CPU
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/graphics/camera3d.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
Camera3D::Camera3D(const Vec3& position, const Vec3& target, const Vec3& up_world, float fov_y_rad, float viewport_w, float viewport_h, float near_plane, float far_plane)
|
||||||
|
: position_(position),
|
||||||
|
target_(target),
|
||||||
|
up_world_(up_world),
|
||||||
|
fov_y_rad_(fov_y_rad),
|
||||||
|
viewport_w_(viewport_w),
|
||||||
|
viewport_h_(viewport_h),
|
||||||
|
near_(near_plane),
|
||||||
|
far_(far_plane) {
|
||||||
|
recomputeBasis();
|
||||||
|
recomputeFocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setPosition(const Vec3& p) {
|
||||||
|
position_ = p;
|
||||||
|
recomputeBasis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setTarget(const Vec3& t) {
|
||||||
|
target_ = t;
|
||||||
|
recomputeBasis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setUpWorld(const Vec3& u) {
|
||||||
|
up_world_ = u;
|
||||||
|
recomputeBasis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setViewport(float w, float h) {
|
||||||
|
viewport_w_ = w;
|
||||||
|
viewport_h_ = h;
|
||||||
|
recomputeFocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setFovY(float fov_y_rad) {
|
||||||
|
fov_y_rad_ = fov_y_rad;
|
||||||
|
recomputeFocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::recomputeBasis() {
|
||||||
|
// Forward = del position cap al target.
|
||||||
|
forward_ = (target_ - position_).normalized();
|
||||||
|
// Right = up_world × forward (convenció right-handed amb Y up,
|
||||||
|
// mirant cap a +Z → right cau a +X). L'invers (forward × up_world)
|
||||||
|
// donava la base mirall i invertia l'eix X de la projecció.
|
||||||
|
right_ = up_world_.cross(forward_).normalized();
|
||||||
|
// Up ortogonal real = forward × right (manté la mà dreta).
|
||||||
|
up_ = forward_.cross(right_).normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::recomputeFocal() {
|
||||||
|
// Focal length en píxels: (viewport_height / 2) / tan(fov_y / 2).
|
||||||
|
// Assumeix píxels quadrats (focal_x == focal_y).
|
||||||
|
const float HALF_FOV = fov_y_rad_ * 0.5F;
|
||||||
|
const float TAN_HALF = std::tan(HALF_FOV);
|
||||||
|
focal_ = (TAN_HALF > 0.0F) ? ((viewport_h_ * 0.5F) / TAN_HALF) : 0.0F;
|
||||||
|
centre_x_ = viewport_w_ * 0.5F;
|
||||||
|
centre_y_ = viewport_h_ * 0.5F;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Camera3D::project(const Vec3& world) const -> std::optional<ProjectedPoint> {
|
||||||
|
const Vec3 REL = world - position_;
|
||||||
|
const float CX = REL.dot(right_);
|
||||||
|
const float CY = REL.dot(up_);
|
||||||
|
const float CZ = REL.dot(forward_);
|
||||||
|
|
||||||
|
if (CZ <= near_) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float SCALE = focal_ / CZ;
|
||||||
|
return ProjectedPoint{
|
||||||
|
.screen = Vec2{
|
||||||
|
.x = centre_x_ + (CX * SCALE),
|
||||||
|
// Flip Y: en pantalla Y creix cap avall.
|
||||||
|
.y = centre_y_ - (CY * SCALE),
|
||||||
|
},
|
||||||
|
.scale = SCALE,
|
||||||
|
.depth = CZ,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// camera3d.hpp - Càmera 3D amb projecció en perspectiva en CPU
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// La càmera viu en l'espai mundial (X dreta, Y amunt, Z davant). El mètode
|
||||||
|
// project() pren un Vec3 mundial i torna les coordenades 2D en píxels lògics
|
||||||
|
// de pantalla, més el factor d'escala focal/depth (útil per renderShape).
|
||||||
|
// Si el punt queda darrere del near plane, torna std::nullopt.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
class Camera3D {
|
||||||
|
public:
|
||||||
|
struct ProjectedPoint {
|
||||||
|
Vec2 screen; // Píxels lògics
|
||||||
|
float scale; // focal / depth (escala visual a aquesta Z)
|
||||||
|
float depth; // Profunditat en l'espai de càmera (cz)
|
||||||
|
};
|
||||||
|
|
||||||
|
Camera3D(const Vec3& position, const Vec3& target, const Vec3& up_world, float fov_y_rad, float viewport_w, float viewport_h, float near_plane = 0.1F, float far_plane = 2000.0F);
|
||||||
|
|
||||||
|
void setPosition(const Vec3& p);
|
||||||
|
void setTarget(const Vec3& t);
|
||||||
|
void setUpWorld(const Vec3& u);
|
||||||
|
void setViewport(float w, float h);
|
||||||
|
void setFovY(float fov_y_rad);
|
||||||
|
|
||||||
|
[[nodiscard]] auto project(const Vec3& world) const -> std::optional<ProjectedPoint>;
|
||||||
|
|
||||||
|
[[nodiscard]] auto position() const -> const Vec3& { return position_; }
|
||||||
|
[[nodiscard]] auto forward() const -> const Vec3& { return forward_; }
|
||||||
|
[[nodiscard]] auto nearPlane() const -> float { return near_; }
|
||||||
|
[[nodiscard]] auto farPlane() const -> float { return far_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void recomputeBasis();
|
||||||
|
void recomputeFocal();
|
||||||
|
|
||||||
|
Vec3 position_{};
|
||||||
|
Vec3 target_{};
|
||||||
|
Vec3 up_world_{};
|
||||||
|
Vec3 right_{.x = 1.0F, .y = 0.0F, .z = 0.0F};
|
||||||
|
Vec3 up_{.x = 0.0F, .y = 1.0F, .z = 0.0F};
|
||||||
|
Vec3 forward_{.x = 0.0F, .y = 0.0F, .z = 1.0F};
|
||||||
|
float fov_y_rad_{0.0F};
|
||||||
|
float viewport_w_{0.0F};
|
||||||
|
float viewport_h_{0.0F};
|
||||||
|
float near_{0.1F};
|
||||||
|
float far_{2000.0F};
|
||||||
|
float focal_{0.0F};
|
||||||
|
float centre_x_{0.0F};
|
||||||
|
float centre_y_{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
// playfield.cpp - Implementació del fons del playfield
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/graphics/playfield.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/rendering/line_renderer.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Easing cubic-out: t → 1 - (1-t)^3. Decelera prop del final.
|
||||||
|
auto easeOutCubic(float t) -> float {
|
||||||
|
const float INV = 1.0F - t;
|
||||||
|
return 1.0F - (INV * INV * INV);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lerp del color base actual (oscil·lador) cap a un color destí en
|
||||||
|
// funció de f ∈ [0, 1]. Alpha > 0 perquè line_renderer l'usi directe.
|
||||||
|
auto lerpColor(SDL_Color target, float f) -> SDL_Color {
|
||||||
|
const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
|
||||||
|
const SDL_Color BASE = Rendering::getLineColor();
|
||||||
|
const auto LERP_U8 = [&](unsigned char a, unsigned char b) {
|
||||||
|
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
||||||
|
return static_cast<unsigned char>(OUT);
|
||||||
|
};
|
||||||
|
return SDL_Color{
|
||||||
|
.r = LERP_U8(BASE.r, target.r),
|
||||||
|
.g = LERP_U8(BASE.g, target.g),
|
||||||
|
.b = LERP_U8(BASE.b, target.b),
|
||||||
|
.a = 255};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Playfield::Playfield(Rendering::Renderer* renderer)
|
||||||
|
: renderer_(renderer) {
|
||||||
|
buildLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::update(float delta_time) {
|
||||||
|
elapsed_s_ += delta_time;
|
||||||
|
|
||||||
|
// Decau l'orbit i avança la fase del sin per cada línia.
|
||||||
|
const float ORBIT_DELTA_PHASE = Defaults::Playfield::ORBIT_FREQ_HZ * 2.0F * Defaults::Math::PI * delta_time;
|
||||||
|
const float ORBIT_DEC = Defaults::Playfield::ORBIT_DECAY_PER_S * delta_time;
|
||||||
|
for (auto& line : lines_) {
|
||||||
|
line.orbit_phase += ORBIT_DELTA_PHASE;
|
||||||
|
line.orbit_amplitude = std::max(0.0F, line.orbit_amplitude - ORBIT_DEC);
|
||||||
|
|
||||||
|
// Avança els pulses; els desactiva quan acaben de vida.
|
||||||
|
for (auto& pulse : line.pulses) {
|
||||||
|
if (!pulse.active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pulse.age_s += delta_time;
|
||||||
|
if (pulse.age_s >= Defaults::Playfield::PULSE_LIFETIME_S) {
|
||||||
|
pulse.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::spawnPulseAt(Line& line, float center_t) {
|
||||||
|
for (auto& pulse : line.pulses) {
|
||||||
|
if (!pulse.active) {
|
||||||
|
pulse.active = true;
|
||||||
|
pulse.age_s = 0.0F;
|
||||||
|
pulse.center_t = std::clamp(center_t, 0.0F, 1.0F);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cap slot lliure: substituïm el més vell.
|
||||||
|
Pulse* oldest = line.pulses.data();
|
||||||
|
for (auto& pulse : line.pulses) {
|
||||||
|
if (pulse.age_s > oldest->age_s) {
|
||||||
|
oldest = &pulse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oldest->active = true;
|
||||||
|
oldest->age_s = 0.0F;
|
||||||
|
oldest->center_t = std::clamp(center_t, 0.0F, 1.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::notifyFireworkSpawn(Vec2 pos) {
|
||||||
|
// Línia vertical més propera (per posició x) i horitzontal més propera (per y).
|
||||||
|
Line* closest_v = nullptr;
|
||||||
|
Line* closest_h = nullptr;
|
||||||
|
float min_dx = std::numeric_limits<float>::max();
|
||||||
|
float min_dy = std::numeric_limits<float>::max();
|
||||||
|
for (auto& line : lines_) {
|
||||||
|
if (line.is_vertical) {
|
||||||
|
const float DX = std::abs(pos.x - line.start.x);
|
||||||
|
if (DX < min_dx) {
|
||||||
|
min_dx = DX;
|
||||||
|
closest_v = &line;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const float DY = std::abs(pos.y - line.start.y);
|
||||||
|
if (DY < min_dy) {
|
||||||
|
min_dy = DY;
|
||||||
|
closest_h = &line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closest_v != nullptr) {
|
||||||
|
const float LINE_LEN = closest_v->end.y - closest_v->start.y;
|
||||||
|
const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.y - closest_v->start.y) / LINE_LEN : 0.5F;
|
||||||
|
spawnPulseAt(*closest_v, CENTER_T);
|
||||||
|
}
|
||||||
|
if (closest_h != nullptr) {
|
||||||
|
const float LINE_LEN = closest_h->end.x - closest_h->start.x;
|
||||||
|
const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.x - closest_h->start.x) / LINE_LEN : 0.5F;
|
||||||
|
spawnPulseAt(*closest_h, CENTER_T);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::notifyShipPass(Vec2 pos, float speed_px_s) {
|
||||||
|
if (speed_px_s < Defaults::Playfield::ORBIT_SHIP_SPEED_THRESHOLD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float MAX_DIST = Defaults::Playfield::ORBIT_PROXIMITY_PX;
|
||||||
|
for (auto& line : lines_) {
|
||||||
|
// Distància perpendicular del punt a la línia (que és horitzontal o vertical).
|
||||||
|
const float DIST = line.is_vertical
|
||||||
|
? std::abs(pos.x - line.start.x)
|
||||||
|
: std::abs(pos.y - line.start.y);
|
||||||
|
if (DIST < MAX_DIST) {
|
||||||
|
line.orbit_amplitude = Defaults::Playfield::ORBIT_AMPLITUDE_MAX_PX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::buildLines() {
|
||||||
|
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||||
|
const float CELL_W = zona.w / static_cast<float>(Defaults::Playfield::COLUMNS);
|
||||||
|
const float CELL_H = zona.h / static_cast<float>(Defaults::Playfield::ROWS);
|
||||||
|
const float SUB_W = CELL_W / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
|
||||||
|
const float SUB_H = CELL_H / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
|
||||||
|
const int SUB_VERTS = Defaults::Playfield::COLUMNS * Defaults::Playfield::SUBDIVISIONS;
|
||||||
|
const int SUB_HORIZ = Defaults::Playfield::ROWS * Defaults::Playfield::SUBDIVISIONS;
|
||||||
|
|
||||||
|
std::vector<Line> verticals;
|
||||||
|
std::vector<Line> horizontals;
|
||||||
|
|
||||||
|
// Verticals: posicions i ∈ [1, SUB_VERTS-1].
|
||||||
|
for (int i = 1; i < SUB_VERTS; i++) {
|
||||||
|
const float X = zona.x + (static_cast<float>(i) * SUB_W);
|
||||||
|
const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0;
|
||||||
|
const float BRIGHTNESS = IS_MAIN
|
||||||
|
? Defaults::Playfield::GRID_BRIGHTNESS
|
||||||
|
: Defaults::Playfield::SUBGRID_BRIGHTNESS;
|
||||||
|
verticals.push_back(Line{
|
||||||
|
.start = {.x = X, .y = zona.y},
|
||||||
|
.end = {.x = X, .y = zona.y + zona.h},
|
||||||
|
.brightness = BRIGHTNESS,
|
||||||
|
.spawn_time_s = 0.0F,
|
||||||
|
.is_vertical = true,
|
||||||
|
.orbit_amplitude = 0.0F,
|
||||||
|
.orbit_phase = 0.0F,
|
||||||
|
.pulses = {}});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
|
||||||
|
for (int j = 1; j < SUB_HORIZ; j++) {
|
||||||
|
const float Y = zona.y + (static_cast<float>(j) * SUB_H);
|
||||||
|
const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0;
|
||||||
|
const float BRIGHTNESS = IS_MAIN
|
||||||
|
? Defaults::Playfield::GRID_BRIGHTNESS
|
||||||
|
: Defaults::Playfield::SUBGRID_BRIGHTNESS;
|
||||||
|
horizontals.push_back(Line{
|
||||||
|
.start = {.x = zona.x, .y = Y},
|
||||||
|
.end = {.x = zona.x + zona.w, .y = Y},
|
||||||
|
.brightness = BRIGHTNESS,
|
||||||
|
.spawn_time_s = 0.0F,
|
||||||
|
.is_vertical = false,
|
||||||
|
.orbit_amplitude = 0.0F,
|
||||||
|
.orbit_phase = 0.0F,
|
||||||
|
.pulses = {}});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ona diagonal: la línia esquerra/superior naix a t=0 i les següents
|
||||||
|
// propaguen cap a la dreta/inferior, en paral·lel. Verticals i
|
||||||
|
// horitzontals comparteixen la finestra temporal així el front arriba
|
||||||
|
// a la cantonada inferior-dreta alhora.
|
||||||
|
const float SPAWN_WINDOW =
|
||||||
|
Defaults::Playfield::TOTAL_ANIMATION_DURATION_S - Defaults::Playfield::LINE_GROWTH_DURATION_S;
|
||||||
|
const int NUM_V = static_cast<int>(verticals.size());
|
||||||
|
const int NUM_H = static_cast<int>(horizontals.size());
|
||||||
|
const float INTERVAL_V = (NUM_V > 1) ? SPAWN_WINDOW / static_cast<float>(NUM_V - 1) : 0.0F;
|
||||||
|
const float INTERVAL_H = (NUM_H > 1) ? SPAWN_WINDOW / static_cast<float>(NUM_H - 1) : 0.0F;
|
||||||
|
|
||||||
|
lines_.clear();
|
||||||
|
lines_.reserve(verticals.size() + horizontals.size());
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_V; i++) {
|
||||||
|
verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
|
||||||
|
lines_.push_back(verticals[i]);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < NUM_H; i++) {
|
||||||
|
horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H;
|
||||||
|
lines_.push_back(horizontals[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Playfield::computeLineProgress(const Line& line) const -> float {
|
||||||
|
const float LINE_ELAPSED = elapsed_s_ - line.spawn_time_s;
|
||||||
|
return std::clamp(LINE_ELAPSED / Defaults::Playfield::LINE_GROWTH_DURATION_S, 0.0F, 1.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::draw() const {
|
||||||
|
for (const auto& line : lines_) {
|
||||||
|
const float RAW_P = computeLineProgress(line);
|
||||||
|
if (RAW_P <= 0.0F) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float P = easeOutCubic(RAW_P);
|
||||||
|
|
||||||
|
// Desplaçament perpendicular per orbit (verticals → x, horitzontals → y).
|
||||||
|
const float ORBIT_OFFSET = line.orbit_amplitude * std::sin(line.orbit_phase);
|
||||||
|
const float ORBIT_DX = line.is_vertical ? ORBIT_OFFSET : 0.0F;
|
||||||
|
const float ORBIT_DY = line.is_vertical ? 0.0F : ORBIT_OFFSET;
|
||||||
|
|
||||||
|
const float START_X = line.start.x + ORBIT_DX;
|
||||||
|
const float START_Y = line.start.y + ORBIT_DY;
|
||||||
|
const float DX = line.end.x - line.start.x;
|
||||||
|
const float DY = line.end.y - line.start.y;
|
||||||
|
const float CURRENT_X = START_X + (DX * P);
|
||||||
|
const float CURRENT_Y = START_Y + (DY * P);
|
||||||
|
|
||||||
|
// Tram base (brillo de la línia).
|
||||||
|
Rendering::linea(
|
||||||
|
renderer_,
|
||||||
|
static_cast<int>(START_X),
|
||||||
|
static_cast<int>(START_Y),
|
||||||
|
static_cast<int>(CURRENT_X),
|
||||||
|
static_cast<int>(CURRENT_Y),
|
||||||
|
line.brightness);
|
||||||
|
|
||||||
|
// Cap brillant mentre creix: l'últim tram de la línia es repinta més brillant.
|
||||||
|
if (P < 1.0F) {
|
||||||
|
const float LENGTH = std::sqrt((DX * DX) + (DY * DY));
|
||||||
|
if (LENGTH > 0.0F) {
|
||||||
|
const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH));
|
||||||
|
const float HEAD_X = START_X + (DX * HEAD_T);
|
||||||
|
const float HEAD_Y = START_Y + (DY * HEAD_T);
|
||||||
|
Rendering::linea(
|
||||||
|
renderer_,
|
||||||
|
static_cast<int>(HEAD_X),
|
||||||
|
static_cast<int>(HEAD_Y),
|
||||||
|
static_cast<int>(CURRENT_X),
|
||||||
|
static_cast<int>(CURRENT_Y),
|
||||||
|
Defaults::Playfield::HEAD_BRIGHTNESS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulses: cada un és un segment brillant centrat a center_t que
|
||||||
|
// s'expandeix amb el temps i s'apaga.
|
||||||
|
const float LINE_LENGTH = std::sqrt((DX * DX) + (DY * DY));
|
||||||
|
if (LINE_LENGTH <= 0.0F) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const SDL_Color PULSE_TARGET = {
|
||||||
|
.r = Defaults::Playfield::PULSE_COLOR_R,
|
||||||
|
.g = Defaults::Playfield::PULSE_COLOR_G,
|
||||||
|
.b = Defaults::Playfield::PULSE_COLOR_B,
|
||||||
|
.a = 255};
|
||||||
|
for (const auto& pulse : line.pulses) {
|
||||||
|
if (!pulse.active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float HALF_WIDTH_T = (pulse.age_s * Defaults::Playfield::PULSE_SPREAD_PER_S) / LINE_LENGTH;
|
||||||
|
const float INTENSITY = std::max(
|
||||||
|
0.0F,
|
||||||
|
1.0F - (pulse.age_s / Defaults::Playfield::PULSE_LIFETIME_S));
|
||||||
|
const float T1 = std::clamp(pulse.center_t - HALF_WIDTH_T, 0.0F, 1.0F);
|
||||||
|
const float T2 = std::clamp(pulse.center_t + HALF_WIDTH_T, 0.0F, 1.0F);
|
||||||
|
if (T2 <= T1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float P1_X = START_X + (DX * T1);
|
||||||
|
const float P1_Y = START_Y + (DY * T1);
|
||||||
|
const float P2_X = START_X + (DX * T2);
|
||||||
|
const float P2_Y = START_Y + (DY * T2);
|
||||||
|
const SDL_Color SEG_COLOR = lerpColor(PULSE_TARGET, INTENSITY);
|
||||||
|
Rendering::linea(
|
||||||
|
renderer_,
|
||||||
|
static_cast<int>(P1_X),
|
||||||
|
static_cast<int>(P1_Y),
|
||||||
|
static_cast<int>(P2_X),
|
||||||
|
static_cast<int>(P2_Y),
|
||||||
|
1.0F,
|
||||||
|
0.0F,
|
||||||
|
SEG_COLOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// playfield.hpp - Fons del playfield (graella + sub-graella amb animació de creació)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// La graella es construeix una sola vegada al constructor. El draw és stateless:
|
||||||
|
// rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció
|
||||||
|
// li toca dibuixar segons el seu slot a la timeline.
|
||||||
|
//
|
||||||
|
// Disseny preparat per a futures capacitats:
|
||||||
|
// - Línies "vives" que reaccionen a explosions / pas de la nau (reaction_intensity).
|
||||||
|
// - Capes addicionals al fons (estrelles, gradients, scanlines).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/defaults/playfield.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
class Playfield {
|
||||||
|
public:
|
||||||
|
explicit Playfield(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
|
// Avança timers interns (creació + reaccions).
|
||||||
|
void update(float delta_time);
|
||||||
|
|
||||||
|
// Pinta la graella. La porció dibuixada de cada línia depèn del timer intern.
|
||||||
|
void draw() const;
|
||||||
|
|
||||||
|
// Notifica que una nau ha passat per (pos) a velocitat (speed_px_s).
|
||||||
|
// Si està prop d'alguna línia i va prou ràpida, la línia entra en orbit.
|
||||||
|
void notifyShipPass(Vec2 pos, float speed_px_s);
|
||||||
|
|
||||||
|
// Notifica el spawn d'un firework a (pos). Les línies V i H més properes
|
||||||
|
// generen un pulse brillant que es propaga.
|
||||||
|
void notifyFireworkSpawn(Vec2 pos);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Pulse {
|
||||||
|
bool active{false};
|
||||||
|
float center_t{0.5F}; // posició al llarg de la línia (0..1)
|
||||||
|
float age_s{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Line {
|
||||||
|
Vec2 start; // top (verticals) o left (horitzontals)
|
||||||
|
Vec2 end; // bottom (verticals) o right (horitzontals)
|
||||||
|
float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS)
|
||||||
|
float spawn_time_s; // moment de naixement
|
||||||
|
bool is_vertical; // direcció (per saber el perpendicular de l'orbit)
|
||||||
|
float orbit_amplitude; // amplitud actual de l'orbit (px, ≥ 0)
|
||||||
|
float orbit_phase; // fase del sin (avança contínuament)
|
||||||
|
std::array<Pulse, Defaults::Playfield::MAX_PULSES_PER_LINE> pulses;
|
||||||
|
};
|
||||||
|
|
||||||
|
void buildLines();
|
||||||
|
[[nodiscard]] auto computeLineProgress(const Line& line) const -> float;
|
||||||
|
static void spawnPulseAt(Line& line, float center_t);
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
std::vector<Line> lines_;
|
||||||
|
float elapsed_s_{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#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),
|
|
||||||
nom_("unnamed") {
|
nom_("unnamed") {
|
||||||
carregar(filepath);
|
load(filepath);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Shape::carregar(const std::string& filepath) {
|
auto Shape::load(const std::string& filepath) -> bool {
|
||||||
// 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) {
|
auto Shape::parseFile(const std::string& contingut) -> bool {
|
||||||
std::istringstream iss(contingut);
|
std::istringstream iss(contingut);
|
||||||
std::string line;
|
std::string line;
|
||||||
|
|
||||||
@@ -49,31 +49,31 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse command
|
// Parse command
|
||||||
if (starts_with(line, "name:")) {
|
if (startsWith(line, "name:")) {
|
||||||
nom_ = trim(extract_value(line));
|
nom_ = trim(extractValue(line));
|
||||||
} else if (starts_with(line, "scale:")) {
|
} else if (startsWith(line, "scale:")) {
|
||||||
try {
|
try {
|
||||||
escala_defecte_ = std::stof(extract_value(line));
|
escala_defecte_ = std::stof(extractValue(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 (startsWith(line, "center:")) {
|
||||||
parse_center(extract_value(line));
|
parseCenter(extractValue(line));
|
||||||
} else if (starts_with(line, "polyline:")) {
|
} else if (startsWith(line, "polyline:")) {
|
||||||
auto points = parse_points(extract_value(line));
|
auto points = parsePoints(extractValue(line));
|
||||||
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 (startsWith(line, "line:")) {
|
||||||
auto points = parse_points(extract_value(line));
|
auto points = parsePoints(extractValue(line));
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ bool Shape::parsejar_fitxer(const std::string& contingut) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper: trim whitespace
|
// Helper: trim whitespace
|
||||||
std::string Shape::trim(const std::string& str) const {
|
auto Shape::trim(const std::string& str) -> std::string {
|
||||||
const char* whitespace = " \t\n\r";
|
const char* whitespace = " \t\n\r";
|
||||||
size_t start = str.find_first_not_of(whitespace);
|
size_t start = str.find_first_not_of(whitespace);
|
||||||
if (start == std::string::npos) {
|
if (start == std::string::npos) {
|
||||||
@@ -100,9 +100,9 @@ std::string Shape::trim(const std::string& str) const {
|
|||||||
return str.substr(start, end - start + 1);
|
return str.substr(start, end - start + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: starts_with
|
// Helper: startsWith
|
||||||
bool Shape::starts_with(const std::string& str,
|
auto Shape::startsWith(const std::string& str,
|
||||||
const std::string& prefix) const {
|
const std::string& prefix) -> bool {
|
||||||
if (str.length() < prefix.length()) {
|
if (str.length() < prefix.length()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ bool Shape::starts_with(const std::string& str,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper: extract value after ':'
|
// Helper: extract value after ':'
|
||||||
std::string Shape::extract_value(const std::string& line) const {
|
auto Shape::extractValue(const std::string& line) -> std::string {
|
||||||
size_t colon = line.find(':');
|
size_t colon = line.find(':');
|
||||||
if (colon == std::string::npos) {
|
if (colon == std::string::npos) {
|
||||||
return "";
|
return "";
|
||||||
@@ -119,23 +119,23 @@ std::string Shape::extract_value(const std::string& line) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper: parse center "x, y"
|
// Helper: parse center "x, y"
|
||||||
void Shape::parse_center(const std::string& value) {
|
void Shape::parseCenter(const std::string& value) {
|
||||||
std::string val = trim(value);
|
std::string val = trim(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 {
|
auto Shape::parsePoints(const std::string& str) -> std::vector<Vec2> {
|
||||||
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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// shape.hpp - Sistema de formes vectorials
|
// shape.hpp - Sistema de formes vectorials
|
||||||
// © 2025 Port a C++20 amb SDL3
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -10,55 +11,57 @@
|
|||||||
|
|
||||||
namespace Graphics {
|
namespace Graphics {
|
||||||
|
|
||||||
// Tipus de primitiva dins d'una forma
|
// Tipo de primitiva dins de una shape
|
||||||
enum class PrimitiveType {
|
enum class PrimitiveType : std::uint8_t {
|
||||||
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);
|
auto load(const std::string& filepath) -> bool;
|
||||||
|
|
||||||
// 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);
|
auto parseFile(const std::string& contingut) -> bool;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
[[nodiscard]] const std::vector<ShapePrimitive>& get_primitives() const {
|
[[nodiscard]] auto getPrimitives() const -> const std::vector<ShapePrimitive>& {
|
||||||
return primitives_;
|
return primitives_;
|
||||||
}
|
}
|
||||||
[[nodiscard]] const Punt& get_centre() const { return centre_; }
|
[[nodiscard]] auto getCenter() const -> const Vec2& { return center_; }
|
||||||
[[nodiscard]] float get_escala_defecte() const { return escala_defecte_; }
|
[[nodiscard]] auto getDefaultScale() const -> float { return escala_defecte_; }
|
||||||
[[nodiscard]] bool es_valida() const { return !primitives_.empty(); }
|
[[nodiscard]] auto isValid() const -> bool { return !primitives_.empty(); }
|
||||||
|
|
||||||
// Info de depuració
|
// Info de depuració
|
||||||
[[nodiscard]] std::string get_nom() const { return nom_; }
|
[[nodiscard]] auto getName() const -> const std::string& { return nom_; }
|
||||||
[[nodiscard]] size_t get_num_primitives() const { return primitives_.size(); }
|
[[nodiscard]] auto getNumPrimitives() const -> size_t { return primitives_.size(); }
|
||||||
|
|
||||||
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_{1.0F}; // Escala per defecte (normalment 1.0). Inicializada para
|
||||||
std::string nom_; // Nom de la forma (per depuració)
|
// que el ctor por defecto no deje el campo indeterminado.
|
||||||
|
std::string nom_; // Nom de la shape (per depuració)
|
||||||
|
|
||||||
// Helpers privats per parsejar
|
// Helpers privats per parsejar. Son estáticos: no necesitan estado
|
||||||
[[nodiscard]] std::string trim(const std::string& str) const;
|
// de instancia, trabajan sobre el string pasado por parámetro.
|
||||||
[[nodiscard]] bool starts_with(const std::string& str, const std::string& prefix) const;
|
[[nodiscard]] static auto trim(const std::string& str) -> std::string;
|
||||||
[[nodiscard]] std::string extract_value(const std::string& line) const;
|
[[nodiscard]] static auto startsWith(const std::string& str, const std::string& prefix) -> bool;
|
||||||
void parse_center(const std::string& value);
|
[[nodiscard]] static auto extractValue(const std::string& line) -> std::string;
|
||||||
[[nodiscard]] std::vector<Punt> parse_points(const std::string& str) const;
|
void parseCenter(const std::string& value);
|
||||||
|
[[nodiscard]] static auto parsePoints(const std::string& str) -> std::vector<Vec2>;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#include "core/graphics/shape_loader.hpp"
|
#include "core/graphics/shape_loader.hpp"
|
||||||
|
|
||||||
@@ -9,78 +9,62 @@
|
|||||||
|
|
||||||
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::shared_ptr<Shape> ShapeLoader::load(const std::string& filename) {
|
auto ShapeLoader::load(const std::string& filename) -> std::shared_ptr<Shape> {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
auto it = cache_.find(filename);
|
auto it = cache.find(filename);
|
||||||
if (it != cache_.end()) {
|
if (it != cache.end()) {
|
||||||
std::cout << "[ShapeLoader] Cache hit: " << filename << '\n';
|
std::cout << "[ShapeLoader] Cache hit: " << filename << '\n';
|
||||||
return it->second; // Cache hit
|
return it->second; // Cache hit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize path: "ship.shp" → "shapes/ship.shp"
|
||||||
|
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
|
||||||
|
std::string normalized = filename;
|
||||||
|
if (!normalized.starts_with("shapes/")) {
|
||||||
|
// Doesn't start with "shapes/", so add it
|
||||||
|
normalized = "shapes/" + normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load from resource system
|
||||||
|
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
||||||
|
if (data.empty()) {
|
||||||
|
std::cerr << "[ShapeLoader] Error: no s'ha pogut load " << normalized
|
||||||
|
<< '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert bytes to string and parse
|
||||||
|
std::string file_content(data.begin(), data.end());
|
||||||
|
auto shape = std::make_shared<Shape>();
|
||||||
|
if (!shape->parseFile(file_content)) {
|
||||||
|
std::cerr << "[ShapeLoader] Error: no s'ha pogut parsejar " << normalized
|
||||||
|
<< '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify shape is valid
|
||||||
|
if (!shape->isValid()) {
|
||||||
|
std::cerr << "[ShapeLoader] Error: shape invàlida " << normalized << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache and return
|
||||||
|
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
|
||||||
|
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n';
|
||||||
|
|
||||||
|
cache[filename] = shape;
|
||||||
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize path: "ship.shp" → "shapes/ship.shp"
|
void ShapeLoader::clearCache() {
|
||||||
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
|
std::cout << "[ShapeLoader] Netejant caché (" << cache.size() << " formes)"
|
||||||
std::string normalized = filename;
|
|
||||||
if (!normalized.starts_with("shapes/")) {
|
|
||||||
// Doesn't start with "shapes/", so add it
|
|
||||||
normalized = "shapes/" + normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load from resource system
|
|
||||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
|
||||||
if (data.empty()) {
|
|
||||||
std::cerr << "[ShapeLoader] Error: no s'ha pogut carregar " << normalized
|
|
||||||
<< '\n';
|
<< '\n';
|
||||||
return nullptr;
|
cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert bytes to string and parse
|
auto ShapeLoader::getCacheSize() -> size_t { return cache.size(); }
|
||||||
std::string file_content(data.begin(), data.end());
|
|
||||||
auto shape = std::make_shared<Shape>();
|
|
||||||
if (!shape->parsejar_fitxer(file_content)) {
|
|
||||||
std::cerr << "[ShapeLoader] Error: no s'ha pogut parsejar " << normalized
|
|
||||||
<< '\n';
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify shape is valid
|
|
||||||
if (!shape->es_valida()) {
|
|
||||||
std::cerr << "[ShapeLoader] Error: forma invàlida " << normalized << '\n';
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache and return
|
|
||||||
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->get_nom()
|
|
||||||
<< ", " << shape->get_num_primitives() << " primitives)" << '\n';
|
|
||||||
|
|
||||||
cache_[filename] = shape;
|
|
||||||
return shape;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShapeLoader::clear_cache() {
|
|
||||||
std::cout << "[ShapeLoader] Netejant caché (" << cache_.size() << " formes)"
|
|
||||||
<< '\n';
|
|
||||||
cache_.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t ShapeLoader::get_cache_size() { return cache_.size(); }
|
|
||||||
|
|
||||||
std::string ShapeLoader::resolve_path(const std::string& filename) {
|
|
||||||
// Si és un path absolut (comença amb '/'), usar-lo directament
|
|
||||||
if (!filename.empty() && filename[0] == '/') {
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si ja conté el prefix base_path, usar-lo directament
|
|
||||||
if (filename.starts_with(base_path_)) {
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Altrament, afegir base_path (ara suporta subdirectoris)
|
|
||||||
return base_path_ + filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -11,29 +11,25 @@
|
|||||||
|
|
||||||
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 auto load(const std::string& filename) -> std::shared_ptr<Shape>;
|
||||||
|
|
||||||
// Netejar caché (útil per debug/recàrrega)
|
// Netejar caché (útil per debug/recàrrega)
|
||||||
static void clear_cache();
|
static void clearCache();
|
||||||
|
|
||||||
// Estadístiques (debug)
|
// Estadístiques (debug)
|
||||||
static size_t get_cache_size();
|
static auto getCacheSize() -> size_t;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::unordered_map<std::string, std::shared_ptr<Shape>> cache_;
|
static std::unordered_map<std::string, std::shared_ptr<Shape>> cache;
|
||||||
static std::string base_path_; // "data/shapes/"
|
};
|
||||||
|
|
||||||
// Helpers privats
|
|
||||||
static std::string resolve_path(const std::string& filename);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// starfield.cpp - Implementació del sistema d'estrelles de fons
|
// starfield.cpp - Implementació del sistema de estrelles de fons
|
||||||
// © 2025 Orni Attack
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#include "core/graphics/starfield.hpp"
|
#include "core/graphics/starfield.hpp"
|
||||||
|
|
||||||
@@ -14,38 +14,35 @@
|
|||||||
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),
|
: shape_estrella_(ShapeLoader::load("star.shp")),
|
||||||
|
renderer_(renderer),
|
||||||
punt_fuga_(punt_fuga),
|
punt_fuga_(punt_fuga),
|
||||||
area_(area),
|
area_(area) {
|
||||||
densitat_(densitat) {
|
if (!shape_estrella_ || !shape_estrella_->isValid()) {
|
||||||
// Carregar forma d'estrella amb ShapeLoader
|
std::cerr << "ERROR: No s'ha pogut load star.shp" << '\n';
|
||||||
shape_estrella_ = ShapeLoader::load("star.shp");
|
|
||||||
|
|
||||||
if (!shape_estrella_ || !shape_estrella_->es_valida()) {
|
|
||||||
std::cerr << "ERROR: No s'ha pogut carregar star.shp" << '\n';
|
|
||||||
return;
|
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++) {
|
||||||
@@ -53,57 +50,57 @@ Starfield::Starfield(SDL_Renderer* renderer,
|
|||||||
estrella.capa = capa_idx;
|
estrella.capa = capa_idx;
|
||||||
|
|
||||||
// Angle aleatori
|
// Angle aleatori
|
||||||
estrella.angle = (static_cast<float>(rand()) / RAND_MAX) * 2.0F * Defaults::Math::PI;
|
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(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()) / static_cast<float>(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::initStar(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()) / static_cast<float>(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 {
|
auto Starfield::isOutsideArea(const Estrella& estrella) const -> bool {
|
||||||
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 {
|
auto Starfield::computeScale(const Estrella& estrella) const -> float {
|
||||||
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 {
|
auto Starfield::computeBrightness(const Estrella& estrella) const -> float {
|
||||||
// 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,58 +109,57 @@ 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_;
|
||||||
|
|
||||||
// Si ha sortit de l'àrea, regenerar-la
|
// Si ha sortit de l'àrea, regenerar-la
|
||||||
if (fora_area(estrella)) {
|
if (isOutsideArea(estrella)) {
|
||||||
inicialitzar_estrella(estrella);
|
initStar(estrella);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Establir multiplicador de brightness
|
// Establir multiplicador de brightness
|
||||||
void Starfield::set_brightness(float multiplier) {
|
void Starfield::setBrightness(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 = computeScale(estrella);
|
||||||
float brightness = calcular_brightness(estrella);
|
float brightness = computeBrightness(estrella);
|
||||||
|
|
||||||
// Renderitzar estrella sense rotació
|
// Renderizar estrella sin rotación
|
||||||
Rendering::render_shape(
|
Rendering::renderShape(
|
||||||
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#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,69 @@
|
|||||||
|
|
||||||
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 setVanishingPoint(const Vec2& point) { punt_fuga_ = point; }
|
||||||
void set_brightness(float multiplier);
|
void setBrightness(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 initStar(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]] auto isOutsideArea(const Estrella& estrella) const -> bool;
|
||||||
|
|
||||||
// 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]] auto computeScale(const Estrella& estrella) const -> float;
|
||||||
|
|
||||||
// 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]] auto computeBrightness(const Estrella& estrella) const -> float;
|
||||||
|
|
||||||
// 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
|
float multiplicador_brightness_{1.0F}; // Multiplicador de brightness (1.0 = default)
|
||||||
float multiplicador_brightness_{1.0F}; // Multiplicador de brillantor (1.0 = default)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// starfield3d.cpp - Implementació del starfield 3D
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/graphics/starfield3d.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "core/defaults.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció
|
||||||
|
// que la resta del joc — veure starfield.cpp).
|
||||||
|
auto randFloat01() -> float {
|
||||||
|
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto randRange(float lo, float hi) -> float {
|
||||||
|
return lo + (randFloat01() * (hi - lo));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Starfield3D::Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density)
|
||||||
|
: renderer_(renderer),
|
||||||
|
camera_(camera),
|
||||||
|
octahedron_(makeOctahedron()) {
|
||||||
|
stars_.resize(static_cast<std::size_t>(std::max(0, density)));
|
||||||
|
for (auto& star : stars_) {
|
||||||
|
// Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far.
|
||||||
|
initStar(star, /*spawn_at_far=*/false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Starfield3D::initStar(Star& star, bool spawn_at_far) {
|
||||||
|
star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X);
|
||||||
|
star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y);
|
||||||
|
star.position.z = spawn_at_far
|
||||||
|
? Z_FAR_SPAWN
|
||||||
|
: randRange(Z_NEAR_RESPAWN, Z_FAR_SPAWN);
|
||||||
|
|
||||||
|
star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z);
|
||||||
|
star.rot_phase_y = randFloat01() * Defaults::Math::PI * 2.0F;
|
||||||
|
star.rot_phase_x = randFloat01() * Defaults::Math::PI * 2.0F;
|
||||||
|
star.rot_speed_y = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
|
||||||
|
star.rot_speed_x = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
|
||||||
|
star.scale = STAR_BASE_SCALE + (randRange(-1.0F, 1.0F) * STAR_SCALE_JITTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Starfield3D::computeBrightness(const Star& star) const -> float {
|
||||||
|
// Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN].
|
||||||
|
const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN;
|
||||||
|
const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN;
|
||||||
|
const float T = std::clamp(T_RAW, 0.0F, 1.0F);
|
||||||
|
const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR));
|
||||||
|
return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Starfield3D::update(float delta_time) {
|
||||||
|
for (auto& star : stars_) {
|
||||||
|
star.position.z += star.velocity_z * delta_time;
|
||||||
|
star.rot_phase_y += star.rot_speed_y * delta_time;
|
||||||
|
star.rot_phase_x += star.rot_speed_x * delta_time;
|
||||||
|
|
||||||
|
if (star.position.z < Z_NEAR_RESPAWN) {
|
||||||
|
initStar(star, /*spawn_at_far=*/true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Starfield3D::draw() const {
|
||||||
|
if (camera_ == nullptr || renderer_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ordena de més lluny a més a prop perquè el blending additiu acumule
|
||||||
|
// brightness sense que els elements de davant queden tapats pels de
|
||||||
|
// darrere. Còpia d'índexs per no modificar l'ordre intern de stars_.
|
||||||
|
std::vector<std::size_t> order(stars_.size());
|
||||||
|
for (std::size_t i = 0; i < order.size(); ++i) {
|
||||||
|
order[i] = i;
|
||||||
|
}
|
||||||
|
std::ranges::sort(order, [&](std::size_t a, std::size_t b) {
|
||||||
|
return stars_[a].position.z > stars_[b].position.z;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (std::size_t idx : order) {
|
||||||
|
const Star& star = stars_[idx];
|
||||||
|
const Transform3D TRANSFORM{
|
||||||
|
.position = star.position,
|
||||||
|
.rotation_euler = Vec3{.x = star.rot_phase_x, .y = star.rot_phase_y, .z = 0.0F},
|
||||||
|
.scale = star.scale,
|
||||||
|
};
|
||||||
|
drawWireframe(renderer_, *camera_, octahedron_, TRANSFORM, computeBrightness(star));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Starfield3D::setBrightness(float multiplier) {
|
||||||
|
brightness_mult_ = std::max(0.0F, multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// starfield3d.hpp - Camp de estrelles 3D real per a l'escena de títol
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Equivalent 3D del `Graphics::Starfield`. Cada estrella és un octaedre
|
||||||
|
// wireframe situat en l'espai mundial (Vec3). Es desplacen cap a la càmera
|
||||||
|
// (Z disminueix); quan creuen el pla Z_NEAR_RESPAWN es regeneren a Z_FAR_SPAWN
|
||||||
|
// amb X/Y aleatori. Cada octaedre rota lentament sobre Y i X per donar volum.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/graphics/camera3d.hpp"
|
||||||
|
#include "core/graphics/wireframe3d.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
class Starfield3D {
|
||||||
|
public:
|
||||||
|
Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200);
|
||||||
|
|
||||||
|
void update(float delta_time);
|
||||||
|
void draw() const;
|
||||||
|
|
||||||
|
void setBrightness(float multiplier);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Star {
|
||||||
|
Vec3 position{};
|
||||||
|
float velocity_z{0.0F}; // Negatiu: cap a càmera
|
||||||
|
float rot_phase_y{0.0F};
|
||||||
|
float rot_phase_x{0.0F};
|
||||||
|
float rot_speed_y{0.0F};
|
||||||
|
float rot_speed_x{0.0F};
|
||||||
|
float scale{1.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
static void initStar(Star& star, bool spawn_at_far);
|
||||||
|
[[nodiscard]] auto computeBrightness(const Star& star) const -> float;
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
const Camera3D* camera_;
|
||||||
|
std::vector<Star> stars_;
|
||||||
|
Mesh3D octahedron_;
|
||||||
|
float brightness_mult_{1.0F};
|
||||||
|
|
||||||
|
// Volum de spawn / regeneració en l'espai 3D.
|
||||||
|
static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera
|
||||||
|
static constexpr float Z_FAR_SPAWN = 800.0F; // Z de regeneració (lluny)
|
||||||
|
static constexpr float HALF_SPAWN_X = 600.0F; // X aleatori dins [-, +]
|
||||||
|
static constexpr float HALF_SPAWN_Y = 360.0F; // Y aleatori dins [-, +]
|
||||||
|
|
||||||
|
// Mida i moviment.
|
||||||
|
static constexpr float STAR_BASE_SCALE = 1.8F;
|
||||||
|
static constexpr float STAR_SCALE_JITTER = 0.6F;
|
||||||
|
static constexpr float MIN_VELOCITY_Z = 80.0F;
|
||||||
|
static constexpr float MAX_VELOCITY_Z = 200.0F;
|
||||||
|
static constexpr float MIN_ROT_SPEED = 0.2F;
|
||||||
|
static constexpr float MAX_ROT_SPEED = 0.8F;
|
||||||
|
|
||||||
|
// Brightness en funció de la distància Z (a prop = més brillant).
|
||||||
|
static constexpr float BRIGHTNESS_FAR = 0.15F;
|
||||||
|
static constexpr float BRIGHTNESS_NEAR = 1.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -1,6 +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
|
// © 2026 JailDesigner
|
||||||
// Test pre-commit hook
|
|
||||||
|
|
||||||
#include "core/graphics/vector_text.hpp"
|
#include "core/graphics/vector_text.hpp"
|
||||||
|
|
||||||
@@ -11,276 +10,274 @@
|
|||||||
|
|
||||||
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 BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter
|
||||||
constexpr float char_height = 40.0F; // Altura base del caràcter
|
constexpr float BASE_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();
|
loadCharset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VectorText::load_charset() {
|
void VectorText::loadCharset() {
|
||||||
// Cargar dígitos 0-9
|
// Cargar dígitos 0-9
|
||||||
for (char c = '0'; c <= '9'; c++) {
|
for (char c = '0'; c <= '9'; c++) {
|
||||||
std::string filename = get_shape_filename(c);
|
std::string filename = getShapeFilename(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 lletres A-Z (majúscules)
|
||||||
|
for (char c = 'A'; c <= 'Z'; c++) {
|
||||||
|
std::string filename = getShapeFilename(c);
|
||||||
|
auto shape = ShapeLoader::load(filename);
|
||||||
|
|
||||||
|
if (shape && shape->isValid()) {
|
||||||
|
chars_[c] = shape;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar símbolos
|
||||||
|
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
|
||||||
|
for (const auto& sym : SYMBOLS) {
|
||||||
|
char c = sym[0];
|
||||||
|
std::string filename = getShapeFilename(c);
|
||||||
|
auto shape = ShapeLoader::load(filename);
|
||||||
|
|
||||||
|
if (shape && shape->isValid()) {
|
||||||
|
chars_[c] = shape;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar símbolo de copyright (©) - UTF-8 U+00A9.
|
||||||
|
// Usamos el segundo byte (0xA9, 169 decimal) como key interna del map.
|
||||||
|
{
|
||||||
|
const std::string FILENAME = "font/char_copyright.shp";
|
||||||
|
auto shape = ShapeLoader::load(FILENAME);
|
||||||
|
|
||||||
|
if (shape && shape->isValid()) {
|
||||||
|
chars_['\xA9'] = shape;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[VectorText] Warning: no s'ha pogut load " << FILENAME
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
auto VectorText::getShapeFilename(char c) -> std::string {
|
||||||
|
// Mapeo carácter → nombre de archivo (con prefix "font/").
|
||||||
|
// Dígitos 0-9 y mayúsculas A-Z comparten el mismo path: la shape se llama
|
||||||
|
// como el caracter mismo, así que se agrupan en un único case.
|
||||||
|
switch (c) {
|
||||||
|
case '0':
|
||||||
|
case '1':
|
||||||
|
case '2':
|
||||||
|
case '3':
|
||||||
|
case '4':
|
||||||
|
case '5':
|
||||||
|
case '6':
|
||||||
|
case '7':
|
||||||
|
case '8':
|
||||||
|
case '9':
|
||||||
|
case 'A':
|
||||||
|
case 'B':
|
||||||
|
case 'C':
|
||||||
|
case 'D':
|
||||||
|
case 'E':
|
||||||
|
case 'F':
|
||||||
|
case 'G':
|
||||||
|
case 'H':
|
||||||
|
case 'I':
|
||||||
|
case 'J':
|
||||||
|
case 'K':
|
||||||
|
case 'L':
|
||||||
|
case 'M':
|
||||||
|
case 'N':
|
||||||
|
case 'O':
|
||||||
|
case 'P':
|
||||||
|
case 'Q':
|
||||||
|
case 'R':
|
||||||
|
case 'S':
|
||||||
|
case 'T':
|
||||||
|
case 'U':
|
||||||
|
case 'V':
|
||||||
|
case 'W':
|
||||||
|
case 'X':
|
||||||
|
case 'Y':
|
||||||
|
case 'Z':
|
||||||
|
return std::string("font/char_") + c + ".shp";
|
||||||
|
|
||||||
|
// Lletres minúscules a-z (convertir a majúscules)
|
||||||
|
case 'a':
|
||||||
|
case 'b':
|
||||||
|
case 'c':
|
||||||
|
case 'd':
|
||||||
|
case 'e':
|
||||||
|
case 'f':
|
||||||
|
case 'g':
|
||||||
|
case 'h':
|
||||||
|
case 'i':
|
||||||
|
case 'j':
|
||||||
|
case 'k':
|
||||||
|
case 'l':
|
||||||
|
case 'm':
|
||||||
|
case 'n':
|
||||||
|
case 'o':
|
||||||
|
case 'p':
|
||||||
|
case 'q':
|
||||||
|
case 'r':
|
||||||
|
case 's':
|
||||||
|
case 't':
|
||||||
|
case 'u':
|
||||||
|
case 'v':
|
||||||
|
case 'w':
|
||||||
|
case 'x':
|
||||||
|
case 'y':
|
||||||
|
case 'z':
|
||||||
|
return std::string("font/char_") + char(c - 32) + ".shp";
|
||||||
|
|
||||||
|
// Símbols
|
||||||
|
case '.':
|
||||||
|
return "font/char_dot.shp";
|
||||||
|
case ',':
|
||||||
|
return "font/char_comma.shp";
|
||||||
|
case '-':
|
||||||
|
return "font/char_minus.shp";
|
||||||
|
case ':':
|
||||||
|
return "font/char_colon.shp";
|
||||||
|
case '!':
|
||||||
|
return "font/char_exclamation.shp";
|
||||||
|
case '?':
|
||||||
|
return "font/char_question.shp";
|
||||||
|
case ' ':
|
||||||
|
return ""; // Espai es maneja sin load shape
|
||||||
|
|
||||||
|
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
|
||||||
|
return "font/char_copyright.shp";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ""; // Caràcter no suportat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar lletres A-Z (majúscules)
|
auto VectorText::isSupported(char c) const -> bool {
|
||||||
for (char c = 'A'; c <= 'Z'; c++) {
|
return chars_.contains(c);
|
||||||
std::string filename = get_shape_filename(c);
|
}
|
||||||
auto shape = ShapeLoader::load(filename);
|
|
||||||
|
|
||||||
if (shape && shape->es_valida()) {
|
void VectorText::render(const std::string& text, const Vec2& position, float scale, float spacing, float brightness, SDL_Color color) const {
|
||||||
chars_[c] = shape;
|
if (renderer_ == nullptr) {
|
||||||
} else {
|
return;
|
||||||
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
|
}
|
||||||
<< '\n';
|
|
||||||
|
// Ancho de un carácter base (20 px a scale 1.0)
|
||||||
|
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
|
||||||
|
|
||||||
|
// Spacing escalado
|
||||||
|
const float SPACING_SCALED = spacing * scale;
|
||||||
|
|
||||||
|
// Altura de un carácter escalado (necesario para ajustar Y)
|
||||||
|
const float CHAR_HEIGHT_SCALED = BASE_CHAR_HEIGHT * scale;
|
||||||
|
|
||||||
|
// Posición X del borde izquierdo del carácter actual
|
||||||
|
// (se ajustará +BASE_CHAR_WIDTH/2 para obtener el centro al renderizar)
|
||||||
|
float current_x = position.x;
|
||||||
|
|
||||||
|
// Iterar sobre cada byte del string (con detecció UTF-8)
|
||||||
|
for (size_t i = 0; i < text.length(); i++) {
|
||||||
|
auto c = static_cast<unsigned char>(text[i]);
|
||||||
|
|
||||||
|
// Detectar copyright UTF-8 (0xC2 0xA9)
|
||||||
|
if (c == 0xC2 && i + 1 < text.length() &&
|
||||||
|
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
||||||
|
c = 0xA9; // Usar segon byte como a key
|
||||||
|
i++; // Saltar el següent byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar espacios (avanzar sin dibujar)
|
||||||
|
if (c == ' ') {
|
||||||
|
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el carácter está soportado
|
||||||
|
auto it = chars_.find(c);
|
||||||
|
if (it != chars_.end()) {
|
||||||
|
// Renderizar carácter
|
||||||
|
// Ajustar X e Y para que position represente esquina superior izquierda
|
||||||
|
// (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
|
||||||
|
Vec2 char_pos = {.x = current_x + (CHAR_WIDTH_SCALED / 2.0F), .y = position.y + (CHAR_HEIGHT_SCALED / 2.0F)};
|
||||||
|
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color);
|
||||||
|
|
||||||
|
// Avanzar posición
|
||||||
|
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
||||||
|
} else {
|
||||||
|
// Carácter no soportado: saltar (o renderizar '?' en el futuro)
|
||||||
|
std::cerr << "[VectorText] Warning: caràcter no suportat '" << c << "'"
|
||||||
|
<< '\n';
|
||||||
|
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar símbolos
|
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness, SDL_Color color) const {
|
||||||
const std::string symbols[] = {".", ",", "-", ":", "!", "?"};
|
// Calcular dimensions del text
|
||||||
for (const auto& sym : symbols) {
|
float text_width = getTextWidth(text, scale, spacing);
|
||||||
char c = sym[0];
|
float text_height = getTextHeight(scale);
|
||||||
std::string filename = get_shape_filename(c);
|
|
||||||
auto shape = ShapeLoader::load(filename);
|
|
||||||
|
|
||||||
if (shape && shape->es_valida()) {
|
// Calcular posición de l'esquina superior izquierda
|
||||||
chars_[c] = shape;
|
// restant la meitat de las dimensions del point central
|
||||||
} else {
|
Vec2 posicio_esquerra = {
|
||||||
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
|
.x = centre_punt.x - (text_width / 2.0F),
|
||||||
<< '\n';
|
.y = centre_punt.y - (text_height / 2.0F)};
|
||||||
}
|
|
||||||
|
// Delegar al método render() existent
|
||||||
|
render(text, posicio_esquerra, scale, spacing, brightness, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar símbolo de copyright (©) - UTF-8 U+00A9
|
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
|
||||||
// Usem el segon byte (0xA9) com a key interna
|
if (text.empty()) {
|
||||||
{
|
return 0.0F;
|
||||||
char c = '\xA9'; // 169 decimal
|
|
||||||
std::string filename = "font/char_copyright.shp";
|
|
||||||
auto shape = ShapeLoader::load(filename);
|
|
||||||
|
|
||||||
if (shape && shape->es_valida()) {
|
|
||||||
chars_[c] = shape;
|
|
||||||
} else {
|
|
||||||
std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename
|
|
||||||
<< '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters"
|
|
||||||
<< '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string VectorText::get_shape_filename(char c) const {
|
|
||||||
// Mapeo carácter → nombre de archivo (amb prefix "font/")
|
|
||||||
switch (c) {
|
|
||||||
case '0':
|
|
||||||
case '1':
|
|
||||||
case '2':
|
|
||||||
case '3':
|
|
||||||
case '4':
|
|
||||||
case '5':
|
|
||||||
case '6':
|
|
||||||
case '7':
|
|
||||||
case '8':
|
|
||||||
case '9':
|
|
||||||
return std::string("font/char_") + c + ".shp";
|
|
||||||
|
|
||||||
// Lletres majúscules A-Z
|
|
||||||
case 'A':
|
|
||||||
case 'B':
|
|
||||||
case 'C':
|
|
||||||
case 'D':
|
|
||||||
case 'E':
|
|
||||||
case 'F':
|
|
||||||
case 'G':
|
|
||||||
case 'H':
|
|
||||||
case 'I':
|
|
||||||
case 'J':
|
|
||||||
case 'K':
|
|
||||||
case 'L':
|
|
||||||
case 'M':
|
|
||||||
case 'N':
|
|
||||||
case 'O':
|
|
||||||
case 'P':
|
|
||||||
case 'Q':
|
|
||||||
case 'R':
|
|
||||||
case 'S':
|
|
||||||
case 'T':
|
|
||||||
case 'U':
|
|
||||||
case 'V':
|
|
||||||
case 'W':
|
|
||||||
case 'X':
|
|
||||||
case 'Y':
|
|
||||||
case 'Z':
|
|
||||||
return std::string("font/char_") + c + ".shp";
|
|
||||||
|
|
||||||
// Lletres minúscules a-z (convertir a majúscules)
|
|
||||||
case 'a':
|
|
||||||
case 'b':
|
|
||||||
case 'c':
|
|
||||||
case 'd':
|
|
||||||
case 'e':
|
|
||||||
case 'f':
|
|
||||||
case 'g':
|
|
||||||
case 'h':
|
|
||||||
case 'i':
|
|
||||||
case 'j':
|
|
||||||
case 'k':
|
|
||||||
case 'l':
|
|
||||||
case 'm':
|
|
||||||
case 'n':
|
|
||||||
case 'o':
|
|
||||||
case 'p':
|
|
||||||
case 'q':
|
|
||||||
case 'r':
|
|
||||||
case 's':
|
|
||||||
case 't':
|
|
||||||
case 'u':
|
|
||||||
case 'v':
|
|
||||||
case 'w':
|
|
||||||
case 'x':
|
|
||||||
case 'y':
|
|
||||||
case 'z':
|
|
||||||
return std::string("font/char_") + char(c - 32) + ".shp";
|
|
||||||
|
|
||||||
// Símbols
|
|
||||||
case '.':
|
|
||||||
return "font/char_dot.shp";
|
|
||||||
case ',':
|
|
||||||
return "font/char_comma.shp";
|
|
||||||
case '-':
|
|
||||||
return "font/char_minus.shp";
|
|
||||||
case ':':
|
|
||||||
return "font/char_colon.shp";
|
|
||||||
case '!':
|
|
||||||
return "font/char_exclamation.shp";
|
|
||||||
case '?':
|
|
||||||
return "font/char_question.shp";
|
|
||||||
case ' ':
|
|
||||||
return ""; // Espai es maneja sense carregar shape
|
|
||||||
|
|
||||||
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
|
|
||||||
return "font/char_copyright.shp";
|
|
||||||
|
|
||||||
default:
|
|
||||||
return ""; // Caràcter no suportat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VectorText::is_supported(char c) const {
|
|
||||||
return chars_.contains(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VectorText::render(const std::string& text, const Punt& posicio, float escala, float spacing, float brightness) const {
|
|
||||||
if (renderer_ == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ancho de un carácter base (20 px a escala 1.0)
|
|
||||||
const float char_width_scaled = char_width * escala;
|
|
||||||
|
|
||||||
// Spacing escalado
|
|
||||||
const float spacing_scaled = spacing * escala;
|
|
||||||
|
|
||||||
// Altura de un carácter escalado (necesario para ajustar Y)
|
|
||||||
const float char_height_scaled = char_height * escala;
|
|
||||||
|
|
||||||
// Posición X del borde izquierdo del carácter actual
|
|
||||||
// (se ajustará +char_width/2 para obtener el centro al renderizar)
|
|
||||||
float current_x = posicio.x;
|
|
||||||
|
|
||||||
// Iterar sobre cada byte del string (con detecció UTF-8)
|
|
||||||
for (size_t i = 0; i < text.length(); i++) {
|
|
||||||
auto c = static_cast<unsigned char>(text[i]);
|
|
||||||
|
|
||||||
// Detectar copyright UTF-8 (0xC2 0xA9)
|
|
||||||
if (c == 0xC2 && i + 1 < text.length() &&
|
|
||||||
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
|
||||||
c = 0xA9; // Usar segon byte com a key
|
|
||||||
i++; // Saltar el següent byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manejar espacios (avanzar sin dibujar)
|
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
|
||||||
if (c == ' ') {
|
const float SPACING_SCALED = spacing * scale;
|
||||||
current_x += char_width_scaled + spacing_scaled;
|
|
||||||
continue;
|
// Contar caracteres visuals (no bytes) - manejar UTF-8
|
||||||
|
size_t visual_chars = 0;
|
||||||
|
for (size_t i = 0; i < text.length(); i++) {
|
||||||
|
auto c = static_cast<unsigned char>(text[i]);
|
||||||
|
|
||||||
|
// Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
|
||||||
|
if (c == 0xC2 && i + 1 < text.length() &&
|
||||||
|
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
||||||
|
visual_chars++; // Un caràcter visual (©)
|
||||||
|
i++; // Saltar el següent byte
|
||||||
|
} else {
|
||||||
|
visual_chars++; // Caràcter normal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar si el carácter está soportado
|
// Ancho total = todos los caracteres VISUALES + spacing entre ellos
|
||||||
auto it = chars_.find(c);
|
return (visual_chars * CHAR_WIDTH_SCALED) + ((visual_chars - 1) * SPACING_SCALED);
|
||||||
if (it != chars_.end()) {
|
|
||||||
// Renderizar carácter
|
|
||||||
// Ajustar X e Y para que posicio represente esquina superior izquierda
|
|
||||||
// (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
|
|
||||||
Punt char_pos = {.x = current_x + (char_width_scaled / 2.0F), .y = posicio.y + (char_height_scaled / 2.0F)};
|
|
||||||
Rendering::render_shape(renderer_, it->second, char_pos, 0.0F, escala, true, 1.0F, brightness);
|
|
||||||
|
|
||||||
// Avanzar posición
|
|
||||||
current_x += char_width_scaled + spacing_scaled;
|
|
||||||
} else {
|
|
||||||
// Carácter no soportado: saltar (o renderizar '?' en el futuro)
|
|
||||||
std::cerr << "[VectorText] Warning: caràcter no suportat '" << c << "'"
|
|
||||||
<< '\n';
|
|
||||||
current_x += char_width_scaled + spacing_scaled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VectorText::render_centered(const std::string& text, const Punt& centre_punt, float escala, float spacing, float brightness) const {
|
|
||||||
// Calcular dimensions del text
|
|
||||||
float text_width = get_text_width(text, escala, spacing);
|
|
||||||
float text_height = get_text_height(escala);
|
|
||||||
|
|
||||||
// Calcular posició de l'esquina superior esquerra
|
|
||||||
// restant la meitat de les dimensions del punt central
|
|
||||||
Punt posicio_esquerra = {
|
|
||||||
.x = centre_punt.x - (text_width / 2.0F),
|
|
||||||
.y = centre_punt.y - (text_height / 2.0F)};
|
|
||||||
|
|
||||||
// Delegar al mètode render() existent
|
|
||||||
render(text, posicio_esquerra, escala, spacing, brightness);
|
|
||||||
}
|
|
||||||
|
|
||||||
float VectorText::get_text_width(const std::string& text, float escala, float spacing) const {
|
|
||||||
if (text.empty()) {
|
|
||||||
return 0.0F;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const float char_width_scaled = char_width * escala;
|
auto VectorText::getTextHeight(float scale) -> float {
|
||||||
const float spacing_scaled = spacing * escala;
|
return BASE_CHAR_HEIGHT * scale;
|
||||||
|
|
||||||
// Contar caracteres visuals (no bytes) - manejar UTF-8
|
|
||||||
size_t visual_chars = 0;
|
|
||||||
for (size_t i = 0; i < text.length(); i++) {
|
|
||||||
auto c = static_cast<unsigned char>(text[i]);
|
|
||||||
|
|
||||||
// Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
|
|
||||||
if (c == 0xC2 && i + 1 < text.length() &&
|
|
||||||
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
|
||||||
visual_chars++; // Un caràcter visual (©)
|
|
||||||
i++; // Saltar el següent byte
|
|
||||||
} else {
|
|
||||||
visual_chars++; // Caràcter normal
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ancho total = todos los caracteres VISUALES + spacing entre ellos
|
|
||||||
return (visual_chars * char_width_scaled) + ((visual_chars - 1) * spacing_scaled);
|
|
||||||
}
|
|
||||||
|
|
||||||
float VectorText::get_text_height(float escala) const {
|
|
||||||
return char_height * escala;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// 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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -10,46 +10,51 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "core/graphics/shape.hpp"
|
#include "core/graphics/shape.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
|
||||||
namespace Graphics {
|
namespace Graphics {
|
||||||
|
|
||||||
class VectorText {
|
class VectorText {
|
||||||
public:
|
public:
|
||||||
VectorText(SDL_Renderer* renderer);
|
explicit 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;
|
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
||||||
|
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) 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;
|
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
||||||
|
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) 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;
|
// Es estático: no depende del estado del VectorText (el ancho viene de
|
||||||
|
// las constantes BASE_CHAR_WIDTH/BASE_CHAR_HEIGHT del archivo .cpp).
|
||||||
|
[[nodiscard]] static auto getTextWidth(const std::string& text, float scale = 1.0F, float spacing = 2.0F) -> float;
|
||||||
|
|
||||||
// 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]] static auto getTextHeight(float scale = 1.0F) -> float;
|
||||||
|
|
||||||
// Verificar si un carácter está soportado
|
// Verificar si un carácter está soportado
|
||||||
[[nodiscard]] bool is_supported(char c) const;
|
[[nodiscard]] auto isSupported(char c) const -> bool;
|
||||||
|
|
||||||
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 loadCharset();
|
||||||
[[nodiscard]] std::string get_shape_filename(char c) const;
|
[[nodiscard]] static auto getShapeFilename(char c) -> std::string;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
// wireframe3d.cpp - Implementació dels meshos 3D wireframe
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/graphics/wireframe3d.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "core/rendering/line_renderer.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
auto applyTransform(const Transform3D& transform, const Vec3& local) -> Vec3 {
|
||||||
|
// 1. Escala uniforme.
|
||||||
|
Vec3 v{
|
||||||
|
.x = local.x * transform.scale,
|
||||||
|
.y = local.y * transform.scale,
|
||||||
|
.z = local.z * transform.scale,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ordre X → Y → Z: amb aquest ordre, una rotació pitch+yaw pot dur el
|
||||||
|
// vector local (0,-1,0) a qualsevol direcció mundial — necessari perquè
|
||||||
|
// les naus calculen pitch+yaw look-at per alinear-se amb el seu path.
|
||||||
|
// L'ordre invers (Y→X) no permet X arbitrari en vectors sobre l'eix Y.
|
||||||
|
|
||||||
|
// 2. Rotació X (pitch): Y i Z.
|
||||||
|
const float CX = std::cos(transform.rotation_euler.x);
|
||||||
|
const float SX = std::sin(transform.rotation_euler.x);
|
||||||
|
{
|
||||||
|
const float NY = (v.y * CX) - (v.z * SX);
|
||||||
|
const float NZ = (v.y * SX) + (v.z * CX);
|
||||||
|
v.y = NY;
|
||||||
|
v.z = NZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Rotació Y (yaw): X i Z.
|
||||||
|
const float CY = std::cos(transform.rotation_euler.y);
|
||||||
|
const float SY = std::sin(transform.rotation_euler.y);
|
||||||
|
{
|
||||||
|
const float NX = (v.x * CY) + (v.z * SY);
|
||||||
|
const float NZ = (-v.x * SY) + (v.z * CY);
|
||||||
|
v.x = NX;
|
||||||
|
v.z = NZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Rotació Z (roll): X i Y.
|
||||||
|
const float CZ = std::cos(transform.rotation_euler.z);
|
||||||
|
const float SZ = std::sin(transform.rotation_euler.z);
|
||||||
|
{
|
||||||
|
const float NX = (v.x * CZ) - (v.y * SZ);
|
||||||
|
const float NY = (v.x * SZ) + (v.y * CZ);
|
||||||
|
v.x = NX;
|
||||||
|
v.y = NY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Translació final.
|
||||||
|
v.x += transform.position.x;
|
||||||
|
v.y += transform.position.y;
|
||||||
|
v.z += transform.position.z;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawWireframe(Rendering::Renderer* renderer, const Camera3D& camera, const Mesh3D& mesh, const Transform3D& transform, float brightness, SDL_Color color) {
|
||||||
|
if (renderer == nullptr || mesh.edges.empty() || mesh.vertices.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projecta tots els vèrtexs un cop; cau-en si queden darrere del near.
|
||||||
|
std::vector<std::optional<Camera3D::ProjectedPoint>> projected;
|
||||||
|
projected.reserve(mesh.vertices.size());
|
||||||
|
for (const auto& vertex : mesh.vertices) {
|
||||||
|
const Vec3 WORLD = applyTransform(transform, vertex);
|
||||||
|
projected.push_back(camera.project(WORLD));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& edge : mesh.edges) {
|
||||||
|
const auto& a_proj = projected[edge.first];
|
||||||
|
const auto& b_proj = projected[edge.second];
|
||||||
|
if (!a_proj.has_value() || !b_proj.has_value()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Rendering::linea(renderer,
|
||||||
|
static_cast<int>(a_proj->screen.x),
|
||||||
|
static_cast<int>(a_proj->screen.y),
|
||||||
|
static_cast<int>(b_proj->screen.x),
|
||||||
|
static_cast<int>(b_proj->screen.y),
|
||||||
|
brightness,
|
||||||
|
0.0F,
|
||||||
|
color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto makeOctahedron() -> Mesh3D {
|
||||||
|
// 6 vèrtexs als eixos: ±X, ±Y, ±Z.
|
||||||
|
Mesh3D mesh;
|
||||||
|
mesh.vertices = {
|
||||||
|
{.x = 1.0F, .y = 0.0F, .z = 0.0F}, // 0: +X
|
||||||
|
{.x = -1.0F, .y = 0.0F, .z = 0.0F}, // 1: -X
|
||||||
|
{.x = 0.0F, .y = 1.0F, .z = 0.0F}, // 2: +Y
|
||||||
|
{.x = 0.0F, .y = -1.0F, .z = 0.0F}, // 3: -Y
|
||||||
|
{.x = 0.0F, .y = 0.0F, .z = 1.0F}, // 4: +Z
|
||||||
|
{.x = 0.0F, .y = 0.0F, .z = -1.0F}, // 5: -Z
|
||||||
|
};
|
||||||
|
// 12 arestes: cada vèrtex axial connecta amb els 4 vèrtexs no oposats.
|
||||||
|
mesh.edges = {
|
||||||
|
// "Equador" XY al voltant de Z.
|
||||||
|
{2, 0},
|
||||||
|
{0, 3},
|
||||||
|
{3, 1},
|
||||||
|
{1, 2},
|
||||||
|
// Piràmide superior (cap a +Z).
|
||||||
|
{2, 4},
|
||||||
|
{0, 4},
|
||||||
|
{3, 4},
|
||||||
|
{1, 4},
|
||||||
|
// Piràmide inferior (cap a -Z).
|
||||||
|
{2, 5},
|
||||||
|
{0, 5},
|
||||||
|
{3, 5},
|
||||||
|
{1, 5},
|
||||||
|
};
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto extrudeShape2D(const Shape& shape, float depth) -> Mesh3D {
|
||||||
|
Mesh3D mesh;
|
||||||
|
if (!shape.isValid()) {
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float HALF = depth * 0.5F;
|
||||||
|
const Vec2 CENTRE = shape.getCenter();
|
||||||
|
// Si depth <= 0, emetem només un pla (sense vèrtexs back ni connexions)
|
||||||
|
// per evitar arestes degenerades i acumulació additiva de brightness.
|
||||||
|
const bool FLAT = (depth <= 0.0F);
|
||||||
|
|
||||||
|
for (const auto& primitive : shape.getPrimitives()) {
|
||||||
|
if (primitive.points.size() < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto BASE = static_cast<std::uint16_t>(mesh.vertices.size());
|
||||||
|
const auto N = static_cast<std::uint16_t>(primitive.points.size());
|
||||||
|
|
||||||
|
// Vèrtexs frontals (z = +HALF, o z = 0 si FLAT).
|
||||||
|
for (const auto& p : primitive.points) {
|
||||||
|
mesh.vertices.push_back(Vec3{
|
||||||
|
.x = p.x - CENTRE.x,
|
||||||
|
.y = p.y - CENTRE.y,
|
||||||
|
.z = HALF,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Arestes "frontals": connecten punts consecutius de la polyline.
|
||||||
|
for (std::uint16_t i = 0; i + 1 < N; ++i) {
|
||||||
|
mesh.edges.emplace_back(BASE + i, BASE + i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FLAT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vèrtexs posteriors (z = -HALF) i arestes corresponents.
|
||||||
|
for (const auto& p : primitive.points) {
|
||||||
|
mesh.vertices.push_back(Vec3{
|
||||||
|
.x = p.x - CENTRE.x,
|
||||||
|
.y = p.y - CENTRE.y,
|
||||||
|
.z = -HALF,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (std::uint16_t i = 0; i + 1 < N; ++i) {
|
||||||
|
mesh.edges.emplace_back(BASE + N + i, BASE + N + i + 1);
|
||||||
|
}
|
||||||
|
// Arestes de connexió front↔posterior per cada vèrtex.
|
||||||
|
// Per polylines tancades (primer == últim punt), el bucle igualment
|
||||||
|
// genera N connexions; el parell duplicat (primer i últim) cau en una
|
||||||
|
// línia idèntica sense efecte visible.
|
||||||
|
for (std::uint16_t i = 0; i < N; ++i) {
|
||||||
|
mesh.edges.emplace_back(BASE + i, BASE + N + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// wireframe3d.hpp - Meshos 3D wireframe i utilitats per dibuixar-los
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs).
|
||||||
|
// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i
|
||||||
|
// emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix
|
||||||
|
// pipeline que la resta del joc: glow verd via ColorOscillator si color.a==0).
|
||||||
|
//
|
||||||
|
// Sense depth buffer: el caller és responsable d'ordenar els meshos per
|
||||||
|
// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST
|
||||||
|
// amb alpha blend additiu).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/graphics/camera3d.hpp"
|
||||||
|
#include "core/graphics/shape.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
struct Mesh3D {
|
||||||
|
std::vector<Vec3> vertices;
|
||||||
|
std::vector<std::pair<std::uint16_t, std::uint16_t>> edges;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Transform3D {
|
||||||
|
Vec3 position{};
|
||||||
|
// Euler en radians, aplicat en ordre Y (yaw) → X (pitch) → Z (roll).
|
||||||
|
Vec3 rotation_euler{};
|
||||||
|
float scale{1.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aplica la Transform3D a un vèrtex local del mesh per obtenir-ne la posició
|
||||||
|
// mundial. Ordre: scale → rotate (Y,X,Z) → translate.
|
||||||
|
[[nodiscard]] auto applyTransform(const Transform3D& transform, const Vec3& local) -> Vec3;
|
||||||
|
|
||||||
|
// Dibuixa el mesh en wireframe a través de la càmera donada. Cada aresta es
|
||||||
|
// projecta en CPU i s'emet via `Rendering::linea`. Les arestes amb algun extrem
|
||||||
|
// darrere del near plane es descarten per complet (clipping primitiu).
|
||||||
|
// - brightness: multiplicador aplicat al color de línia.
|
||||||
|
// - color: si alpha == 0, usa el color global del oscil·lador (glow verd).
|
||||||
|
void drawWireframe(Rendering::Renderer* renderer, const Camera3D& camera, const Mesh3D& mesh, const Transform3D& transform, float brightness = 1.0F, SDL_Color color = {.r = 0, .g = 0, .b = 0, .a = 0});
|
||||||
|
|
||||||
|
// Factory: octaedre regular amb 6 vèrtexs als eixos a distància 1 i 12 arestes.
|
||||||
|
// Pensat com a estrella 3D al starfield (escalable amb Transform3D::scale).
|
||||||
|
[[nodiscard]] auto makeOctahedron() -> Mesh3D;
|
||||||
|
|
||||||
|
// Factory: extrusió en Z d'un shape 2D. Cada polyline genera dues còpies
|
||||||
|
// (z = +depth/2 i z = -depth/2) més arestes de connexió frontal↔posterior
|
||||||
|
// per cada vèrtex de la polyline.
|
||||||
|
[[nodiscard]] auto extrudeShape2D(const Shape& shape, float depth) -> Mesh3D;
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
+43
-71
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
|
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
|
||||||
|
|
||||||
|
#include <algorithm> // Para std::ranges::any_of
|
||||||
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
|
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
|
||||||
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
|
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
|
||||||
#include <ranges> // Para __find_if_fn, find_if
|
|
||||||
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||||
#include <utility> // Para pair, move
|
#include <utility> // Para move
|
||||||
|
|
||||||
#include "game/options.hpp" // Para Options::controls
|
|
||||||
|
|
||||||
// Singleton
|
// Singleton
|
||||||
Input* Input::instance = nullptr;
|
Input* Input::instance = nullptr;
|
||||||
@@ -30,7 +28,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}},
|
||||||
@@ -41,6 +39,8 @@ Input::Input(std::string game_controller_db_path)
|
|||||||
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
|
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
|
||||||
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
|
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
|
||||||
{Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F4}},
|
{Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F4}},
|
||||||
|
{Action::TOGGLE_ANTIALIAS, KeyState{.scancode = SDL_SCANCODE_F5}},
|
||||||
|
{Action::TOGGLE_POSTFX, KeyState{.scancode = SDL_SCANCODE_F6}},
|
||||||
{Action::EXIT, KeyState{.scancode = SDL_SCANCODE_ESCAPE}}};
|
{Action::EXIT, KeyState{.scancode = SDL_SCANCODE_ESCAPE}}};
|
||||||
|
|
||||||
initSDLGamePad(); // Inicializa el subsistema SDL_INIT_GAMEPAD
|
initSDLGamePad(); // Inicializa el subsistema SDL_INIT_GAMEPAD
|
||||||
@@ -51,34 +51,6 @@ void Input::bindKey(Action action, SDL_Scancode code) {
|
|||||||
keyboard_.bindings[action].scancode = code;
|
keyboard_.bindings[action].scancode = code;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica las teclas configuradas desde Options
|
|
||||||
void Input::applyKeyboardBindingsFromOptions() {
|
|
||||||
bindKey(Action::LEFT, Options::keyboard_controls.key_left);
|
|
||||||
bindKey(Action::RIGHT, Options::keyboard_controls.key_right);
|
|
||||||
bindKey(Action::THRUST, Options::keyboard_controls.key_thrust);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado
|
|
||||||
void Input::applyGamepadBindingsFromOptions() {
|
|
||||||
// Si no hay gamepads conectados, no hay nada que hacer
|
|
||||||
if (gamepads_.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtener el primer gamepad conectado
|
|
||||||
const auto& gamepad = gamepads_[0];
|
|
||||||
|
|
||||||
// Aplicar bindings desde Options
|
|
||||||
// Los valores pueden ser:
|
|
||||||
// - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders)
|
|
||||||
// - 100: L2 trigger
|
|
||||||
// - 101: R2 trigger
|
|
||||||
// - 200+: Ejes del stick analógico
|
|
||||||
gamepad->bindings[Action::LEFT].button = Options::gamepad_controls.button_left;
|
|
||||||
gamepad->bindings[Action::RIGHT].button = Options::gamepad_controls.button_right;
|
|
||||||
gamepad->bindings[Action::THRUST].button = Options::gamepad_controls.button_thrust;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asigna inputs a botones del mando
|
// Asigna inputs a botones del mando
|
||||||
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) {
|
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) {
|
||||||
if (gamepad != nullptr) {
|
if (gamepad != nullptr) {
|
||||||
@@ -188,14 +160,11 @@ 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) {
|
return std::ranges::any_of(actions, [this, repeat](const InputAction& action) {
|
||||||
if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
|
return checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat);
|
||||||
return true;
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprueba si hay algun mando conectado
|
// Comprueba si hay algun mando conectado
|
||||||
@@ -388,14 +357,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;
|
||||||
@@ -420,8 +389,11 @@ auto Input::handleEvent(const SDL_Event& event) -> std::string {
|
|||||||
return addGamepad(event.gdevice.which);
|
return addGamepad(event.gdevice.which);
|
||||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||||
return removeGamepad(event.gdevice.which);
|
return removeGamepad(event.gdevice.which);
|
||||||
|
default:
|
||||||
|
// El resto de eventos SDL no interesan a Input (los maneja el resto
|
||||||
|
// del sistema: ventana, teclado, mouse).
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Input::addGamepad(int device_index) -> std::string {
|
auto Input::addGamepad(int device_index) -> std::string {
|
||||||
@@ -493,23 +465,23 @@ 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::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||||
// 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 = bindings.keyboard.key_left;
|
||||||
player1_keyboard_bindings_[Action::RIGHT].scancode = Options::player1.keyboard.key_right;
|
player1_keyboard_bindings_[Action::RIGHT].scancode = bindings.keyboard.key_right;
|
||||||
player1_keyboard_bindings_[Action::THRUST].scancode = Options::player1.keyboard.key_thrust;
|
player1_keyboard_bindings_[Action::THRUST].scancode = bindings.keyboard.key_thrust;
|
||||||
player1_keyboard_bindings_[Action::SHOOT].scancode = Options::player1.keyboard.key_shoot;
|
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||||
player1_keyboard_bindings_[Action::START].scancode = Options::player1.keyboard.key_start;
|
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||||
|
|
||||||
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
|
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
|
||||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||||
if (Options::player1.gamepad_name.empty()) {
|
if (bindings.gamepad_name.empty()) {
|
||||||
// Fallback: usar primer gamepad disponible
|
// Fallback: usar primer gamepad disponible
|
||||||
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
|
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
|
||||||
} else {
|
} else {
|
||||||
// Buscar por nombre
|
// Buscar por nombre
|
||||||
gamepad = findAvailableGamepadByName(Options::player1.gamepad_name);
|
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gamepad) {
|
if (!gamepad) {
|
||||||
@@ -518,32 +490,32 @@ void Input::applyPlayer1BindingsFromOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Aplicar bindings de gamepad
|
// 3. Aplicar bindings de gamepad
|
||||||
gamepad->bindings[Action::LEFT].button = Options::player1.gamepad.button_left;
|
gamepad->bindings[Action::LEFT].button = bindings.gamepad.button_left;
|
||||||
gamepad->bindings[Action::RIGHT].button = Options::player1.gamepad.button_right;
|
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||||
gamepad->bindings[Action::THRUST].button = Options::player1.gamepad.button_thrust;
|
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||||
gamepad->bindings[Action::SHOOT].button = Options::player1.gamepad.button_shoot;
|
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||||
|
|
||||||
// 4. Cachear referencia
|
// 4. Cachear referencia
|
||||||
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::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
||||||
// 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 = bindings.keyboard.key_left;
|
||||||
player2_keyboard_bindings_[Action::RIGHT].scancode = Options::player2.keyboard.key_right;
|
player2_keyboard_bindings_[Action::RIGHT].scancode = bindings.keyboard.key_right;
|
||||||
player2_keyboard_bindings_[Action::THRUST].scancode = Options::player2.keyboard.key_thrust;
|
player2_keyboard_bindings_[Action::THRUST].scancode = bindings.keyboard.key_thrust;
|
||||||
player2_keyboard_bindings_[Action::SHOOT].scancode = Options::player2.keyboard.key_shoot;
|
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||||
player2_keyboard_bindings_[Action::START].scancode = Options::player2.keyboard.key_start;
|
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||||
|
|
||||||
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
|
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
|
||||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||||
if (Options::player2.gamepad_name.empty()) {
|
if (bindings.gamepad_name.empty()) {
|
||||||
// Fallback: usar segundo gamepad disponible
|
// Fallback: usar segundo gamepad disponible
|
||||||
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
|
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
|
||||||
} else {
|
} else {
|
||||||
// Buscar por nombre
|
// Buscar por nombre
|
||||||
gamepad = findAvailableGamepadByName(Options::player2.gamepad_name);
|
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gamepad) {
|
if (!gamepad) {
|
||||||
@@ -552,16 +524,16 @@ void Input::applyPlayer2BindingsFromOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Aplicar bindings de gamepad
|
// 3. Aplicar bindings de gamepad
|
||||||
gamepad->bindings[Action::LEFT].button = Options::player2.gamepad.button_left;
|
gamepad->bindings[Action::LEFT].button = bindings.gamepad.button_left;
|
||||||
gamepad->bindings[Action::RIGHT].button = Options::player2.gamepad.button_right;
|
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||||
gamepad->bindings[Action::THRUST].button = Options::player2.gamepad.button_thrust;
|
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||||
gamepad->bindings[Action::SHOOT].button = Options::player2.gamepad.button_shoot;
|
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||||
|
|
||||||
// 4. Cachear referencia
|
// 4. Cachear referencia
|
||||||
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 +555,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;
|
||||||
|
|||||||
+118
-120
@@ -7,156 +7,154 @@
|
|||||||
#include <span> // Para span
|
#include <span> // Para span
|
||||||
#include <string> // Para string, basic_string
|
#include <string> // Para string, basic_string
|
||||||
#include <unordered_map> // Para unordered_map
|
#include <unordered_map> // Para unordered_map
|
||||||
#include <utility> // Para pair
|
|
||||||
#include <vector> // Para vector
|
#include <vector> // Para vector
|
||||||
|
|
||||||
|
#include "core/config/engine_config.hpp"
|
||||||
#include "core/input/input_types.hpp" // for InputAction
|
#include "core/input/input_types.hpp" // for InputAction
|
||||||
|
|
||||||
// --- Clase Input: gestiona la entrada de teclado y mandos (singleton) ---
|
// --- Clase Input: gestiona la entrada de teclado y mandos (singleton) ---
|
||||||
class Input {
|
class Input {
|
||||||
public:
|
public:
|
||||||
// --- Constantes ---
|
// --- Constantes ---
|
||||||
static constexpr bool ALLOW_REPEAT = true; // Permite repetición
|
static constexpr bool ALLOW_REPEAT = true; // Permite repetición
|
||||||
static constexpr bool DO_NOT_ALLOW_REPEAT = false; // No permite repetición
|
static constexpr bool DO_NOT_ALLOW_REPEAT = false; // No permite repetición
|
||||||
static constexpr bool CHECK_KEYBOARD = true; // Comprueba teclado
|
static constexpr bool CHECK_KEYBOARD = true; // Comprueba teclado
|
||||||
static constexpr bool DO_NOT_CHECK_KEYBOARD = false; // No comprueba teclado
|
static constexpr bool DO_NOT_CHECK_KEYBOARD = false; // No comprueba teclado
|
||||||
static constexpr int TRIGGER_L2_AS_BUTTON = 100; // L2 como botón
|
static constexpr int TRIGGER_L2_AS_BUTTON = 100; // L2 como botón
|
||||||
static constexpr int TRIGGER_R2_AS_BUTTON = 101; // R2 como botón
|
static constexpr int TRIGGER_R2_AS_BUTTON = 101; // R2 como botón
|
||||||
|
|
||||||
// --- Tipos ---
|
// --- Tipos ---
|
||||||
using Action = InputAction; // Alias para mantener compatibilidad
|
using Action = InputAction; // Alias para mantener compatibilidad
|
||||||
|
|
||||||
// --- Estructuras ---
|
// --- Estructuras ---
|
||||||
struct KeyState {
|
struct KeyState {
|
||||||
Uint8 scancode{0}; // Scancode asociado
|
Uint8 scancode{0}; // Scancode asociado
|
||||||
bool is_held{false}; // Está pulsada ahora mismo
|
bool is_held{false}; // Está pulsada ahora mismo
|
||||||
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ButtonState {
|
struct ButtonState {
|
||||||
int button{static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)}; // GameControllerButton asociado
|
int button{static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)}; // GameControllerButton asociado
|
||||||
bool is_held{false}; // Está pulsada ahora mismo
|
bool is_held{false}; // Está pulsada ahora mismo
|
||||||
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
||||||
bool axis_active{false}; // Estado del eje
|
bool axis_active{false}; // Estado del eje
|
||||||
bool trigger_active{false}; // Estado del trigger como botón digital
|
bool trigger_active{false}; // Estado del trigger como botón digital
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Keyboard {
|
struct Keyboard {
|
||||||
std::unordered_map<Action, KeyState> bindings; // Mapa de acciones a estados de tecla
|
std::unordered_map<Action, KeyState> bindings; // Mapa de acciones a estados de tecla
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Gamepad {
|
struct Gamepad {
|
||||||
SDL_Gamepad* pad{nullptr}; // Puntero al gamepad SDL
|
SDL_Gamepad* pad{nullptr}; // Puntero al gamepad SDL
|
||||||
SDL_JoystickID instance_id{0}; // ID de instancia del joystick
|
SDL_JoystickID instance_id{0}; // ID de instancia del joystick
|
||||||
std::string name; // Nombre del gamepad
|
std::string name; // Nombre del gamepad
|
||||||
std::string path; // Ruta del dispositivo
|
std::string path; // Ruta del dispositivo
|
||||||
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
|
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
|
||||||
|
|
||||||
explicit Gamepad(SDL_Gamepad* gamepad)
|
explicit Gamepad(SDL_Gamepad* gamepad)
|
||||||
: pad(gamepad),
|
: pad(gamepad),
|
||||||
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
|
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
|
||||||
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)}},
|
||||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {}
|
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {}
|
||||||
|
|
||||||
~Gamepad() {
|
~Gamepad() {
|
||||||
if (pad != nullptr) {
|
if (pad != nullptr) {
|
||||||
SDL_CloseGamepad(pad);
|
SDL_CloseGamepad(pad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reasigna un botón a una acción
|
// Reasigna un botón a una acción
|
||||||
void rebindAction(Action action, SDL_GamepadButton new_button) {
|
void rebindAction(Action action, SDL_GamepadButton new_button) {
|
||||||
bindings[action].button = static_cast<int>(new_button);
|
bindings[action].button = static_cast<int>(new_button);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Tipos ---
|
// --- Tipos ---
|
||||||
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // Vector de gamepads
|
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // Vector de gamepads
|
||||||
|
|
||||||
// --- Singleton ---
|
// --- Singleton ---
|
||||||
static void init(const std::string& game_controller_db_path);
|
static void init(const std::string& game_controller_db_path);
|
||||||
static void destroy();
|
static void destroy();
|
||||||
static auto get() -> Input*;
|
static auto get() -> Input*;
|
||||||
|
|
||||||
// --- Actualización del sistema ---
|
// --- Actualización del sistema ---
|
||||||
void update(); // Actualiza estados de entrada
|
void update(); // Actualiza estados de entrada
|
||||||
|
|
||||||
// --- Configuración de controles ---
|
// --- Configuración de controles ---
|
||||||
void bindKey(Action action, SDL_Scancode code);
|
void bindKey(Action action, SDL_Scancode code);
|
||||||
void applyKeyboardBindingsFromOptions();
|
|
||||||
void applyGamepadBindingsFromOptions();
|
|
||||||
|
|
||||||
// Configuración por jugador (Orni - dos jugadores)
|
// Configuración por player (Orni - dos jugadores)
|
||||||
void applyPlayer1BindingsFromOptions();
|
void applyPlayer1Bindings(const Config::PlayerBindings& bindings);
|
||||||
void applyPlayer2BindingsFromOptions();
|
void applyPlayer2Bindings(const Config::PlayerBindings& bindings);
|
||||||
|
|
||||||
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button);
|
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button);
|
||||||
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source);
|
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source);
|
||||||
|
|
||||||
// --- Consulta de entrada ---
|
// --- Consulta de entrada ---
|
||||||
auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
||||||
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
|
||||||
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;
|
||||||
|
|
||||||
// Check if any player pressed any action from a list
|
// Check if any player pressed any action from a list
|
||||||
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||||
|
|
||||||
// --- Gestión de gamepads ---
|
// --- Gestión de gamepads ---
|
||||||
[[nodiscard]] auto gameControllerFound() const -> bool;
|
[[nodiscard]] auto gameControllerFound() const -> bool;
|
||||||
[[nodiscard]] auto getNumGamepads() const -> int;
|
[[nodiscard]] auto getNumGamepads() const -> int;
|
||||||
[[nodiscard]] auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Gamepad>;
|
[[nodiscard]] auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Gamepad>;
|
||||||
[[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
|
[[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
|
||||||
[[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; }
|
[[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; }
|
||||||
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
|
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
|
||||||
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
|
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
|
||||||
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
|
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
|
||||||
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton;
|
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton;
|
||||||
void printConnectedGamepads() const;
|
void printConnectedGamepads() const;
|
||||||
|
|
||||||
// --- Eventos ---
|
// --- Eventos ---
|
||||||
auto handleEvent(const SDL_Event& event) -> std::string;
|
auto handleEvent(const SDL_Event& event) -> std::string;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// --- Constantes ---
|
// --- Constantes ---
|
||||||
static constexpr Sint16 AXIS_THRESHOLD = 30000; // Umbral para ejes analógicos
|
static constexpr Sint16 AXIS_THRESHOLD = 30000; // Umbral para ejes analógicos
|
||||||
static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (50% del rango)
|
static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (50% del rango)
|
||||||
static constexpr std::array<Action, 1> BUTTON_INPUTS = {Action::SHOOT}; // Inputs que usan botones
|
static constexpr std::array<Action, 1> BUTTON_INPUTS = {Action::SHOOT}; // Inputs que usan botones
|
||||||
|
|
||||||
// --- Métodos ---
|
// --- Métodos ---
|
||||||
explicit Input(std::string game_controller_db_path);
|
explicit Input(std::string game_controller_db_path);
|
||||||
~Input() = default;
|
~Input() = default;
|
||||||
|
|
||||||
void initSDLGamePad();
|
void initSDLGamePad();
|
||||||
static auto checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
static auto checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
||||||
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
|
||||||
auto addGamepad(int device_index) -> std::string;
|
auto addGamepad(int device_index) -> std::string;
|
||||||
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||||
void addGamepadMappingsFromFile();
|
void addGamepadMappingsFromFile();
|
||||||
void discoverGamepads();
|
void discoverGamepads();
|
||||||
|
|
||||||
// --- Variables miembro ---
|
// --- Variables miembro ---
|
||||||
static Input* instance; // Instancia única del singleton
|
static Input* instance; // Instancia única del singleton
|
||||||
|
|
||||||
Gamepads gamepads_; // Lista de gamepads conectados
|
Gamepads gamepads_; // Lista de gamepads conectados
|
||||||
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,7 +1,5 @@
|
|||||||
#include "input_types.hpp"
|
#include "input_types.hpp"
|
||||||
|
|
||||||
#include <utility> // Para pair
|
|
||||||
|
|
||||||
// Definición de los mapas
|
// Definición de los mapas
|
||||||
const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||||
{InputAction::LEFT, "LEFT"},
|
{InputAction::LEFT, "LEFT"},
|
||||||
|
|||||||
@@ -3,23 +3,26 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
// --- Enums ---
|
// --- Enums ---
|
||||||
enum class InputAction : int { // Acciones de entrada posibles en el juego
|
enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el juego
|
||||||
// Inputs de juego (movimiento y acción)
|
// Inputs de juego (movimiento y acción)
|
||||||
LEFT, // Rotar izquierda
|
LEFT, // Rotar izquierda
|
||||||
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
|
||||||
WINDOW_DEC_ZOOM, // F1
|
WINDOW_DEC_ZOOM, // F1
|
||||||
TOGGLE_FULLSCREEN, // F3
|
TOGGLE_FULLSCREEN, // F3
|
||||||
TOGGLE_VSYNC, // F4
|
TOGGLE_VSYNC, // F4
|
||||||
|
TOGGLE_ANTIALIAS, // F5
|
||||||
|
TOGGLE_POSTFX, // F6
|
||||||
EXIT, // ESC
|
EXIT, // ESC
|
||||||
|
|
||||||
// Input obligatorio
|
// Input obligatorio
|
||||||
|
|||||||
+10
-10
@@ -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) {
|
||||||
@@ -42,7 +42,7 @@ void setForceHidden(bool force) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isForceHidden() {
|
auto isForceHidden() -> bool {
|
||||||
return force_hidden;
|
return force_hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ 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();
|
||||||
|
|
||||||
// Control de visibilidad forzada (para modo pantalla completa)
|
// Control de visibilidad forzada (para modo pantalla completa)
|
||||||
void setForceHidden(bool force); // Activar/desactivar ocultación forzada
|
void setForceHidden(bool force); // Activar/desactivar ocultación forzada
|
||||||
bool isForceHidden(); // Consultar estado actual
|
auto isForceHidden() -> bool; // Consultar estado actual
|
||||||
} // namespace Mouse
|
} // namespace Mouse
|
||||||
|
|||||||
+11
-11
@@ -1,5 +1,5 @@
|
|||||||
// easing.hpp - Funcions d'interpolació i easing
|
// easing.hpp - Funciones de interpolació i easing
|
||||||
// © 2025 Orni Attack
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -7,22 +7,22 @@ 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 auto easeOutQuad(float t) -> float {
|
||||||
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 auto easeInQuad(float t) -> float {
|
||||||
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 auto easeInOutQuad(float t) -> float {
|
||||||
return (t < 0.5F)
|
return (t < 0.5F)
|
||||||
? 2.0F * t * t
|
? 2.0F * t * t
|
||||||
: 1.0F - ((-2.0F * t + 2.0F) * (-2.0F * t + 2.0F) / 2.0F);
|
: 1.0F - ((-2.0F * t + 2.0F) * (-2.0F * t + 2.0F) / 2.0F);
|
||||||
@@ -30,14 +30,14 @@ 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 auto easeOutCubic(float t) -> float {
|
||||||
float t1 = 1.0F - t;
|
float t1 = 1.0F - t;
|
||||||
return 1.0F - (t1 * t1 * t1);
|
return 1.0F - (t1 * t1 * t1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interpolación lineal básica (para referencia)
|
// Interpolación lineal básica (para referencia)
|
||||||
inline float lerp(float start, float end, float t) {
|
inline auto lerp(float start, float end, float t) -> float {
|
||||||
return start + ((end - start) * t);
|
return start + ((end - start) * t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#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 auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
|
||||||
// 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);
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
// physics_world.cpp - Implementación del mundo físico
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#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.
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Resol col·lisió contra un parell paret-axis (mín i màx).
|
||||||
|
// pos/vel són les referències al component de l'axis actiu (x o y);
|
||||||
|
// contact_perp és la coordenada del component perpendicular (la fixa de la
|
||||||
|
// paret a l'eix actiu — usada per al contact_point).
|
||||||
|
void resolveAxis(float& pos,
|
||||||
|
float& vel,
|
||||||
|
float radius,
|
||||||
|
float min_val,
|
||||||
|
float max_val,
|
||||||
|
float restitution,
|
||||||
|
bool axis_is_x,
|
||||||
|
float contact_perp,
|
||||||
|
const PhysicsWorld::BoundsHitCallback& callback) {
|
||||||
|
// Cara mínima (esquerra o superior)
|
||||||
|
if (pos - radius < min_val) {
|
||||||
|
pos = min_val + radius;
|
||||||
|
if (vel < 0.0F) {
|
||||||
|
if (callback) {
|
||||||
|
const Vec2 CONTACT = axis_is_x
|
||||||
|
? Vec2{.x = min_val, .y = contact_perp}
|
||||||
|
: Vec2{.x = contact_perp, .y = min_val};
|
||||||
|
callback(BoundsHit{.contact_point = CONTACT, .impact_speed = -vel});
|
||||||
|
}
|
||||||
|
vel = -vel * restitution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cara màxima (dreta o inferior)
|
||||||
|
if (pos + radius > max_val) {
|
||||||
|
pos = max_val - radius;
|
||||||
|
if (vel > 0.0F) {
|
||||||
|
if (callback) {
|
||||||
|
const Vec2 CONTACT = axis_is_x
|
||||||
|
? Vec2{.x = max_val, .y = contact_perp}
|
||||||
|
: Vec2{.x = contact_perp, .y = max_val};
|
||||||
|
callback(BoundsHit{.contact_point = CONTACT, .impact_speed = vel});
|
||||||
|
}
|
||||||
|
vel = -vel * restitution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// Eix X (esquerra/dreta): contact_perp = y actual del cos.
|
||||||
|
resolveAxis(body->position.x, body->velocity.x, body->radius, MIN_X, MAX_X, body->restitution, /*axis_is_x=*/true, body->position.y, bounds_hit_callback_);
|
||||||
|
// Eix Y (superior/inferior): contact_perp = x actual (ja clampejada en l'eix X).
|
||||||
|
resolveAxis(body->position.y, body->velocity.y, body->radius, MIN_Y, MAX_Y, body->restitution, /*axis_is_x=*/false, body->position.x, bounds_hit_callback_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
resolveBodyPair(*a, *b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) {
|
||||||
|
// Dos cuerpos estáticos no necesitan resolución
|
||||||
|
if (a.isStatic() && b.isStatic()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Un cuerpo con radius=0 es cinemático puro (ej. la bala) y no participa
|
||||||
|
// en body-body. La detecció de gameplay (Physics::checkCollision) usa
|
||||||
|
// el radius de l'entity (no el del body) i s'encarrega d'aquesta parella.
|
||||||
|
if (a.radius <= 0.0F || b.radius <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restitución promedio (Box2D usa max; promedio es más permisivo)
|
||||||
|
const float E = (a.restitution + b.restitution) * 0.5F;
|
||||||
|
const float J = -(1.0F + E) * VEL_ALONG_NORMAL / TOTAL_INV_MASS;
|
||||||
|
const Vec2 IMPULSE = NORMAL * J;
|
||||||
|
|
||||||
|
if (!a.isStatic()) {
|
||||||
|
a.velocity -= IMPULSE * a.inverse_mass;
|
||||||
|
}
|
||||||
|
if (!b.isStatic()) {
|
||||||
|
b.velocity += IMPULSE * b.inverse_mass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Physics
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// physics_world.hpp - Mundo físico 2D
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// 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 <functional>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
namespace Physics {
|
||||||
|
|
||||||
|
struct RigidBody;
|
||||||
|
|
||||||
|
// Notificació d'impacte contra un dels bounds del PLAYAREA. impact_speed és
|
||||||
|
// la magnitud de la component de velocity perpendicular a la paret (≥ 0).
|
||||||
|
struct BoundsHit {
|
||||||
|
Vec2 contact_point;
|
||||||
|
float impact_speed;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PhysicsWorld {
|
||||||
|
public:
|
||||||
|
using BoundsHitCallback = std::function<void(const BoundsHit&)>;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// Callback opcional invocat cada vegada que un cos impacta contra
|
||||||
|
// un dels bounds del PLAYAREA. S'invoca abans de la reflexió de
|
||||||
|
// velocity perquè impact_speed sigui la magnitud entrant.
|
||||||
|
void setBoundsHitCallback(BoundsHitCallback callback) {
|
||||||
|
bounds_hit_callback_ = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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};
|
||||||
|
BoundsHitCallback bounds_hit_callback_;
|
||||||
|
|
||||||
|
void integrate(float dt);
|
||||||
|
void resolveBoundsCollisions();
|
||||||
|
void resolveBodyCollisions();
|
||||||
|
// Resol un únic parell (a, b): correcció posicional + impulso elàstic.
|
||||||
|
// Estàtic: només toca els dos cossos rebuts, no consulta el world.
|
||||||
|
static void resolveBodyPair(RigidBody& a, RigidBody& b);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Physics
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// rigid_body.hpp - Cuerpo rígido 2D para el sistema de física
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// 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
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user