Compare commits
262 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c0502eefb | |||
| 9b3da3a6e7 | |||
| bc41169176 | |||
| b3a1afce06 | |||
| 4b6dc8a47a | |||
| 3dadd5fc1a | |||
| bea844d51e | |||
| 5fb6c68df4 | |||
| 866a057704 | |||
| da8eab330d | |||
| 39bda0775e | |||
| ed4d3a3915 | |||
| 6447932212 | |||
| 9f278772bb | |||
| 2d073b6055 | |||
| 99b18d208d | |||
| 1321566910 | |||
| cefafe99e4 | |||
| daa7eaf811 | |||
| 3dcf5c3a99 | |||
| 99d0f62ab5 | |||
| 85050c8da4 | |||
| 120c5502fd | |||
| 64a6599e81 | |||
| a4b567588f | |||
| 2e74fea2d5 | |||
| c4933875dd | |||
| 10a54aef91 | |||
| 34be79192c | |||
| fcf13591be | |||
| 3e8f2f35bf | |||
| e5a91825b1 | |||
| b3271b17a2 | |||
| d4117e3505 | |||
| 73c7e4ea76 | |||
| 23cc5ce68d | |||
| e42059e486 | |||
| 00f40d194b | |||
| 31f348328e | |||
| 8c48a9a772 | |||
| bacfbe6eac | |||
| 63d08aef46 | |||
| 87f818ef96 | |||
| 7eafe21623 | |||
| 22827c28fa | |||
| 8c21345f14 | |||
| 56d7d4af52 | |||
| 71c43ec6fe | |||
| 443b461974 | |||
| cc16908b86 | |||
| c4c6881bd6 | |||
| 35d720bb77 | |||
| 274ce1ca63 | |||
| 252e881e93 | |||
| d36ad7d1c5 | |||
| 7305d2f5dc | |||
| 4cfad053f0 | |||
| 807f71ffa7 | |||
| d12f24d798 | |||
| f9d2539a45 | |||
| 87bfccd14f | |||
| e5e3729215 | |||
| 6210985548 | |||
| 20250a0d6d | |||
| e5616f7c3a | |||
| 3b1e469a4f | |||
| 70ca19eb87 | |||
| 7e52eaeddb | |||
| d618b6d561 | |||
| e954d4ea59 | |||
| b1ee23cd20 | |||
| d86b10c14e | |||
| 1ea38d4f6a | |||
| 26bd5a9efa | |||
| 4b0d85c010 | |||
| 149b485a9b | |||
| 6b1f064cda | |||
| 1cef6a2c23 | |||
| 007460dc51 | |||
| 10057a82de | |||
| 73fa5bf1d1 | |||
| c32b564da1 | |||
| 7b9b5ce569 | |||
| f0b3a1fbc4 | |||
| 869b4374ba | |||
| ea192cd9de | |||
| 5d30f6be68 | |||
| a342d79b86 | |||
| 1db7368c9f | |||
| 88b002b277 | |||
| 044a3a3bbf | |||
| 49070aa843 | |||
| 18e05e36e6 | |||
| bf79eecca0 | |||
| b80216dce1 | |||
| 87138f9a1f | |||
| c6560514d8 | |||
| 839f73e1ef | |||
| 2ca2062011 | |||
| 03209ee23b | |||
| c61299f17f | |||
| 880af293ef | |||
| 67c59992c9 | |||
| be3d696f60 | |||
| 6b8f6a267d | |||
| 120b8ada38 | |||
| 8bb052981d | |||
| 7fc8e48596 | |||
| ff518195f8 | |||
| 54d3e683a1 | |||
| a29c2b9cc2 | |||
| 85e7e70767 | |||
| 3f10c61e22 | |||
| 5de9a5003b | |||
| 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/
|
||||||
|
|||||||
+58
-5
@@ -1,11 +1,9 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(orni VERSION 0.7.2 LANGUAGES CXX)
|
project(orni VERSION 0.8.0 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)
|
||||||
@@ -112,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
|
|||||||
tools/pack_resources/pack_resources.cpp
|
tools/pack_resources/pack_resources.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
target_include_directories(pack_resources PRIVATE
|
||||||
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
|
)
|
||||||
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
|
||||||
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
||||||
@@ -135,6 +136,57 @@ 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,
|
||||||
|
# així que glslc només és necessari quan canvien els .glsl o falten headers.
|
||||||
|
#
|
||||||
|
# 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"
|
||||||
|
)
|
||||||
|
set(ALL_SHADER_HEADERS_PRESENT TRUE)
|
||||||
|
foreach(_spv_header IN LISTS ALL_SHADER_HEADERS)
|
||||||
|
if(NOT EXISTS "${_spv_header}")
|
||||||
|
set(ALL_SHADER_HEADERS_PRESENT FALSE)
|
||||||
|
break()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
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(ALL_SHADER_HEADERS_PRESENT)
|
||||||
|
message(STATUS "Shaders: glslc no trobat — s'usaran els headers SPV ja commiteats al repo")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "glslc no trobat i falten headers SPV: instal·la 'shaderc' o 'vulkan-sdk' per generar-los")
|
||||||
|
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 +273,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
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: bullet
|
||||||
|
|
||||||
|
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
|
||||||
|
# scale el modula visualment i pel hitbox.
|
||||||
|
shape:
|
||||||
|
path: bullet.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
|
||||||
|
# (body_.radius = 0 al spawn), però sí participa al gameplay via
|
||||||
|
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
|
||||||
|
# només per calcular l'impuls que rep l'enemic en impactar.
|
||||||
|
physics:
|
||||||
|
mass: 0.5
|
||||||
|
restitution: 0.0 # irrelevant (no rebota)
|
||||||
|
linear_damping: 0.0 # movement rectilini uniforme
|
||||||
|
angular_damping: 0.0
|
||||||
|
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [155, 255, 175] # verd laser
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: pentagon
|
||||||
|
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: enemy_pentagon.shp
|
||||||
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 5.0
|
||||||
|
speed: 35.0 # px/s (esquivador lent)
|
||||||
|
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
|
||||||
|
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
|
||||||
|
restitution: 1.0 # rebot elàstic perfecte contra parets
|
||||||
|
linear_damping: 0.0 # manté velocitat (sense fricció)
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
behavior:
|
||||||
|
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
|
||||||
|
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
|
||||||
|
zigzag_prob_per_second: 0.8
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse: # respiració d'escala aleatòria
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel: # acceleració/desacceleració de rotació visual
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.0 # segons en estat ferit abans d'explotar
|
||||||
|
blink_hz: 10.0 # parpelleig color normal ↔ wounded
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 36.0 # px mínim respecte al player al spawn
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [0, 255, 255] # Cyan pur "esquivador"
|
||||||
|
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
|
||||||
|
|
||||||
|
score: 100
|
||||||
|
|
||||||
|
events:
|
||||||
|
# Comportament clàssic: dos impactes per matar (set_hurt entra wounded;
|
||||||
|
# el segon hit detecta wounded i destrueix automàticament).
|
||||||
|
on_hit:
|
||||||
|
- action: apply_impulse
|
||||||
|
- action: set_hurt
|
||||||
|
on_hurt_end:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: pinwheel
|
||||||
|
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: enemy_pinwheel.shp
|
||||||
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 4.0 # Més lleuger — àgil
|
||||||
|
speed: 50.0 # px/s (el més ràpid)
|
||||||
|
rotation_delta_min: 3.0 # rad/s — rotació base elevada
|
||||||
|
rotation_delta_max: 6.0
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
behavior:
|
||||||
|
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
|
||||||
|
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
|
||||||
|
proximity_distance: 100.0 # Llindar de distància (px)
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse:
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel:
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.0
|
||||||
|
blink_hz: 10.0
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 36.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 0, 255] # Magenta pur "agressiu"
|
||||||
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
|
score: 200
|
||||||
|
|
||||||
|
events:
|
||||||
|
on_hit:
|
||||||
|
- action: apply_impulse
|
||||||
|
- action: set_hurt
|
||||||
|
on_hurt_end:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: player_ship
|
||||||
|
|
||||||
|
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
||||||
|
# Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor.
|
||||||
|
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
|
||||||
|
# per cada model.
|
||||||
|
#
|
||||||
|
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
|
||||||
|
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
|
||||||
|
# automàtic de la shape; tocar només si el feel del hitbox
|
||||||
|
# no quadra amb la silueta visual (default 1.0).
|
||||||
|
shape:
|
||||||
|
path: ship.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 10.0
|
||||||
|
restitution: 0.6
|
||||||
|
linear_damping: 1.5
|
||||||
|
angular_damping: 0.0
|
||||||
|
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
|
||||||
|
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
|
||||||
|
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
|
||||||
|
# Factor de transferència del moment lineal de la nau a l'enemic en el
|
||||||
|
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
|
||||||
|
death_impact_factor: 0.3
|
||||||
|
|
||||||
|
invulnerability:
|
||||||
|
duration: 3.0 # segons d'invulnerabilitat post-respawn
|
||||||
|
blink_visible: 0.1 # segons visible per cicle de parpelleig
|
||||||
|
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
|
||||||
|
|
||||||
|
hurt:
|
||||||
|
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
|
||||||
|
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
|
||||||
|
|
||||||
|
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
|
||||||
|
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
|
||||||
|
visual_thrust:
|
||||||
|
push_divisor: 33.33
|
||||||
|
scale_divisor: 12.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 255, 255] # blanc neutre
|
||||||
|
hurt: [255, 220, 60] # daurat (estat ferit)
|
||||||
|
|
||||||
|
weapon:
|
||||||
|
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: square
|
||||||
|
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: enemy_square.shp
|
||||||
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 8.0 # Més pesat — "tanc"
|
||||||
|
speed: 40.0 # px/s (velocitat mitjana)
|
||||||
|
rotation_delta_min: 0.3 # rad/s — rotació lenta
|
||||||
|
rotation_delta_max: 1.5
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
behavior:
|
||||||
|
# Square: tracking discret cap a la nau cada N segons.
|
||||||
|
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
|
||||||
|
tracking_interval: 1.0 # segons entre updates d'angle
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse:
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel:
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.0
|
||||||
|
blink_hz: 10.0
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 36.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 0, 0] # Roig pur "tanc"
|
||||||
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
|
score: 150
|
||||||
|
|
||||||
|
events:
|
||||||
|
on_hit:
|
||||||
|
- action: apply_impulse
|
||||||
|
- action: set_hurt
|
||||||
|
on_hurt_end:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: star
|
||||||
|
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: star_5.shp
|
||||||
|
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 5.0
|
||||||
|
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
|
||||||
|
rotation_delta_min: 0.75
|
||||||
|
rotation_delta_max: 3.75
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
behavior:
|
||||||
|
# Hereta el comportament de Pentagon (zigzag esquivador).
|
||||||
|
angle_change_max: 1.0
|
||||||
|
zigzag_prob_per_second: 0.8
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse:
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel:
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.0
|
||||||
|
blink_hz: 10.0
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 36.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 255, 0] # Groc estrella
|
||||||
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
|
score: 100
|
||||||
|
|
||||||
|
events:
|
||||||
|
# STAR: mor al primer impacte, sense passar per wounded.
|
||||||
|
on_hit:
|
||||||
|
- action: apply_impulse
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Orni Attack - locale: Catala (valencia)
|
||||||
|
# Interficie traduida; pool in-game identic a en.yaml (es queda en angles).
|
||||||
|
# Tots els textos en ASCII: VectorText no suporta caracters accentuats.
|
||||||
|
|
||||||
|
notification:
|
||||||
|
press_again_exit: "PREMEU ESC UN ALTRE COP PER EIXIR"
|
||||||
|
zoom: "ZOOM: {z}X"
|
||||||
|
fullscreen_on: "PANTALLA COMPLETA"
|
||||||
|
fullscreen_off: "MODE FINESTRA"
|
||||||
|
vsync_on: "VSYNC ACTIU"
|
||||||
|
vsync_off: "VSYNC INACTIU"
|
||||||
|
antialias_on: "AA ACTIU"
|
||||||
|
antialias_off: "AA INACTIU"
|
||||||
|
postfx_on: "POSTPROCESSAT ACTIU"
|
||||||
|
postfx_off: "POSTPROCESSAT INACTIU"
|
||||||
|
locale_switched: "IDIOMA: {lang}"
|
||||||
|
gamepad_connected: "{name} CONNECTAT"
|
||||||
|
gamepad_disconnected: "{name} DESCONNECTAT"
|
||||||
|
|
||||||
|
language:
|
||||||
|
ca: "CATALA"
|
||||||
|
en: "ANGLES"
|
||||||
|
|
||||||
|
hud:
|
||||||
|
level: "NIVELL "
|
||||||
|
|
||||||
|
title:
|
||||||
|
press_start: "PREMEU START PER JUGAR"
|
||||||
|
|
||||||
|
game_screen:
|
||||||
|
game_over: "FI DEL JOC"
|
||||||
|
continue: "CONTINUAR"
|
||||||
|
continues_left: "CONTINUACIONS: {n}"
|
||||||
|
|
||||||
|
stage:
|
||||||
|
start:
|
||||||
|
- "ORNI ALERT!"
|
||||||
|
- "INCOMING ORNIS!"
|
||||||
|
- "ROLLING THREAT!"
|
||||||
|
- "ENEMY WAVE!"
|
||||||
|
- "WAVE OF ORNIS DETECTED!"
|
||||||
|
- "NEXT SWARM APPROACHING!"
|
||||||
|
- "BRACE FOR THE NEXT WAVE!"
|
||||||
|
- "ANOTHER ATTACK INCOMING!"
|
||||||
|
- "SENSORS DETECT HOSTILE ORNIS..."
|
||||||
|
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
|
||||||
|
- "ENEMY FORCES MOBILIZING!"
|
||||||
|
- "PREPARE FOR IMPACT!"
|
||||||
|
completed: "GOOD JOB COMMANDER!"
|
||||||
|
|
||||||
|
service_menu:
|
||||||
|
title: "MENU DE SERVEI"
|
||||||
|
video: "VIDEO"
|
||||||
|
audio: "AUDIO"
|
||||||
|
options: "OPCIONS"
|
||||||
|
system: "SISTEMA"
|
||||||
|
controls: "CONTROLS"
|
||||||
|
back: "ENRERE"
|
||||||
|
exit: "EIXIR DEL JOC"
|
||||||
|
# Items del submenu VIDEO
|
||||||
|
video_zoom: "ZOOM"
|
||||||
|
video_fullscreen: "PANTALLA COMPLETA"
|
||||||
|
video_vsync: "VSYNC"
|
||||||
|
video_aa: "ANTIALIAS"
|
||||||
|
video_postfx: "POSTPROCESSAT"
|
||||||
|
video_resolution: "RESOLUCIO"
|
||||||
|
# Items del submenu OPCIONS
|
||||||
|
options_language: "IDIOMA"
|
||||||
|
options_show_info: "MOSTRAR INFO"
|
||||||
|
# Items del submenu AUDIO
|
||||||
|
audio_master: "AUDIO"
|
||||||
|
audio_master_volume: "VOLUM GENERAL"
|
||||||
|
audio_music: "MUSICA"
|
||||||
|
audio_music_volume: "VOLUM MUSICA"
|
||||||
|
audio_sound: "EFECTES"
|
||||||
|
audio_sound_volume: "VOLUM EFECTES"
|
||||||
|
# Items del submenu SISTEMA
|
||||||
|
system_restart: "REINICIAR"
|
||||||
|
# Pagines de confirmacio (estructura: titol + NO/SI)
|
||||||
|
confirm_restart: "ESTAS SEGUR DE REINICIAR?"
|
||||||
|
confirm_exit: "ESTAS SEGUR DE EIXIR?"
|
||||||
|
confirm_no: "NO"
|
||||||
|
confirm_yes: "SI"
|
||||||
|
# Valors comuns
|
||||||
|
value_on: "ACTIU"
|
||||||
|
value_off: "INACTIU"
|
||||||
|
# Items del submenu CONTROLS
|
||||||
|
controls_pad_p1: "MANDO JUGADOR 1"
|
||||||
|
controls_pad_p2: "MANDO JUGADOR 2"
|
||||||
|
controls_no_pad: "SENSE MANDO"
|
||||||
|
controls_define_keyboard_p1: "REDEFINIR TECLES P1"
|
||||||
|
controls_define_keyboard_p2: "REDEFINIR TECLES P2"
|
||||||
|
controls_define_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||||
|
controls_define_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||||
|
|
||||||
|
# Overlay modal de redefinicio (DefineInputs)
|
||||||
|
define:
|
||||||
|
title_keyboard_p1: "REDEFINIR TECLES P1"
|
||||||
|
title_keyboard_p2: "REDEFINIR TECLES P2"
|
||||||
|
title_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||||
|
title_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||||
|
press_key: "PREMEU UNA TECLA"
|
||||||
|
press_button: "PREMEU UN BOTO"
|
||||||
|
complete: "CONFIGURACIO COMPLETA"
|
||||||
|
no_gamepad: "CAP MANDO ASSIGNAT AL JUGADOR"
|
||||||
|
action:
|
||||||
|
left: "ESQUERRA"
|
||||||
|
right: "DRETA"
|
||||||
|
fire: "DISPARAR"
|
||||||
|
accelerate: "ACCELERAR"
|
||||||
|
start: "START"
|
||||||
|
menu: "MENU"
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Orni Attack - locale: English
|
||||||
|
# In-game pool kept English in both locales per design.
|
||||||
|
|
||||||
|
notification:
|
||||||
|
press_again_exit: "PRESS ESC AGAIN TO EXIT"
|
||||||
|
zoom: "ZOOM: {z}X"
|
||||||
|
fullscreen_on: "FULLSCREEN"
|
||||||
|
fullscreen_off: "WINDOWED"
|
||||||
|
vsync_on: "VSYNC ON"
|
||||||
|
vsync_off: "VSYNC OFF"
|
||||||
|
antialias_on: "AA ON"
|
||||||
|
antialias_off: "AA OFF"
|
||||||
|
postfx_on: "POSTPROCESS ON"
|
||||||
|
postfx_off: "POSTPROCESS OFF"
|
||||||
|
locale_switched: "LANGUAGE: {lang}"
|
||||||
|
gamepad_connected: "{name} CONNECTED"
|
||||||
|
gamepad_disconnected: "{name} DISCONNECTED"
|
||||||
|
|
||||||
|
language:
|
||||||
|
ca: "CATALAN"
|
||||||
|
en: "ENGLISH"
|
||||||
|
|
||||||
|
hud:
|
||||||
|
level: "LEVEL "
|
||||||
|
|
||||||
|
title:
|
||||||
|
press_start: "PRESS START TO PLAY"
|
||||||
|
|
||||||
|
game_screen:
|
||||||
|
game_over: "GAME OVER"
|
||||||
|
continue: "CONTINUE"
|
||||||
|
continues_left: "CONTINUES LEFT: {n}"
|
||||||
|
|
||||||
|
stage:
|
||||||
|
start:
|
||||||
|
- "ORNI ALERT!"
|
||||||
|
- "INCOMING ORNIS!"
|
||||||
|
- "ROLLING THREAT!"
|
||||||
|
- "ENEMY WAVE!"
|
||||||
|
- "WAVE OF ORNIS DETECTED!"
|
||||||
|
- "NEXT SWARM APPROACHING!"
|
||||||
|
- "BRACE FOR THE NEXT WAVE!"
|
||||||
|
- "ANOTHER ATTACK INCOMING!"
|
||||||
|
- "SENSORS DETECT HOSTILE ORNIS..."
|
||||||
|
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
|
||||||
|
- "ENEMY FORCES MOBILIZING!"
|
||||||
|
- "PREPARE FOR IMPACT!"
|
||||||
|
completed: "GOOD JOB COMMANDER!"
|
||||||
|
|
||||||
|
service_menu:
|
||||||
|
title: "SERVICE MENU"
|
||||||
|
video: "VIDEO"
|
||||||
|
audio: "AUDIO"
|
||||||
|
options: "OPTIONS"
|
||||||
|
system: "SYSTEM"
|
||||||
|
controls: "CONTROLS"
|
||||||
|
back: "BACK"
|
||||||
|
exit: "EXIT GAME"
|
||||||
|
# Items of VIDEO submenu
|
||||||
|
video_zoom: "ZOOM"
|
||||||
|
video_fullscreen: "FULLSCREEN"
|
||||||
|
video_vsync: "VSYNC"
|
||||||
|
video_aa: "ANTIALIAS"
|
||||||
|
video_postfx: "POSTPROCESS"
|
||||||
|
video_resolution: "RESOLUTION"
|
||||||
|
# Items of OPTIONS submenu
|
||||||
|
options_language: "LANGUAGE"
|
||||||
|
options_show_info: "SHOW INFO"
|
||||||
|
# Items of AUDIO submenu
|
||||||
|
audio_master: "AUDIO"
|
||||||
|
audio_master_volume: "MASTER VOLUME"
|
||||||
|
audio_music: "MUSIC"
|
||||||
|
audio_music_volume: "MUSIC VOLUME"
|
||||||
|
audio_sound: "SOUNDS"
|
||||||
|
audio_sound_volume: "SOUND VOLUME"
|
||||||
|
# Items of SYSTEM submenu
|
||||||
|
system_restart: "RESTART"
|
||||||
|
# Confirmation pages (structure: title + NO/YES)
|
||||||
|
confirm_restart: "REALLY RESTART?"
|
||||||
|
confirm_exit: "REALLY EXIT?"
|
||||||
|
confirm_no: "NO"
|
||||||
|
confirm_yes: "YES"
|
||||||
|
# Common values
|
||||||
|
value_on: "ON"
|
||||||
|
value_off: "OFF"
|
||||||
|
# Items of CONTROLS submenu
|
||||||
|
controls_pad_p1: "PLAYER 1 GAMEPAD"
|
||||||
|
controls_pad_p2: "PLAYER 2 GAMEPAD"
|
||||||
|
controls_no_pad: "NO GAMEPAD"
|
||||||
|
controls_define_keyboard_p1: "REDEFINE KEYS P1"
|
||||||
|
controls_define_keyboard_p2: "REDEFINE KEYS P2"
|
||||||
|
controls_define_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||||
|
controls_define_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||||
|
|
||||||
|
# Modal overlay for input redefinition (DefineInputs)
|
||||||
|
define:
|
||||||
|
title_keyboard_p1: "REDEFINE KEYS P1"
|
||||||
|
title_keyboard_p2: "REDEFINE KEYS P2"
|
||||||
|
title_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||||
|
title_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||||
|
press_key: "PRESS A KEY"
|
||||||
|
press_button: "PRESS A BUTTON"
|
||||||
|
complete: "CONFIGURATION COMPLETE"
|
||||||
|
no_gamepad: "NO GAMEPAD ASSIGNED TO PLAYER"
|
||||||
|
action:
|
||||||
|
left: "LEFT"
|
||||||
|
right: "RIGHT"
|
||||||
|
fire: "FIRE"
|
||||||
|
accelerate: "ACCELERATE"
|
||||||
|
start: "START"
|
||||||
|
menu: "MENU"
|
||||||
+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
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# bullet_double.shp - Bala anular (dos cercles concèntrics)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Dos octàgons concèntrics al centre (0,0):
|
||||||
|
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
|
||||||
|
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
|
||||||
|
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
|
||||||
|
|
||||||
|
name: bullet_double
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Cercle exterior (octàgon, radi 4)
|
||||||
|
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
|
||||||
|
|
||||||
|
# Cercle interior (octàgon, radi 2)
|
||||||
|
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# bullet_long.shp - Bala allargada (dos octàgons tangents + tapes superior i inferior)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Dos cercles (octàgons radi 3) tangents externament al punt (0,0), units
|
||||||
|
# per una línia horitzontal superior i una d'inferior. La silueta resultant
|
||||||
|
# és una càpsula amb la separació visible dels dos cercles al centre.
|
||||||
|
#
|
||||||
|
# Geometria:
|
||||||
|
# Centre octàgon esquerre: (-3, 0)
|
||||||
|
# Centre octàgon dret: ( 3, 0)
|
||||||
|
# Punt de tangència: ( 0, 0)
|
||||||
|
# Bounding radius natiu ≈ 6 (extrem horitzontal a x=±6).
|
||||||
|
|
||||||
|
name: bullet_long
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Octàgon esquerre (centre x=-3, radi 3)
|
||||||
|
polyline: -3,-3 -0.88,-2.12 0,0 -0.88,2.12 -3,3 -5.12,2.12 -6,0 -5.12,-2.12 -3,-3
|
||||||
|
|
||||||
|
# Octàgon dret (centre x=3, radi 3)
|
||||||
|
polyline: 3,-3 5.12,-2.12 6,0 5.12,2.12 3,3 0.88,2.12 0,0 0.88,-2.12 3,-3
|
||||||
|
|
||||||
|
# Tapa superior: uneix el cim de l'octàgon esquerre amb el del dret
|
||||||
|
polyline: -3,-3 3,-3
|
||||||
|
|
||||||
|
# Tapa inferior: uneix la base de l'octàgon esquerre amb la del dret
|
||||||
|
polyline: -3,3 3,3
|
||||||
@@ -1,21 +1,11 @@
|
|||||||
# enemy_pentagon.shp - ORNI enemic (pentàgon regular)
|
# enemy_pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=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
|
# Pentàgon exterior (vèrtex apuntant amunt, 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
|
||||||
|
|
||||||
|
# Pentàgon interior (radi 10, rotat 36° → vèrtex apuntant a les arestes exteriors)
|
||||||
|
polyline: 5.88,-8.09 9.51,3.09 0,10 -9.51,3.09 -5.88,-8.09 5.88,-8.09
|
||||||
|
|||||||
@@ -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,14 @@
|
|||||||
# enemy_square.shp - ORNI enemic (quadrat regular)
|
# enemy_square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
|
||||||
# © 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)
|
# Rombe exterior
|
||||||
# 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
|
||||||
|
|
||||||
|
# Ull (dos arcs units, forma d'almetlla). Amplada 20px, altura 8px.
|
||||||
|
polyline: -10,0 -5,-3 0,-4 5,-3 10,0 5,3 0,4 -5,3 -10,0
|
||||||
|
|
||||||
|
# Pupil·la (octàgon, radi 2) al centre
|
||||||
|
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# char_lparen.shp - Símbol ( (parèntesi esquerre)
|
||||||
|
# Dimensions: 20×40 (blocky display)
|
||||||
|
|
||||||
|
name: char_lparen
|
||||||
|
scale: 1.0
|
||||||
|
center: 10, 20
|
||||||
|
|
||||||
|
# Arc cap a l'esquerra aproximat amb 4 trams rectes
|
||||||
|
polyline: 14,4 8,12 6,20 8,28 14,36
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# char_rparen.shp - Símbol ) (parèntesi dret)
|
||||||
|
# Dimensions: 20×40 (blocky display)
|
||||||
|
|
||||||
|
name: char_rparen
|
||||||
|
scale: 1.0
|
||||||
|
center: 10, 20
|
||||||
|
|
||||||
|
# Arc cap a la dreta aproximat amb 4 trams rectes
|
||||||
|
polyline: 6,4 12,12 14,20 12,28 6,36
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# char_slash.shp - Símbol / (barra)
|
||||||
|
# Dimensions: 20×40 (blocky display)
|
||||||
|
|
||||||
|
name: char_slash
|
||||||
|
scale: 1.0
|
||||||
|
center: 10, 20
|
||||||
|
|
||||||
|
# Línia diagonal de baix-esquerra a dalt-dreta
|
||||||
|
line: 4,36 16,4
|
||||||
+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,28 +0,0 @@
|
|||||||
# ship2_perspective.shp - Nave P2 con perspectiva pre-calculada
|
|
||||||
# Posición optimizada: "4 del reloj" (Abajo-Derecha)
|
|
||||||
# Dirección: Volando hacia el fondo (centro pantalla)
|
|
||||||
|
|
||||||
name: ship2_perspective
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# TRANSFORMACIÓN APLICADA:
|
|
||||||
# 1. Rotación -45° (apuntando al centro desde abajo-dcha)
|
|
||||||
# 2. Proyección de perspectiva:
|
|
||||||
# - Punta (p1): Reducida al 60% (simula lejanía)
|
|
||||||
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
|
|
||||||
# 3. Flip horizontal (simétrica a ship_starfield.shp)
|
|
||||||
#
|
|
||||||
# Nuevos Punts (aprox):
|
|
||||||
# p1 (Punta): (-4, -4) -> Lejos, pequeña y apuntando arriba-izq
|
|
||||||
# p2 (Ala Izq): (-3, 11) -> Cerca, lado interior
|
|
||||||
# p4 (Base Cnt): (3, 5) -> Centro base
|
|
||||||
# p3 (Ala Dcha): (11, 2) -> Cerca, lado exterior (más grande)
|
|
||||||
|
|
||||||
#polyline: -4,-4 -3,11 3,5 11,2 -4,-4
|
|
||||||
polyline: -4,-4 -3,11 11,2 -4,-4
|
|
||||||
|
|
||||||
# Circulito central (octàgon r=2.5)
|
|
||||||
# Distintiu visual del jugador 2
|
|
||||||
# Sin perspectiva (está en el centro de la nave)
|
|
||||||
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
|
|
||||||
@@ -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,21 +0,0 @@
|
|||||||
# ship_perspective.shp - Nave con perspectiva pre-calculada
|
|
||||||
# Posición optimizada: "8 del reloj" (Abajo-Izquierda)
|
|
||||||
# Dirección: Volando hacia el fondo (centro pantalla)
|
|
||||||
|
|
||||||
name: ship_perspective
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# TRANSFORMACIÓN APLICADA:
|
|
||||||
# 1. Rotación +45° (apuntando al centro desde abajo-izq)
|
|
||||||
# 2. Proyección de perspectiva:
|
|
||||||
# - Punta (p1): Reducida al 60% (simula lejanía)
|
|
||||||
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
|
|
||||||
#
|
|
||||||
# Nuevos Puntos (aprox):
|
|
||||||
# p1 (Punta): (4, -4) -> Lejos, pequeña y apuntando arriba-dcha
|
|
||||||
# p2 (Ala Dcha): (3, 11) -> Cerca, lado interior
|
|
||||||
# p4 (Base Cnt): (-3, 5) -> Centro base
|
|
||||||
# p3 (Ala Izq): (-11, 2) -> Cerca, lado exterior (más grande)
|
|
||||||
|
|
||||||
polyline: 4,-4 3,11 -3,5 -11,2 4,-4
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# star_5.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
|
||||||
|
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
|
||||||
|
# Vèrtex apuntant amunt (igual que enemy_pentagon).
|
||||||
|
#
|
||||||
|
# Sense línies interiors: una única polyline que recorre el perímetre.
|
||||||
|
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
|
||||||
|
|
||||||
|
name: star_5
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
|
||||||
|
# 4 puntes als cardinals (radi 30) i valls còncaus als 45° (corba Bezier
|
||||||
|
# quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
|
||||||
|
|
||||||
|
name: title_flash
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
polyline: 0,-30 3.76,-21.76 8.64,-14.64 14.64,-8.64 21.76,-3.76 30,0 21.76,3.76 14.64,8.64 8.64,14.64 3.76,21.76 0,30 -3.76,21.76 -8.64,14.64 -14.64,8.64 -21.76,3.76 -30,0 -21.76,-3.76 -14.64,-8.64 -8.64,-14.64 -3.76,-21.76 0,-30
|
||||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18
-17
@@ -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"
|
||||||
@@ -7,17 +7,18 @@ metadata:
|
|||||||
description: "Progressive difficulty curve from novice to expert"
|
description: "Progressive difficulty curve from novice to expert"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
# STAGE 1: Tutorial - Only pentagons, slow speed
|
# STAGE 1: Tutorial - Mix de tots 4 tipus al 25% per mostrar-los junts
|
||||||
- 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: 25
|
||||||
quadrat: 0
|
cuadrado: 25
|
||||||
molinillo: 0
|
molinillo: 25
|
||||||
|
star: 25
|
||||||
difficulty_multipliers:
|
difficulty_multipliers:
|
||||||
speed_multiplier: 0.7
|
speed_multiplier: 0.7
|
||||||
rotation_multiplier: 0.8
|
rotation_multiplier: 0.8
|
||||||
@@ -32,7 +33,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 +49,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 +65,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 +81,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 +97,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 +113,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 +129,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 +145,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 +161,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: '-*'
|
|
||||||
+225
-114
@@ -1,183 +1,294 @@
|
|||||||
#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
|
auto* resource = AudioResource::getMusic(name);
|
||||||
auto* resource = AudioCache::getMusic(name);
|
if (resource == nullptr) { return; }
|
||||||
if (resource == nullptr) {
|
|
||||||
// manejo de error opcional
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
|
playMusicInternal(resource, loop, crossfade_ms);
|
||||||
if (music_.state == MusicState::PLAYING) {
|
|
||||||
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
|
|
||||||
}
|
|
||||||
|
|
||||||
// Llamada al motor para reproducir la nueva pista
|
|
||||||
JA_PlayMusic(resource, loop);
|
|
||||||
|
|
||||||
// Actualizar estado y metadatos después de iniciar con éxito
|
|
||||||
music_.name = name;
|
music_.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 == 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 same-track early
|
||||||
|
// return i del nom. El gate de música deshabilitada NO atura la reproducció:
|
||||||
|
// effectiveVolume porta el volum efectiu a 0 i la pista continua sonant
|
||||||
|
// silenciada, per garantir que reactivar la música la torne a sentir sense
|
||||||
|
// haver de reiniciar la pista. 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 (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 (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_) {
|
engine_->stopMusic();
|
||||||
JA_StopMusic();
|
}
|
||||||
music_.state = MusicState::STOPPED;
|
|
||||||
|
void Audio::setMusicSpeed(float ratio) {
|
||||||
|
engine_->setMusicSpeed(ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reprodueix un so per nom
|
||||||
|
void Audio::playSound(const std::string& name, Group group) {
|
||||||
|
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reprodueix un so per punter directe
|
||||||
|
void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||||
|
if (sound != nullptr) {
|
||||||
|
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reproduce un sonido por nombre
|
// Variant con velocitat (i to) escalats. Apliquem el ratio al canal
|
||||||
void Audio::playSound(const std::string& name, Group group) const {
|
// just retornat per `playSound`: así el `SDL_AudioStream` recent creat
|
||||||
if (sound_enabled_) {
|
// processa tot el sample con el ratio des del primer pull del callback.
|
||||||
JA_PlaySound(AudioCache::getSound(name), 0, static_cast<int>(group));
|
// 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) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reproduce un sonido por puntero directo
|
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
|
||||||
void Audio::playSound(JA_Sound_t* sound, Group group) const {
|
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
|
||||||
if (sound_enabled_) {
|
// sec — l'usuari sent el so aún que la cua no s'apliqui.
|
||||||
JA_PlaySound(sound, 0, static_cast<int>(group));
|
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detiene todos los sonidos
|
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
||||||
void Audio::stopAllSounds() const {
|
// fallback que playSoundWithEcho.
|
||||||
if (sound_enabled_) {
|
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
||||||
JA_StopChannel(-1);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Realiza un fundido de salida de la música
|
// Atura tots los sons
|
||||||
void Audio::fadeOutMusic(int milliseconds) const {
|
void Audio::stopAllSounds() {
|
||||||
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
|
engine_->stopChannel(-1);
|
||||||
JA_FadeOutMusic(milliseconds);
|
}
|
||||||
|
|
||||||
|
// Fa una fosa de sortida de la música
|
||||||
|
void Audio::fadeOutMusic(int milliseconds) {
|
||||||
|
if (getMusicState() == MusicState::PLAYING) {
|
||||||
|
engine_->fadeOutMusic(milliseconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consulta directamente el estado real de la música en jailaudio
|
// Registra un callback que el motor dispararà cuando la pista actual acabi de
|
||||||
auto Audio::getRealMusicState() -> MusicState {
|
// drenar (times == 0 + stream buit). S'executa al mismo thread que
|
||||||
JA_Music_state ja_state = JA_GetMusicState();
|
// Audio::update (render loop); los consumidors no poden fer I/O blocant.
|
||||||
switch (ja_state) {
|
void Audio::setOnMusicEnded(std::function<void()> callback) {
|
||||||
case JA_MUSIC_PLAYING:
|
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resol el nom contra el cache de recursos i retorna la duración pre-calculada
|
||||||
|
// al `loadMusic`. 0 si la pista no existeix — así el caller pot decidir
|
||||||
|
// fallback (p. ex. usar un timeout fix) sin haver de propagar errors.
|
||||||
|
auto Audio::getMusicDurationMs(const std::string& name) -> int {
|
||||||
|
auto* music = AudioResource::getMusic(name);
|
||||||
|
return (music != nullptr) ? music->duration_ms : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consulta directament l'estat a Ja y el projecta al subconjunt d'estats que
|
||||||
|
// exposa Audio (INVALID/DISABLED de Ja col·lapsen a STOPPED — la capa d'usuari
|
||||||
|
// solo vol saber si está sonant, pausat o parat).
|
||||||
|
auto Audio::getMusicState() -> MusicState {
|
||||||
|
if (!instance || !instance->engine_) { return MusicState::STOPPED; }
|
||||||
|
switch (instance->engine_->getMusicState()) {
|
||||||
|
case Ja::MusicState::PLAYING:
|
||||||
return MusicState::PLAYING;
|
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). Actualitza el valor cachejat
|
||||||
void Audio::setMusicVolume(float music_volume) const {
|
// a config_ perquè els getters i les re-aplicacions internes (enableSound,
|
||||||
if (music_enabled_) {
|
// setMasterVolume) puguin tornar al volum que l'usuari va triar.
|
||||||
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||||
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
|
config_.sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
JA_SetMusicVolume(CONVERTED_VOLUME);
|
engine_->setSoundVolume(effectiveVolume(config_.sound_volume, sound_enabled_), static_cast<int>(group));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica la configuración
|
// Estableix el volum de la música (float 0.0..1.0). Cf. setSoundVolume.
|
||||||
void Audio::applySettings() {
|
void Audio::setMusicVolume(float music_volume) {
|
||||||
enable(Options::audio.enabled);
|
config_.music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
engine_->setMusicVolume(effectiveVolume(config_.music_volume, music_enabled_));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Establecer estado general
|
// Estableix el volum master (multiplicador aplicat a sound + music). Re-aplica
|
||||||
|
// els canals perquè el canvi tingui efecte immediat sense esperar al següent
|
||||||
|
// setSoundVolume/setMusicVolume explícit.
|
||||||
|
void Audio::setMasterVolume(float master_volume) {
|
||||||
|
config_.volume = std::clamp(master_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
setSoundVolume(config_.sound_volume);
|
||||||
|
setMusicVolume(config_.music_volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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. Re-aplica els volums actuals; effectiveVolume
|
||||||
|
// retalla a 0 quan enabled_ és false, sense perdre els valors guardats.
|
||||||
void Audio::enable(bool value) {
|
void Audio::enable(bool value) {
|
||||||
enabled_ = value;
|
enabled_ = value;
|
||||||
|
setSoundVolume(config_.sound_volume);
|
||||||
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
|
setMusicVolume(config_.music_volume);
|
||||||
setMusicVolume(enabled_ ? Options::audio.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);
|
||||||
|
}
|
||||||
|
|||||||
+151
-78
@@ -1,97 +1,170 @@
|
|||||||
#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
|
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
|
||||||
|
|
||||||
// --- Configuración de sonidos ---
|
// Getters dels volums actuals (lectura de la config_ cachejada). Reflexen
|
||||||
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
|
// el valor que l'usuari ha triat l'última vegada, independent del gating
|
||||||
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
|
// d'enabled/channel.
|
||||||
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
|
[[nodiscard]] auto getMasterVolume() const -> float { return config_.volume; }
|
||||||
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
|
[[nodiscard]] auto getSoundVolume() const -> float { return config_.sound_volume; }
|
||||||
|
[[nodiscard]] auto getMusicVolume() const -> float { return config_.music_volume; }
|
||||||
|
|
||||||
// --- Configuración de música ---
|
// --- Helpers de conversió para la capa de presentació ---
|
||||||
void enableMusic() { music_enabled_ = true; } // Habilitar música
|
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||||
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
|
// No són constexpr porque std::lround no ho es en C++20; s'usen en runtime.
|
||||||
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
|
static auto toPercent(float volume) -> int {
|
||||||
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
|
return static_cast<int>(std::lround(volume * 100.0F));
|
||||||
|
}
|
||||||
|
static auto fromPercent(int percent) -> float {
|
||||||
|
return static_cast<float>(percent) / 100.0F;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Consultas de estado ---
|
// --- Configuración general ---
|
||||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
void enable(bool value); // Estableix l'estat general (reaplica volums)
|
||||||
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
void toggleEnabled() { enable(!enabled_); } // Alterna l'estat general (reaplica volums)
|
||||||
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
void applySettings(const Config& config); // Aplica una nueva configuración
|
||||||
[[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 sons ---
|
||||||
// --- Tipos anidados ---
|
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
|
||||||
struct Music {
|
void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (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 ---
|
// --- Configuración de música ---
|
||||||
Audio(); // Constructor privado
|
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
|
||||||
~Audio(); // Destructor privado
|
void toggleMusic() { enableMusic(!music_enabled_); } // Alterna l'estat de la música (reaplica volum)
|
||||||
void initSDLAudio(); // Inicializa SDL Audio
|
|
||||||
|
|
||||||
// --- Variables miembro ---
|
// --- Consultes d'estat ---
|
||||||
static Audio* instance; // Instancia única de Audio
|
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||||
|
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
||||||
|
[[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;
|
||||||
|
|
||||||
Music music_; // Estado de la música
|
private:
|
||||||
bool enabled_{true}; // Estado general del audio
|
// --- Tipus anidats ---
|
||||||
bool sound_enabled_{true}; // Estado de los efectos de sonido
|
struct Music {
|
||||||
bool music_enabled_{true}; // Estado de la música
|
std::string name; // Última pista de música reproduïda (buida si es va passar per punter sin filename)
|
||||||
};
|
bool loop{false}; // Si el play actual es en bucle
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Mètodes ---
|
||||||
|
explicit Audio(const Config& config); // Constructor privat: rep la config
|
||||||
|
void initSDLAudio(); // Inicialitza SDL Audio
|
||||||
|
void playMusicInternal(Ja::Music* music, int loop, int crossfade_ms); // Camí comú dels dos overloads de playMusic
|
||||||
|
[[nodiscard]] auto effectiveVolume(float volume, bool channel_enabled) const -> float; // Gate master+channel: 0 si algun está off, clamp 0..1 altrament
|
||||||
|
|
||||||
|
// --- Variables 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 = 50;
|
||||||
|
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,83 @@
|
|||||||
|
// 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 <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
|
||||||
|
int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button
|
||||||
|
int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlayerBindings {
|
||||||
|
KeyboardBindings keyboard{};
|
||||||
|
GamepadBindings gamepad{};
|
||||||
|
std::string gamepad_name; // Empty = auto-assign by index
|
||||||
|
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AudioConfig {
|
||||||
|
bool enabled{true};
|
||||||
|
float volume{1.0F}; // Master 0..1
|
||||||
|
bool music_enabled{true};
|
||||||
|
float music_volume{1.0F};
|
||||||
|
bool sound_enabled{true};
|
||||||
|
float sound_volume{0.25F};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EngineConfig {
|
||||||
|
WindowConfig window{};
|
||||||
|
RenderingConfig rendering{};
|
||||||
|
AudioConfig audio{};
|
||||||
|
PlayerBindings player1{};
|
||||||
|
PlayerBindings player2{};
|
||||||
|
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
|
||||||
|
GamepadBindings gamepad_controls{};
|
||||||
|
bool console{false};
|
||||||
|
std::string locale{"ca"}; // "ca" | "en" — fixat a l'arrencada, sense hot-swap
|
||||||
|
};
|
||||||
|
|
||||||
|
} // 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/starfield_parallax.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,49 @@
|
|||||||
|
// 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* HURT = "effects/hurt.wav"; // Nau pròpia entra a 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,87 @@
|
|||||||
|
// effects.hpp - Constants per a efectes visuals (fireworks, etc.)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Defaults::FX::Glow {
|
||||||
|
|
||||||
|
// Neon glow per outline gruixut, aplicat automàticament per renderShape.
|
||||||
|
// Els gruixos d'halo són RÀTIOS del bounding_radius de la shape (escalat
|
||||||
|
// per scale), de manera que un pentàgon (radius 20) té halo gros i una bala
|
||||||
|
// (radius 3) té halo subtil. El core (últim pass) usa el gruix de línia
|
||||||
|
// global (1.5px) — no escala amb la shape.
|
||||||
|
//
|
||||||
|
// Cap superior: si la shape és molt gran (logos del títol, intro), el
|
||||||
|
// bounding_radius es satura a aquest valor — així cap shape té més
|
||||||
|
// glow que el pentàgon (referència de gameplay).
|
||||||
|
constexpr float MAX_REFERENCE_RADIUS = 20.0F;
|
||||||
|
|
||||||
|
struct Pass {
|
||||||
|
float thickness_ratio; // % del bounding_radius*scale. <0 → usa core (gruix global)
|
||||||
|
float alpha;
|
||||||
|
};
|
||||||
|
constexpr Pass PASSES[] = {
|
||||||
|
{.thickness_ratio = 0.55F, .alpha = 0.07F},
|
||||||
|
{.thickness_ratio = 0.35F, .alpha = 0.14F},
|
||||||
|
{.thickness_ratio = 0.20F, .alpha = 0.28F},
|
||||||
|
{.thickness_ratio = -1.0F, .alpha = 1.0F}, // core: línia "real"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Glow per a línies "raw" (sense shape). Gruixos absoluts (px), no
|
||||||
|
// ratios — una línia individual no té bounding radius. Útil per a
|
||||||
|
// partícules de firework, sparks, etc.
|
||||||
|
namespace Line {
|
||||||
|
struct Pass {
|
||||||
|
float thickness; // px. <0 → usa el thickness passat pel caller (core)
|
||||||
|
float alpha;
|
||||||
|
};
|
||||||
|
constexpr Pass PASSES[] = {
|
||||||
|
{.thickness = 18.0F, .alpha = 0.10F},
|
||||||
|
{.thickness = 12.0F, .alpha = 0.20F},
|
||||||
|
{.thickness = 6.0F, .alpha = 0.40F},
|
||||||
|
{.thickness = -1.0F, .alpha = 1.0F}, // core: línia "real"
|
||||||
|
};
|
||||||
|
} // namespace Line
|
||||||
|
|
||||||
|
} // namespace Defaults::FX::Glow
|
||||||
|
|
||||||
|
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,18 @@
|
|||||||
|
// enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Tots els paràmetres jugables (physics, animation, wounded, spawn,
|
||||||
|
// behavior, colors, scoring) viuen a data/entities/<type>/<type>.yaml i
|
||||||
|
// s'accedeixen via EnemyRegistry::get(EnemyType). Aquí només queda el
|
||||||
|
// que no és per personalitzar per tipus.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Spawn {
|
||||||
|
|
||||||
|
// Sostre de reintents al cercar una posició de spawn que respecti el
|
||||||
|
// safety_distance del tipus. No és un paràmetre jugable: és el llindar
|
||||||
|
// tècnic abans de caure a un fallback aleatori amb advertència.
|
||||||
|
constexpr int MAX_SPAWN_ATTEMPTS = 50;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Spawn
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 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_BULLETS = 50;
|
||||||
|
|
||||||
|
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
|
||||||
|
// calcula el seu collision_radius com a
|
||||||
|
// shape.bounding_radius × shape.scale × shape.collision_factor
|
||||||
|
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
|
||||||
|
|
||||||
|
} // 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,101 @@
|
|||||||
|
// 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)
|
||||||
|
// Ha de ser ≥ 1.0F: PhysicsWorld separa els cossos al contacte exacte (dist == suma de radis),
|
||||||
|
// així que un amplificador < 1 fa que el check de gameplay no es dispari mai. Marge petit
|
||||||
|
// (1.05F) per tolerar floating-point i petites separacions post-impuls.
|
||||||
|
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 1.05F;
|
||||||
|
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
|
||||||
|
// Wounded chain: el rebot físic separa els cossos abans que arribi
|
||||||
|
// la detecció gameplay; amplier generós perquè el toc compti.
|
||||||
|
constexpr float COLLISION_WOUNDED_CHAIN_AMPLIFIER = 1.25F;
|
||||||
|
|
||||||
|
// Friendly fire system
|
||||||
|
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||||
|
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
||||||
|
// BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
|
||||||
|
|
||||||
|
// 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 zone 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,46 @@
|
|||||||
|
// 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 FPS_LINE_HEIGHT = 28.0F; // separació després del FPS (scale 0.7 → ~28 px)
|
||||||
|
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
|
||||||
|
constexpr float FPS_SCALE = 0.7F; // FPS més gran que la resta
|
||||||
|
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,23 @@
|
|||||||
|
// 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.
|
||||||
|
// Tots els colors d'entitats han migrat al seu YAML respectiu
|
||||||
|
// (data/entities/<name>/<name>.yaml, secció `colors`):
|
||||||
|
// - SHIP → player.yaml
|
||||||
|
// - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
|
||||||
|
// - BULLET → bullet.yaml
|
||||||
|
|
||||||
|
} // namespace Defaults::Palette
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// physics.hpp - Constants de física del control de la nau i debris d'explosió
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// NOTA: els paràmetres del player (rotation_speed, acceleration,
|
||||||
|
// max_velocity, death_impact_factor) viuen a data/entities/player/player.yaml.
|
||||||
|
// Els paràmetres específics de la bala (mass, restitution, damping,
|
||||||
|
// impact_momentum_factor) viuen a data/entities/bullet/bullet.yaml.
|
||||||
|
// Aquest fitxer només conté els paràmetres compartits del subsistema de
|
||||||
|
// debris (explosions visuals).
|
||||||
|
|
||||||
|
namespace Defaults::Physics::Debris {
|
||||||
|
|
||||||
|
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||||
|
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
|
||||||
|
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
||||||
|
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||||
|
constexpr float ROTATION_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 INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||||
|
constexpr float INHERITANCE_FACTOR_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 SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
||||||
|
|
||||||
|
} // namespace Defaults::Physics::Debris
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// playfield.hpp - Configuració del fons del playfield (graella, sub-graella, animació)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
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 = 4; // cada cel·la principal es divideix en N subcel·les
|
||||||
|
|
||||||
|
// Brillo respecte al color global (border = 1.0)
|
||||||
|
constexpr float GRID_BRIGHTNESS = 0.20F;
|
||||||
|
constexpr float SUBGRID_BRIGHTNESS = 0.10F;
|
||||||
|
|
||||||
|
// Color de la rejilla (lila/violeta synthwave). Es modula amb brillantor.
|
||||||
|
constexpr SDL_Color GRID_COLOR = {.r = 160, .g = 80, .b = 255, .a = 255};
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Ripples: deformacions circulars que travessen la graella com ones d'aigua.
|
||||||
|
// Cada ripple desplaça radialment cap a fora els vèrtexs de les línies que
|
||||||
|
// travessa, amb una envoltant que decau a les vores de l'anell i amb el temps.
|
||||||
|
namespace Ripple {
|
||||||
|
constexpr int POOL_SIZE = 32;
|
||||||
|
|
||||||
|
// Ones grans (explosions / fireworks).
|
||||||
|
constexpr float BIG_AMPLITUDE_PX = 10.0F;
|
||||||
|
constexpr float BIG_SPEED_PX_S = 320.0F;
|
||||||
|
constexpr float BIG_LIFETIME_S = 1.4F;
|
||||||
|
constexpr float BIG_THICKNESS_PX = 40.0F;
|
||||||
|
|
||||||
|
// Ones petites (pas de nau, cadència estil trail).
|
||||||
|
constexpr float SMALL_AMPLITUDE_PX = 2.5F;
|
||||||
|
constexpr float SMALL_SPEED_PX_S = 160.0F;
|
||||||
|
constexpr float SMALL_LIFETIME_S = 0.55F;
|
||||||
|
constexpr float SMALL_THICKNESS_PX = 18.0F;
|
||||||
|
|
||||||
|
// Cadència "soltar gotetes" per nau (patró TrailManager).
|
||||||
|
constexpr float SHIP_COOLDOWN_S = 0.10F;
|
||||||
|
constexpr float SHIP_COOLDOWN_JITTER_S = 0.03F;
|
||||||
|
constexpr float SHIP_SPEED_THRESHOLD_PX_S = 80.0F;
|
||||||
|
|
||||||
|
// Subdivisió de línies quan estan dins una ripple.
|
||||||
|
constexpr int MAIN_SEGMENTS = 24; // línies principals
|
||||||
|
constexpr int SUB_SEGMENTS = 12; // sub-graella
|
||||||
|
} // namespace Ripple
|
||||||
|
|
||||||
|
} // 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,63 @@
|
|||||||
|
// service_menu.hpp - Constants del menu de servei (F12)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Defaults::ServiceMenu {
|
||||||
|
|
||||||
|
// ---- Mides en coordenades logiques del joc (1280×720) ----
|
||||||
|
// BOX_WIDTH_MIN es el minim: si el titol o algun item no hi caben, el
|
||||||
|
// marc s'expandeix dinamicament amb animacio (cf. WIDTH_RATE).
|
||||||
|
constexpr int BOX_WIDTH_MIN = 460;
|
||||||
|
constexpr int GAP_Y = 22;
|
||||||
|
constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text
|
||||||
|
constexpr int SUBTITLE_HEIGHT = 18; // scale 0.4 ≈ 16 px de text
|
||||||
|
constexpr int SEPARATOR_HEIGHT = 1;
|
||||||
|
constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight
|
||||||
|
constexpr int ITEM_GAP_Y = 6;
|
||||||
|
|
||||||
|
// Brackets als 4 cantons (substitueixen la vora completa: estètica sci-fi).
|
||||||
|
constexpr int CORNER_ARM_H = 48;
|
||||||
|
constexpr int CORNER_ARM_V = 28;
|
||||||
|
constexpr int CORNER_THICKNESS = 2;
|
||||||
|
|
||||||
|
// ---- Animacio open/close (mateixos parametres que aee_arcade) ----
|
||||||
|
constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir
|
||||||
|
constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar
|
||||||
|
constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa
|
||||||
|
constexpr float WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines
|
||||||
|
|
||||||
|
// ---- Animacio del highlight (rectangle del cursor) ----
|
||||||
|
// Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial).
|
||||||
|
constexpr float HIGHLIGHT_RATE = 18.0F;
|
||||||
|
constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada
|
||||||
|
constexpr int HIGHLIGHT_THICKNESS = 1;
|
||||||
|
constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text
|
||||||
|
constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical
|
||||||
|
constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre)
|
||||||
|
constexpr int MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic)
|
||||||
|
|
||||||
|
// ---- Colors RGBA ----
|
||||||
|
constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215};
|
||||||
|
constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon
|
||||||
|
constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255};
|
||||||
|
constexpr SDL_Color SUBTITLE_COLOR{.r = 110, .g = 170, .b = 210, .a = 220}; // cian apagat
|
||||||
|
constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180};
|
||||||
|
constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255};
|
||||||
|
constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat
|
||||||
|
constexpr SDL_Color HIGHLIGHT_OUTLINE{.r = 255, .g = 230, .b = 120, .a = 255}; // mateix groc, opac
|
||||||
|
constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid
|
||||||
|
|
||||||
|
// ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ----
|
||||||
|
constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard
|
||||||
|
constexpr float SUBTITLE_SCALE = 0.40F; // sota el titol, info decorativa (versio/hash)
|
||||||
|
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions
|
||||||
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
|
|
||||||
|
// ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ----
|
||||||
|
constexpr const char* SELECT_SOUND = "ui/menu_select.wav";
|
||||||
|
constexpr const char* ACCEPT_SOUND = "ui/menu_accept.wav";
|
||||||
|
|
||||||
|
} // namespace Defaults::ServiceMenu
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// starfield_parallax.hpp - Capa de fons del playfield: estrelles 2D amb parallax
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// 3 capes de profunditat. Cada capa té estrelles amb brillantor, mida i
|
||||||
|
// factor parallax propis. Les més properes són més brillants i grans i es
|
||||||
|
// mouen més ràpid quan el món es desplaça; les més llunyanes són tènues i
|
||||||
|
// petites i amb prou feines es mouen.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::StarfieldParallax {
|
||||||
|
|
||||||
|
namespace Far {
|
||||||
|
constexpr int COUNT = 60;
|
||||||
|
constexpr float BRIGHTNESS = 0.15F;
|
||||||
|
constexpr float PARALLAX_FACTOR = 0.15F; // multiplicador sobre world_velocity
|
||||||
|
constexpr int SIZE_PX = 1; // 1 px (punt)
|
||||||
|
} // namespace Far
|
||||||
|
|
||||||
|
namespace Mid {
|
||||||
|
constexpr int COUNT = 50;
|
||||||
|
constexpr float BRIGHTNESS = 0.30F;
|
||||||
|
constexpr float PARALLAX_FACTOR = 0.35F;
|
||||||
|
constexpr int SIZE_PX = 2; // creu de 3x3 (extensió ±1)
|
||||||
|
} // namespace Mid
|
||||||
|
|
||||||
|
namespace Near {
|
||||||
|
constexpr int COUNT = 40;
|
||||||
|
constexpr float BRIGHTNESS = 0.55F;
|
||||||
|
constexpr float PARALLAX_FACTOR = 0.70F;
|
||||||
|
constexpr int SIZE_PX = 3; // creu de 5x5 (extensió ±2)
|
||||||
|
} // namespace Near
|
||||||
|
|
||||||
|
constexpr int TOTAL_COUNT = Far::COUNT + Mid::COUNT + Near::COUNT;
|
||||||
|
|
||||||
|
} // namespace Defaults::StarfieldParallax
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// title.hpp - Animacions de naves i layout de l'escena de títol
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#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: (radius 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.5F; // 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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Coreografia de la seqüència d'entrada al state MAIN.
|
||||||
|
// Tots els elements (logo, footer, naus, press start) entren ordenadament
|
||||||
|
// segons aquests thresholds. Vegeu title_scene.cpp/updateMainState.
|
||||||
|
//
|
||||||
|
// Per al logo i el footer, l'efecte simula un moviment 3D des de l'usuari
|
||||||
|
// cap al VP: el text arrenca gran i a la posició projectada extrema (com
|
||||||
|
// si estigués prop de la càmera, fora de pantalla) i acaba a la seva
|
||||||
|
// posició final amb escala normal (com si hagués aterrat al VP). Pivot:
|
||||||
|
// centre de pantalla (= projecció del VP 3D).
|
||||||
|
namespace Sequence {
|
||||||
|
// Factor d'escala inicial. >1 = sprite gran a l'inici (prop de l'usuari).
|
||||||
|
// La posició inicial es deriva: pivot=centre, delta multiplicat per aquest factor.
|
||||||
|
constexpr float LOGO_INTRO_SCALE_START = 2.5F;
|
||||||
|
constexpr float FOOTER_INTRO_SCALE_START = 2.5F;
|
||||||
|
|
||||||
|
// Durades de les animacions d'entrada (segons).
|
||||||
|
constexpr float LOGO_ENTRY_DURATION = 1.2F;
|
||||||
|
constexpr float JAILGAMES_ENTRY_DURATION = 0.7F;
|
||||||
|
constexpr float COPYRIGHT_ENTRY_DURATION = 0.7F;
|
||||||
|
|
||||||
|
// Stagger "pam-pam" entre l'arrencada de JAILGAMES i la de COPYRIGHT.
|
||||||
|
constexpr float COPYRIGHT_STAGGER = 0.18F;
|
||||||
|
|
||||||
|
// Delays entre etapes.
|
||||||
|
constexpr float SHIPS_DELAY_AFTER_FOOTER = 0.20F;
|
||||||
|
constexpr float PRESS_START_DELAY_AFTER_SHIPS = 0.40F;
|
||||||
|
} // namespace Sequence
|
||||||
|
|
||||||
|
// Paleta neon de l'escena de títol (cian + magenta synthwave).
|
||||||
|
// alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori
|
||||||
|
// el color global de l'oscil·lador per a aquesta crida.
|
||||||
|
namespace Colors {
|
||||||
|
constexpr SDL_Color LOGO_MAIN = {.r = 80, .g = 240, .b = 255, .a = 255}; // Cian elèctric
|
||||||
|
constexpr SDL_Color LOGO_SHADOW = {.r = 255, .g = 60, .b = 180, .a = 255}; // Magenta neon (offset)
|
||||||
|
constexpr SDL_Color SHIP_P1 = {.r = 255, .g = 100, .b = 200, .a = 255}; // Rosa hot
|
||||||
|
constexpr SDL_Color SHIP_P2 = {.r = 160, .g = 120, .b = 255, .a = 255}; // Violeta elèctric
|
||||||
|
constexpr SDL_Color STARFIELD = {.r = 200, .g = 220, .b = 255, .a = 255}; // Blanc-blau gel
|
||||||
|
constexpr SDL_Color PRESS_START = {.r = 255, .g = 200, .b = 70, .a = 255}; // Ambre neon
|
||||||
|
constexpr SDL_Color JAILGAMES_LOGO = {.r = 120, .g = 220, .b = 200, .a = 255}; // Teal suau
|
||||||
|
constexpr SDL_Color COPYRIGHT = {.r = 140, .g = 180, .b = 200, .a = 255}; // Gris-cian apagat
|
||||||
|
} // namespace Colors
|
||||||
|
|
||||||
|
} // 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 player.yaml::physics.max_velocity (180 px/s)
|
||||||
|
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,56 @@
|
|||||||
|
// entity_loader.cpp - Implementació del carregador d'entitats YAML
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/entities/entity_loader.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|
||||||
|
namespace Entities {
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> EntityLoader::cache;
|
||||||
|
|
||||||
|
auto EntityLoader::load(const std::string& name) -> std::shared_ptr<fkyaml::node> {
|
||||||
|
// Cache hit
|
||||||
|
auto it = cache.find(name);
|
||||||
|
if (it != cache.end()) {
|
||||||
|
std::cout << "[EntityLoader] Cache hit: " << name << '\n';
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string PATH = "entities/" + name + "/" + name + ".yaml";
|
||||||
|
|
||||||
|
std::vector<uint8_t> data = Resource::Helper::loadFile(PATH);
|
||||||
|
if (data.empty()) {
|
||||||
|
std::cerr << "[EntityLoader] Error: no s'ha pogut load " << PATH << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::string yaml_content(data.begin(), data.end());
|
||||||
|
std::stringstream stream(yaml_content);
|
||||||
|
auto node = std::make_shared<fkyaml::node>(fkyaml::node::deserialize(stream));
|
||||||
|
|
||||||
|
std::cout << "[EntityLoader] Carregat: " << PATH << '\n';
|
||||||
|
cache[name] = node;
|
||||||
|
return node;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[EntityLoader] Excepció parsejant " << PATH << ": " << e.what() << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityLoader::clearCache() {
|
||||||
|
std::cout << "[EntityLoader] Netejant caché (" << cache.size() << " entitats)" << '\n';
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto EntityLoader::getCacheSize() -> size_t { return cache.size(); }
|
||||||
|
|
||||||
|
} // namespace Entities
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Cada entitat viu a `data/entities/<name>/<name>.yaml` (mateix patró que el
|
||||||
|
// projecte germà aee_arcade). Aquest loader resol el path, llegeix del
|
||||||
|
// resource pack via Resource::Helper, parseja amb fkyaml i cacheja el node
|
||||||
|
// per evitar relectures. Retorna nullptr en cas d'error (el caller decideix
|
||||||
|
// si abortar).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
namespace Entities {
|
||||||
|
|
||||||
|
class EntityLoader {
|
||||||
|
public:
|
||||||
|
EntityLoader() = delete; // tot estàtic
|
||||||
|
|
||||||
|
// Carrega el descriptor d'una entitat per nom (ex. "player" →
|
||||||
|
// "entities/player/player.yaml"). Retorna nullptr si no es pot
|
||||||
|
// carregar o parsejar. Cachejat per nom.
|
||||||
|
static auto load(const std::string& name) -> std::shared_ptr<fkyaml::node>;
|
||||||
|
|
||||||
|
// Buidar caché (útil per debug/recàrrega).
|
||||||
|
static void clearCache();
|
||||||
|
|
||||||
|
[[nodiscard]] static auto getCacheSize() -> size_t;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // 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& zone = Defaults::Zones::PLAYAREA;
|
||||||
|
const std::array<float, SIDE_COUNT> DISTANCES = {
|
||||||
|
/* TOP */ std::abs(contact_point.y - zone.y),
|
||||||
|
/* RIGHT */ std::abs((zone.x + zone.w) - contact_point.x),
|
||||||
|
/* BOTTOM */ std::abs((zone.y + zone.h) - contact_point.y),
|
||||||
|
/* LEFT */ std::abs(contact_point.x - zone.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& zone = Defaults::Zones::PLAYAREA;
|
||||||
|
const int X1 = static_cast<int>(zone.x);
|
||||||
|
const int Y1 = static_cast<int>(zone.y);
|
||||||
|
const int X2 = static_cast<int>(zone.x + zone.w);
|
||||||
|
const int Y2 = static_cast<int>(zone.y + zone.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,344 @@
|
|||||||
|
// playfield.cpp - Implementació del fons del playfield
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/graphics/playfield.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto randUniform(float min_v, float max_v) -> float {
|
||||||
|
const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||||
|
return min_v + (NORM * (max_v - min_v));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desplaçament radial acumulat al punt (px, py) sumant totes les ripples
|
||||||
|
// que el toquen. Retorna {dx, dy} a sumar a la posició original.
|
||||||
|
auto computeRippleDisplacement(float px, float py, const Playfield::Ripple* const* hits, int n_hits) -> Vec2 {
|
||||||
|
float dx_total = 0.0F;
|
||||||
|
float dy_total = 0.0F;
|
||||||
|
for (int i = 0; i < n_hits; i++) {
|
||||||
|
const auto& r = *hits[i];
|
||||||
|
const float RADIUS = r.age_s * r.speed_px_s;
|
||||||
|
const float THICKNESS = r.thickness_px;
|
||||||
|
const float DX = px - r.center.x;
|
||||||
|
const float DY = py - r.center.y;
|
||||||
|
const float D = std::sqrt((DX * DX) + (DY * DY));
|
||||||
|
if (D < 0.001F) {
|
||||||
|
continue; // centre exacte: no hi ha direcció radial
|
||||||
|
}
|
||||||
|
const float PHASE = (D - RADIUS) / THICKNESS;
|
||||||
|
if (std::fabs(PHASE) >= 1.0F) {
|
||||||
|
continue; // fora de l'anell d'aquesta ripple
|
||||||
|
}
|
||||||
|
const float ENVELOPE = std::cos(PHASE * Defaults::Math::PI * 0.5F);
|
||||||
|
const float AMP_EFF = r.amplitude_px * (1.0F - (r.age_s / r.lifetime_s));
|
||||||
|
const float UX = DX / D;
|
||||||
|
const float UY = DY / D;
|
||||||
|
dx_total += UX * AMP_EFF * ENVELOPE;
|
||||||
|
dy_total += UY * AMP_EFF * ENVELOPE;
|
||||||
|
}
|
||||||
|
return Vec2{.x = dx_total, .y = dy_total};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Playfield::Playfield(Rendering::Renderer* renderer)
|
||||||
|
: renderer_(renderer) {
|
||||||
|
buildLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::update(float delta_time) {
|
||||||
|
elapsed_s_ += delta_time;
|
||||||
|
for (auto& ripple : ripples_) {
|
||||||
|
if (!ripple.active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ripple.age_s += delta_time;
|
||||||
|
if (ripple.age_s >= ripple.lifetime_s) {
|
||||||
|
ripple.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Playfield::findFreeRipple() -> Ripple* {
|
||||||
|
Ripple* oldest = nullptr;
|
||||||
|
for (auto& ripple : ripples_) {
|
||||||
|
if (!ripple.active) {
|
||||||
|
return &ripple;
|
||||||
|
}
|
||||||
|
if (oldest == nullptr || ripple.age_s > oldest->age_s) {
|
||||||
|
oldest = &ripple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oldest; // pool ple: substituïm la més vella
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::spawnBig(Vec2 pos) {
|
||||||
|
Ripple* r = findFreeRipple();
|
||||||
|
if (r == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r->center = pos;
|
||||||
|
r->age_s = 0.0F;
|
||||||
|
r->lifetime_s = Defaults::Playfield::Ripple::BIG_LIFETIME_S;
|
||||||
|
r->speed_px_s = Defaults::Playfield::Ripple::BIG_SPEED_PX_S;
|
||||||
|
r->amplitude_px = Defaults::Playfield::Ripple::BIG_AMPLITUDE_PX;
|
||||||
|
r->thickness_px = Defaults::Playfield::Ripple::BIG_THICKNESS_PX;
|
||||||
|
r->active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::spawnSmall(Vec2 pos) {
|
||||||
|
Ripple* r = findFreeRipple();
|
||||||
|
if (r == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r->center = pos;
|
||||||
|
r->age_s = 0.0F;
|
||||||
|
r->lifetime_s = Defaults::Playfield::Ripple::SMALL_LIFETIME_S;
|
||||||
|
r->speed_px_s = Defaults::Playfield::Ripple::SMALL_SPEED_PX_S;
|
||||||
|
r->amplitude_px = Defaults::Playfield::Ripple::SMALL_AMPLITUDE_PX;
|
||||||
|
r->thickness_px = Defaults::Playfield::Ripple::SMALL_THICKNESS_PX;
|
||||||
|
r->active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::notifyExplosion(Vec2 pos) {
|
||||||
|
spawnBig(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time) {
|
||||||
|
if (player_id >= ship_ripple_cooldown_.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (speed_px_s < Defaults::Playfield::Ripple::SHIP_SPEED_THRESHOLD_PX_S) {
|
||||||
|
ship_ripple_cooldown_[player_id] = 0.0F;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ship_ripple_cooldown_[player_id] -= delta_time;
|
||||||
|
if (ship_ripple_cooldown_[player_id] > 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
spawnSmall(pos);
|
||||||
|
const float JITTER = randUniform(
|
||||||
|
-Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S,
|
||||||
|
Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S);
|
||||||
|
ship_ripple_cooldown_[player_id] =
|
||||||
|
Defaults::Playfield::Ripple::SHIP_COOLDOWN_S + JITTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::buildLines() {
|
||||||
|
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||||
|
const float CELL_W = zone.w / static_cast<float>(Defaults::Playfield::COLUMNS);
|
||||||
|
const float CELL_H = zone.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 = zone.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 = zone.y},
|
||||||
|
.end = {.x = X, .y = zone.y + zone.h},
|
||||||
|
.brightness = BRIGHTNESS,
|
||||||
|
.spawn_time_s = 0.0F,
|
||||||
|
.is_vertical = true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
|
||||||
|
for (int j = 1; j < SUB_HORIZ; j++) {
|
||||||
|
const float Y = zone.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 = zone.x, .y = Y},
|
||||||
|
.end = {.x = zone.x + zone.w, .y = Y},
|
||||||
|
.brightness = BRIGHTNESS,
|
||||||
|
.spawn_time_s = 0.0F,
|
||||||
|
.is_vertical = false});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
// El spawn_time_s s'assigna per índex espacial perquè la diagonal de
|
||||||
|
// l'ona de creixement avanci uniformement. L'ordre dins lines_, en
|
||||||
|
// canvi, ha de garantir que el grid principal (més brillant) es
|
||||||
|
// dibuixi DESPRÉS del subgrid: així a les interseccions guanya el
|
||||||
|
// principal i no queden tallades pel subgrid.
|
||||||
|
for (int i = 0; i < NUM_V; i++) {
|
||||||
|
verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < NUM_H; i++) {
|
||||||
|
horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passada 1: subgrid (verticals + horitzontals).
|
||||||
|
for (const auto& v : verticals) {
|
||||||
|
if (v.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
|
||||||
|
lines_.push_back(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& h : horizontals) {
|
||||||
|
if (h.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
|
||||||
|
lines_.push_back(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Passada 2: grid principal (verticals + horitzontals).
|
||||||
|
for (const auto& v : verticals) {
|
||||||
|
if (v.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
|
||||||
|
lines_.push_back(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& h : horizontals) {
|
||||||
|
if (h.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
|
||||||
|
lines_.push_back(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Recollir ripples actives (punters per accés ràpid al hot loop).
|
||||||
|
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> active{};
|
||||||
|
int n_active = 0;
|
||||||
|
for (const auto& ripple : ripples_) {
|
||||||
|
if (ripple.active) {
|
||||||
|
active[n_active++] = &ripple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& line : lines_) {
|
||||||
|
drawLine(line, active.data(), n_active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::drawLine(const Line& line, const Ripple* const* active, int n_active) const {
|
||||||
|
const float RAW_P = computeLineProgress(line);
|
||||||
|
if (RAW_P <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float P = easeOutCubic(RAW_P);
|
||||||
|
|
||||||
|
const float START_X = line.start.x;
|
||||||
|
const float START_Y = line.start.y;
|
||||||
|
const float DX = line.end.x - line.start.x;
|
||||||
|
const float DY = line.end.y - line.start.y;
|
||||||
|
const float END_X = START_X + (DX * P);
|
||||||
|
const float END_Y = START_Y + (DY * P);
|
||||||
|
|
||||||
|
// AABB de la porció visible de la línia + filtre de ripples.
|
||||||
|
const float LINE_MIN_X = std::min(START_X, END_X);
|
||||||
|
const float LINE_MAX_X = std::max(START_X, END_X);
|
||||||
|
const float LINE_MIN_Y = std::min(START_Y, END_Y);
|
||||||
|
const float LINE_MAX_Y = std::max(START_Y, END_Y);
|
||||||
|
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> hits{};
|
||||||
|
int n_hits = 0;
|
||||||
|
for (int i = 0; i < n_active; i++) {
|
||||||
|
const auto& r = *active[i];
|
||||||
|
const float R_MAX = (r.age_s * r.speed_px_s) + r.thickness_px;
|
||||||
|
if ((r.center.x + R_MAX) < LINE_MIN_X || (r.center.x - R_MAX) > LINE_MAX_X ||
|
||||||
|
(r.center.y + R_MAX) < LINE_MIN_Y || (r.center.y - R_MAX) > LINE_MAX_Y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
hits[n_hits++] = &r;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n_hits == 0) {
|
||||||
|
// Camí ràpid: una sola crida com abans.
|
||||||
|
Rendering::linea(
|
||||||
|
renderer_,
|
||||||
|
static_cast<int>(START_X),
|
||||||
|
static_cast<int>(START_Y),
|
||||||
|
static_cast<int>(END_X),
|
||||||
|
static_cast<int>(END_Y),
|
||||||
|
line.brightness,
|
||||||
|
0.0F,
|
||||||
|
Defaults::Playfield::GRID_COLOR);
|
||||||
|
// Cap brillant mentre creix.
|
||||||
|
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));
|
||||||
|
Rendering::linea(
|
||||||
|
renderer_,
|
||||||
|
static_cast<int>(START_X + (DX * HEAD_T)),
|
||||||
|
static_cast<int>(START_Y + (DY * HEAD_T)),
|
||||||
|
static_cast<int>(END_X),
|
||||||
|
static_cast<int>(END_Y),
|
||||||
|
Defaults::Playfield::HEAD_BRIGHTNESS,
|
||||||
|
0.0F,
|
||||||
|
Defaults::Playfield::GRID_COLOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camí deformat: subdividir en N segments i desplaçar cada vèrtex.
|
||||||
|
const bool IS_MAIN = line.brightness >= Defaults::Playfield::GRID_BRIGHTNESS;
|
||||||
|
const int N = IS_MAIN
|
||||||
|
? Defaults::Playfield::Ripple::MAIN_SEGMENTS
|
||||||
|
: Defaults::Playfield::Ripple::SUB_SEGMENTS;
|
||||||
|
const Vec2 D0 = computeRippleDisplacement(START_X, START_Y, hits.data(), n_hits);
|
||||||
|
float prev_x = START_X + D0.x;
|
||||||
|
float prev_y = START_Y + D0.y;
|
||||||
|
for (int i = 1; i <= N; i++) {
|
||||||
|
const float T = static_cast<float>(i) / static_cast<float>(N);
|
||||||
|
const float X = START_X + (DX * P * T);
|
||||||
|
const float Y = START_Y + (DY * P * T);
|
||||||
|
const Vec2 D = computeRippleDisplacement(X, Y, hits.data(), n_hits);
|
||||||
|
const float NX = X + D.x;
|
||||||
|
const float NY = Y + D.y;
|
||||||
|
Rendering::linea(
|
||||||
|
renderer_,
|
||||||
|
static_cast<int>(prev_x),
|
||||||
|
static_cast<int>(prev_y),
|
||||||
|
static_cast<int>(NX),
|
||||||
|
static_cast<int>(NY),
|
||||||
|
line.brightness,
|
||||||
|
0.0F,
|
||||||
|
Defaults::Playfield::GRID_COLOR);
|
||||||
|
prev_x = NX;
|
||||||
|
prev_y = NY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Reaccions disponibles:
|
||||||
|
// - Ripples: deformacions circulars (ones d'aigua) que travessen la graella.
|
||||||
|
// Disparades per explosions (grans) i pas de la nau (petites, cadència estil
|
||||||
|
// trail). Cada vèrtex d'una línia afectada es desplaça radialment cap a fora
|
||||||
|
// amb una envoltant en cos(·) que decau a les vores de l'anell i amb el temps.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#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ó + ripples).
|
||||||
|
void update(float delta_time);
|
||||||
|
|
||||||
|
// Pinta la graella. La porció dibuixada de cada línia depèn del timer intern,
|
||||||
|
// i s'aplica deformació radial per cada ripple activa que afecti la línia.
|
||||||
|
void draw() const;
|
||||||
|
|
||||||
|
// Notifica que una nau ha passat per (pos) a (speed_px_s). Genera ones
|
||||||
|
// petites darrere la nau a cadència regular amb jitter (estil TrailManager).
|
||||||
|
void notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time);
|
||||||
|
|
||||||
|
// Notifica una explosió a (pos): genera una ripple gran centrada al punt.
|
||||||
|
void notifyExplosion(Vec2 pos);
|
||||||
|
|
||||||
|
// Pública per accés des d'helpers a l'anonymous namespace del .cpp.
|
||||||
|
struct Ripple {
|
||||||
|
Vec2 center{};
|
||||||
|
float age_s{0.0F};
|
||||||
|
float lifetime_s{0.0F};
|
||||||
|
float speed_px_s{0.0F};
|
||||||
|
float amplitude_px{0.0F};
|
||||||
|
float thickness_px{0.0F};
|
||||||
|
bool active{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
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ó
|
||||||
|
};
|
||||||
|
|
||||||
|
void buildLines();
|
||||||
|
void drawLine(const Line& line, const Ripple* const* active, int n_active) const;
|
||||||
|
[[nodiscard]] auto computeLineProgress(const Line& line) const -> float;
|
||||||
|
void spawnBig(Vec2 pos);
|
||||||
|
void spawnSmall(Vec2 pos);
|
||||||
|
auto findFreeRipple() -> Ripple*;
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
std::vector<Line> lines_;
|
||||||
|
std::array<Ripple, Defaults::Playfield::Ripple::POOL_SIZE> ripples_{};
|
||||||
|
std::array<float, 2> ship_ripple_cooldown_{};
|
||||||
|
float elapsed_s_{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Graphics
|
||||||
+139
-124
@@ -1,159 +1,174 @@
|
|||||||
// 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"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
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") {
|
|
||||||
carregar(filepath);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Shape::carregar(const std::string& filepath) {
|
nom_("unnamed") {
|
||||||
// Llegir fitxer
|
load(filepath);
|
||||||
std::ifstream file(filepath);
|
|
||||||
if (!file.is_open()) {
|
|
||||||
std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Llegir tot el contingut
|
auto Shape::load(const std::string& filepath) -> bool {
|
||||||
std::stringstream buffer;
|
// Llegir file
|
||||||
buffer << file.rdbuf();
|
std::ifstream file(filepath);
|
||||||
std::string contingut = buffer.str();
|
if (!file.is_open()) {
|
||||||
file.close();
|
std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n';
|
||||||
|
return false;
|
||||||
// Parsejar
|
|
||||||
return parsejar_fitxer(contingut);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Shape::parsejar_fitxer(const std::string& contingut) {
|
|
||||||
std::istringstream iss(contingut);
|
|
||||||
std::string line;
|
|
||||||
|
|
||||||
while (std::getline(iss, line)) {
|
|
||||||
// Trim whitespace
|
|
||||||
line = trim(line);
|
|
||||||
|
|
||||||
// Skip comments and blanks
|
|
||||||
if (line.empty() || line[0] == '#') {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse command
|
// Llegir todo el contingut
|
||||||
if (starts_with(line, "name:")) {
|
std::stringstream buffer;
|
||||||
nom_ = trim(extract_value(line));
|
buffer << file.rdbuf();
|
||||||
} else if (starts_with(line, "scale:")) {
|
std::string contingut = buffer.str();
|
||||||
try {
|
file.close();
|
||||||
escala_defecte_ = std::stof(extract_value(line));
|
|
||||||
} catch (...) {
|
// Parsejar
|
||||||
std::cerr << "[Shape] Warning: escala invàlida, usant 1.0" << '\n';
|
return parseFile(contingut);
|
||||||
escala_defecte_ = 1.0F;
|
}
|
||||||
|
|
||||||
|
auto Shape::parseFile(const std::string& contingut) -> bool {
|
||||||
|
std::istringstream iss(contingut);
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
while (std::getline(iss, line)) {
|
||||||
|
// Trim whitespace
|
||||||
|
line = trim(line);
|
||||||
|
|
||||||
|
// Skip comments and blanks
|
||||||
|
if (line.empty() || line[0] == '#') {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} else if (starts_with(line, "center:")) {
|
|
||||||
parse_center(extract_value(line));
|
// Parse command
|
||||||
} else if (starts_with(line, "polyline:")) {
|
if (startsWith(line, "name:")) {
|
||||||
auto points = parse_points(extract_value(line));
|
nom_ = trim(extractValue(line));
|
||||||
if (points.size() >= 2) {
|
} else if (startsWith(line, "scale:")) {
|
||||||
primitives_.push_back({PrimitiveType::POLYLINE, points});
|
try {
|
||||||
} else {
|
escala_defecte_ = std::stof(extractValue(line));
|
||||||
std::cerr << "[Shape] Warning: polyline amb menys de 2 punts ignorada"
|
} catch (...) {
|
||||||
<< '\n';
|
std::cerr << "[Shape] Warning: scale invàlida, usant 1.0" << '\n';
|
||||||
|
escala_defecte_ = 1.0F;
|
||||||
|
}
|
||||||
|
} else if (startsWith(line, "center:")) {
|
||||||
|
parseCenter(extractValue(line));
|
||||||
|
} else if (startsWith(line, "polyline:")) {
|
||||||
|
auto points = parsePoints(extractValue(line));
|
||||||
|
if (points.size() >= 2) {
|
||||||
|
primitives_.push_back({PrimitiveType::POLYLINE, points});
|
||||||
|
} else {
|
||||||
|
std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
} else if (startsWith(line, "line:")) {
|
||||||
|
auto points = parsePoints(extractValue(line));
|
||||||
|
if (points.size() == 2) {
|
||||||
|
primitives_.push_back({PrimitiveType::LINE, points});
|
||||||
|
} else {
|
||||||
|
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (starts_with(line, "line:")) {
|
// Comandes desconegudes ignorades silenciosament
|
||||||
auto points = parse_points(extract_value(line));
|
}
|
||||||
if (points.size() == 2) {
|
|
||||||
primitives_.push_back({PrimitiveType::LINE, points});
|
if (primitives_.empty()) {
|
||||||
} else {
|
std::cerr << "[Shape] Error: sin primitiva carregada" << '\n';
|
||||||
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 punts"
|
return false;
|
||||||
<< '\n';
|
}
|
||||||
|
|
||||||
|
bounding_radius_ = computeBoundingRadius(primitives_, center_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Shape::computeBoundingRadius(const std::vector<ShapePrimitive>& primitives,
|
||||||
|
const Vec2& center) -> float {
|
||||||
|
float max_dist_sq = 0.0F;
|
||||||
|
for (const auto& prim : primitives) {
|
||||||
|
for (const auto& p : prim.points) {
|
||||||
|
const float DX = p.x - center.x;
|
||||||
|
const float DY = p.y - center.y;
|
||||||
|
max_dist_sq = std::max(max_dist_sq, (DX * DX) + (DY * DY));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Comandes desconegudes ignorades silenciosament
|
return std::sqrt(max_dist_sq);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (primitives_.empty()) {
|
// Helper: trim whitespace
|
||||||
std::cerr << "[Shape] Error: cap primitiva carregada" << '\n';
|
auto Shape::trim(const std::string& str) -> std::string {
|
||||||
return false;
|
const char* whitespace = " \t\n\r";
|
||||||
}
|
size_t start = str.find_first_not_of(whitespace);
|
||||||
|
if (start == std::string::npos) {
|
||||||
return true;
|
return "";
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: trim whitespace
|
|
||||||
std::string Shape::trim(const std::string& str) const {
|
|
||||||
const char* whitespace = " \t\n\r";
|
|
||||||
size_t start = str.find_first_not_of(whitespace);
|
|
||||||
if (start == std::string::npos) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t end = str.find_last_not_of(whitespace);
|
|
||||||
return str.substr(start, end - start + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: starts_with
|
|
||||||
bool Shape::starts_with(const std::string& str,
|
|
||||||
const std::string& prefix) const {
|
|
||||||
if (str.length() < prefix.length()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return str.starts_with(prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: extract value after ':'
|
|
||||||
std::string Shape::extract_value(const std::string& line) const {
|
|
||||||
size_t colon = line.find(':');
|
|
||||||
if (colon == std::string::npos) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return line.substr(colon + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: parse center "x, y"
|
|
||||||
void Shape::parse_center(const std::string& value) {
|
|
||||||
std::string val = trim(value);
|
|
||||||
size_t comma = val.find(',');
|
|
||||||
if (comma != std::string::npos) {
|
|
||||||
try {
|
|
||||||
centre_.x = std::stof(trim(val.substr(0, comma)));
|
|
||||||
centre_.y = std::stof(trim(val.substr(comma + 1)));
|
|
||||||
} catch (...) {
|
|
||||||
std::cerr << "[Shape] Warning: centre invàlid, usant (0,0)" << '\n';
|
|
||||||
centre_ = {.x = 0.0F, .y = 0.0F};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t end = str.find_last_not_of(whitespace);
|
||||||
|
return str.substr(start, end - start + 1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: parse points "x1,y1 x2,y2 x3,y3"
|
// Helper: startsWith
|
||||||
std::vector<Punt> Shape::parse_points(const std::string& str) const {
|
auto Shape::startsWith(const std::string& str,
|
||||||
std::vector<Punt> points;
|
const std::string& prefix) -> bool {
|
||||||
std::istringstream iss(trim(str));
|
if (str.length() < prefix.length()) {
|
||||||
std::string pair;
|
return false;
|
||||||
|
}
|
||||||
|
return str.starts_with(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
while (iss >> pair) { // Whitespace-separated
|
// Helper: extract value after ':'
|
||||||
size_t comma = pair.find(',');
|
auto Shape::extractValue(const std::string& line) -> std::string {
|
||||||
|
size_t colon = line.find(':');
|
||||||
|
if (colon == std::string::npos) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return line.substr(colon + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: parse center "x, y"
|
||||||
|
void Shape::parseCenter(const std::string& value) {
|
||||||
|
std::string val = trim(value);
|
||||||
|
size_t comma = val.find(',');
|
||||||
if (comma != std::string::npos) {
|
if (comma != std::string::npos) {
|
||||||
try {
|
try {
|
||||||
float x = std::stof(pair.substr(0, comma));
|
center_.x = std::stof(trim(val.substr(0, comma)));
|
||||||
float y = std::stof(pair.substr(comma + 1));
|
center_.y = std::stof(trim(val.substr(comma + 1)));
|
||||||
points.push_back({x, y});
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
std::cerr << "[Shape] Warning: punt invàlid ignorat: " << pair
|
std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n';
|
||||||
<< '\n';
|
center_ = {.x = 0.0F, .y = 0.0F};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return points;
|
// Helper: parse points "x1,y1 x2,y2 x3,y3"
|
||||||
}
|
auto Shape::parsePoints(const std::string& str) -> std::vector<Vec2> {
|
||||||
|
std::vector<Vec2> points;
|
||||||
|
std::istringstream iss(trim(str));
|
||||||
|
std::string pair;
|
||||||
|
|
||||||
|
while (iss >> pair) { // Whitespace-separated
|
||||||
|
size_t comma = pair.find(',');
|
||||||
|
if (comma != std::string::npos) {
|
||||||
|
try {
|
||||||
|
float x = std::stof(pair.substr(0, comma));
|
||||||
|
float y = std::stof(pair.substr(comma + 1));
|
||||||
|
points.push_back({x, y});
|
||||||
|
} catch (...) {
|
||||||
|
std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -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,64 @@
|
|||||||
|
|
||||||
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(); }
|
// Distància màx. del center_ al vèrtex més llunyà; ús: dimensionar
|
||||||
|
// efectes proporcionals a la mida de la shape (halos, glow).
|
||||||
|
[[nodiscard]] auto getBoundingRadius() const -> float { return bounding_radius_; }
|
||||||
|
[[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.
|
||||||
|
float bounding_radius_{0.0F}; // Distància màx. del center_ al vèrtex més llunyà.
|
||||||
|
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>;
|
||||||
|
[[nodiscard]] static auto computeBoundingRadius(
|
||||||
|
const std::vector<ShapePrimitive>& primitives,
|
||||||
|
const Vec2& center) -> float;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user