Compare commits
361 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 817c8fc8a0 | |||
| 3fe8fa9b32 | |||
| 65f710bf7a | |||
| 72302554ae | |||
| 03530d0439 | |||
| 705d32e919 | |||
| e420db2896 | |||
| 785700f819 | |||
| 07863577bc | |||
| 8a341be027 | |||
| 93fb914e54 | |||
| 8d659c44e5 | |||
| 5407f66c9e | |||
| dd91b07a14 | |||
| fc8233ef57 | |||
| ef2c13b011 | |||
| 69e337393a | |||
| 56c3f978d3 | |||
| cb958f33ba | |||
| e3d12e6e27 | |||
| 47e9d85708 | |||
| 82027e4975 | |||
| ab06cb32c9 | |||
| 9e7061d8b7 | |||
| b4b95c883f | |||
| a46b93c917 | |||
| 8d18c50aaa | |||
| b412435862 | |||
| 5b90a9a767 | |||
| 5ba562178b | |||
| 55b37ba594 | |||
| 20825c8138 | |||
| 9235e684e8 | |||
| 0350063fb7 | |||
| 56065995fd | |||
| 17e9206d26 | |||
| 462e91d967 | |||
| 3bc87ad652 | |||
| a7233e13df | |||
| 0abd661905 | |||
| a808226481 | |||
| 317e2a3fd9 | |||
| e4f8f586d6 | |||
| 6f29731679 | |||
| d7a9bd4ab2 | |||
| ab5489a080 | |||
| f4567a2e82 | |||
| 4b298ffc1c | |||
| 0f986cbf80 | |||
| 582bd0ee30 | |||
| 2e4030c2f2 | |||
| a9b662840b | |||
| 30bbb37bff | |||
| 2f6d6c405f | |||
| 068f42782b | |||
| 472c543c7b | |||
| 4e67a67ace | |||
| 1e63d3ae9d | |||
| b363efd1f0 | |||
| 0abbaa09f8 | |||
| 455b7a6893 | |||
| 92f76d091d | |||
| c1956e0028 | |||
| 491992a4d7 | |||
| e5b727216c | |||
| f03e337b9a | |||
| 99e99e7e08 | |||
| b93761eb1e | |||
| 4f5421191d | |||
| 71ed9dc24f | |||
| 1a0cc504c4 | |||
| 86775d4642 | |||
| b936f410ce | |||
| ddcd2076a1 | |||
| 9345facaed | |||
| 885caa6bc3 | |||
| a77bbe4420 | |||
| 61a4886e62 | |||
| 164f58c883 | |||
| fbfacb825b | |||
| 5e4d2cf993 | |||
| 97d3749269 | |||
| 0dcecf9a3c | |||
| c75e6406cd | |||
| 0254b44369 | |||
| ff11567471 | |||
| 06e383fe2c | |||
| dc5b31087a | |||
| 9e745dc3fc | |||
| 14b10c663e | |||
| f64c72f9a6 | |||
| 610eaf257e | |||
| b511740d93 | |||
| b0643b6f62 | |||
| 7e8d79222c | |||
| 14295ce859 | |||
| 5ad433e63a | |||
| 61e40e88f4 | |||
| 410955de3c | |||
| 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,14 @@ Checks:
|
|||||||
- -bugprone-easily-swappable-parameters
|
- -bugprone-easily-swappable-parameters
|
||||||
- -bugprone-narrowing-conversions
|
- -bugprone-narrowing-conversions
|
||||||
- -modernize-avoid-c-arrays
|
- -modernize-avoid-c-arrays
|
||||||
|
# No forçar reemplaç de bucles "normals" per std::any_of/std::all_of.
|
||||||
|
# Equivalent a `--suppress=useStlAlgorithm` que ja tenim a cppcheck.
|
||||||
|
- -readability-use-anyofallof
|
||||||
|
# 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/
|
||||||
|
|||||||
+59
-6
@@ -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.1 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)
|
||||||
@@ -71,7 +69,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
|||||||
if(EXTERNAL_SOURCES)
|
if(EXTERNAL_SOURCES)
|
||||||
set_source_files_properties(
|
set_source_files_properties(
|
||||||
${EXTERNAL_SOURCES}
|
${EXTERNAL_SOURCES}
|
||||||
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations"
|
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations;-Wno-tautological-compare"
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -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,838 @@
|
|||||||
|
# Arquitectura de Orni Attack
|
||||||
|
|
||||||
|
> Documento de orientación para alguien que llega nuevo al proyecto. Cada
|
||||||
|
> afirmación está anclada a código real (fichero/clase/función con su ruta).
|
||||||
|
> Cuando algo no se ha podido verificar o no existe, se indica explícitamente.
|
||||||
|
> El objetivo no es vender una arquitectura ideal, sino describir lo que **este**
|
||||||
|
> proyecto hace, incluso donde es poco convencional.
|
||||||
|
|
||||||
|
## Índice
|
||||||
|
|
||||||
|
1. [Visión general](#1-visión-general)
|
||||||
|
2. [Punto de entrada y el Director](#2-punto-de-entrada-y-el-director)
|
||||||
|
3. [Bucle principal](#3-bucle-principal)
|
||||||
|
4. [Sistema de escenas](#4-sistema-de-escenas)
|
||||||
|
5. [Renderizado: de la lógica al píxel](#5-renderizado-de-la-lógica-al-píxel)
|
||||||
|
6. [Entrada](#6-entrada)
|
||||||
|
7. [Audio](#7-audio)
|
||||||
|
8. [Recursos](#8-recursos)
|
||||||
|
9. [Comunicación entre módulos](#9-comunicación-entre-módulos)
|
||||||
|
10. [Lógica del juego](#10-lógica-del-juego)
|
||||||
|
11. [IA del modo demo (attract)](#11-ia-del-modo-demo-attract)
|
||||||
|
12. [Efectos visuales](#12-efectos-visuales)
|
||||||
|
13. [Configuración, constantes y convenciones](#13-configuración-constantes-y-convenciones)
|
||||||
|
14. [Guía de navegación](#14-guía-de-navegación)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visión general
|
||||||
|
|
||||||
|
Orni Attack es un arcade vectorial (estética CRT de líneas con bloom) construido
|
||||||
|
sobre **SDL3**, usando la **GPU API de SDL3** (`SDL_gpu`) para el render — **no**
|
||||||
|
`SDL_Renderer`. El código está partido en dos grandes mundos:
|
||||||
|
|
||||||
|
- **`source/core/`** — el "motor": ventana, GPU, audio, input, recursos, i18n,
|
||||||
|
overlays de sistema. No conoce nada del juego concreto. Por ejemplo,
|
||||||
|
[audio.hpp](source/core/audio/audio.hpp) recibe un struct de configuración y no
|
||||||
|
lee YAML, e [input.hpp](source/core/input/input.hpp) no incluye nada de `game/`.
|
||||||
|
- **`source/game/`** — la lógica concreta de Orni Attack: escenas, entidades
|
||||||
|
(naves, enemigos, balas), sistemas (colisiones, IA), stages/oleadas y efectos.
|
||||||
|
|
||||||
|
El punto de indirección entre ambos mundos para el render es
|
||||||
|
[render_context.hpp](source/core/rendering/render_context.hpp): el juego habla con
|
||||||
|
un `Rendering::Renderer*` opaco que es un alias de `GPU::GpuFrameRenderer`. Esto
|
||||||
|
permite cambiar de backend sin tocar las firmas del juego.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph entry["Punto de entrada"]
|
||||||
|
MAIN["main.cpp<br/>SDL_MAIN_USE_CALLBACKS"]
|
||||||
|
end
|
||||||
|
MAIN -->|posee| DIR["Director<br/>(es el programa)"]
|
||||||
|
|
||||||
|
subgraph core["source/core (motor)"]
|
||||||
|
SDLM["SDLManager<br/>ventana + GPU"]
|
||||||
|
GE["GlobalEvents<br/>F1-F7/F12/ESC/hotplug"]
|
||||||
|
INPUT["Input (singleton)"]
|
||||||
|
AUDIO["Audio (singleton)"]
|
||||||
|
RES["Resource::Loader / Pack"]
|
||||||
|
LOC["Locale (i18n)"]
|
||||||
|
OVL["Notifier · ServiceMenu<br/>DebugOverlay · DefineInputs"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph game["source/game (juego)"]
|
||||||
|
SCN["Scenes<br/>Logo · Title · Game"]
|
||||||
|
ENT["Entities<br/>Ship · Enemy · Bullet"]
|
||||||
|
SYS["Systems<br/>Collision · EnemyAi · DemoPilot"]
|
||||||
|
STG["StageManager / WaveRunner"]
|
||||||
|
FX["Effects<br/>debris · firework · score · trail"]
|
||||||
|
end
|
||||||
|
|
||||||
|
DIR --> SDLM
|
||||||
|
DIR --> GE
|
||||||
|
DIR --> OVL
|
||||||
|
DIR --> SCN
|
||||||
|
SCN --> ENT
|
||||||
|
SCN --> SYS
|
||||||
|
SCN --> STG
|
||||||
|
SCN --> FX
|
||||||
|
GE --> INPUT
|
||||||
|
SCN -.usa.-> AUDIO
|
||||||
|
SCN -.usa.-> RES
|
||||||
|
OVL -.usa.-> LOC
|
||||||
|
```
|
||||||
|
|
||||||
|
**Patrón dominante de comunicación:** singletons globales (`Input::get()`,
|
||||||
|
`Audio::get()`, `Locale::get()`, `Notifier`, `ServiceMenu`) más paso por
|
||||||
|
referencia de un `Rendering::Renderer*` y un `SceneContext&`. **No hay** un bus de
|
||||||
|
eventos genérico ni un ECS — las entidades viven en `std::array` de tamaño fijo
|
||||||
|
dentro de `GameScene` y los sistemas operan sobre un struct `Context` de punteros
|
||||||
|
(ver [§10](#10-lógica-del-juego)).
|
||||||
|
|
||||||
|
**Rasgo de diseño destacable:** gran parte de la lógica es *data-driven*. Los
|
||||||
|
enemigos, balas y el jugador se describen en **YAML declarativo**
|
||||||
|
(`data/entities/*/*.yaml`: physics/ai/animation/events), los stages en
|
||||||
|
`data/stages/stages.yaml` (oleadas), y las figuras vectoriales en ficheros `.shp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Punto de entrada y el Director
|
||||||
|
|
||||||
|
El `main` real está en [main.cpp](source/main.cpp) y usa el modo de callbacks de
|
||||||
|
SDL3 (`#define SDL_MAIN_USE_CALLBACKS 1`). En lugar de un bucle `while` clásico,
|
||||||
|
SDL llama a cuatro funciones, y todas son pura fontanería que delega en un
|
||||||
|
`Director`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// main.cpp
|
||||||
|
auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
|
||||||
|
System::Relaunch::setArgv(argc, argv);
|
||||||
|
auto director = std::make_unique<Director>(argc, argv);
|
||||||
|
*appstate = director.release(); // SDL guarda el puntero
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
auto SDL_AppEvent(void* s, SDL_Event* e) { return ((Director*)s)->handleEvent(*e); }
|
||||||
|
auto SDL_AppIterate(void* s) { return ((Director*)s)->iterate(); }
|
||||||
|
void SDL_AppQuit(void* s, ...) { /* reabsorbe y destruye el Director */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
La filosofía está escrita en el propio comentario de cabecera de
|
||||||
|
[director.hpp](source/core/system/director.hpp):
|
||||||
|
|
||||||
|
> *El Director és EL programa: posseeix la configuració, els subsistemes i
|
||||||
|
> l'estat.*
|
||||||
|
|
||||||
|
Como con `SDL_MAIN_USE_CALLBACKS` no hay un `scope` que envuelva todo el bucle,
|
||||||
|
el estado que antes vivía en un `run()` ahora es **miembro** del Director:
|
||||||
|
`sdl_` (SDLManager), `context_` (SceneContext), `debug_overlay_` y
|
||||||
|
`current_scene_` (todos `std::unique_ptr`, ver
|
||||||
|
[director.hpp:45-48](source/core/system/director.hpp#L45-L48)).
|
||||||
|
|
||||||
|
### Orden de arranque (constructor)
|
||||||
|
|
||||||
|
El constructor [Director::Director](source/core/system/director.cpp#L46) ejecuta el
|
||||||
|
bootstrap completo, en este orden:
|
||||||
|
|
||||||
|
1. `ConfigYaml::init()` — valores por defecto de configuración.
|
||||||
|
2. Parseo de argumentos (`--console`, `--reset-config`) en
|
||||||
|
[checkProgramArguments](source/core/system/director.cpp#L241).
|
||||||
|
3. `Utils::initializePathSystem()` + sistema de recursos
|
||||||
|
([§8](#8-recursos)): en *release* el `resources.pack` es obligatorio; en *dev*
|
||||||
|
hay fallback a `data/`.
|
||||||
|
4. Crea la carpeta de sistema (`~/.config/jailgames/<NAME>` en Linux) y carga/crea
|
||||||
|
`config.yaml` ([createSystemFolder](source/core/system/director.cpp#L260)).
|
||||||
|
5. Carga el `locale` ([§7](#7-audio) usa lo mismo: i18n).
|
||||||
|
6. `Input::init()` con el `gamecontrollerdb.txt` (autoasigna mandos a P1/P2 la
|
||||||
|
primera vez).
|
||||||
|
7. Crea `SDLManager` (ventana + GPU), oculta el cursor, inicializa `Audio`.
|
||||||
|
8. **Precarga bloqueante** de todos los recursos (música, sonidos, shapes) para
|
||||||
|
evitar tirones de I/O en las transiciones
|
||||||
|
([director.cpp:187-195](source/core/system/director.cpp#L187-L195)).
|
||||||
|
9. Crea el `SceneContext` y fija la escena inicial: `TITLE` en `_DEBUG`, `LOGO`
|
||||||
|
en el resto ([director.cpp:200-205](source/core/system/director.cpp#L200-L205)).
|
||||||
|
10. Inicializa los overlays de sistema: `DebugOverlay`, `Notifier`, `ServiceMenu`,
|
||||||
|
`DefineInputs`.
|
||||||
|
|
||||||
|
El destructor [Director::~Director](source/core/system/director.cpp#L218) guarda
|
||||||
|
la config y destruye los subsistemas **en orden inverso** a la construcción (el
|
||||||
|
`Notifier` referencia el renderer, así que debe morir antes que `sdl_`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Bucle principal
|
||||||
|
|
||||||
|
Cada frame, SDL llama a `SDL_AppIterate`, que delega en
|
||||||
|
[Director::iterate()](source/core/system/director.cpp#L383). Su estructura es:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant SDL
|
||||||
|
participant Dir as Director::iterate()
|
||||||
|
participant Scene
|
||||||
|
participant SDLM as SDLManager
|
||||||
|
participant GPU as GpuFrameRenderer
|
||||||
|
|
||||||
|
SDL->>Dir: iterate()
|
||||||
|
Note over Dir: si wants_quit_ → SDL_APP_SUCCESS
|
||||||
|
Dir->>Dir: si !scene o scene.isFinished() → advanceScene()
|
||||||
|
Dir->>Dir: delta_time = (now - last) capeado a 50 ms
|
||||||
|
Dir->>Dir: Input::update()
|
||||||
|
Dir->>Scene: update(dt)
|
||||||
|
Dir->>Dir: overlays.update(dt) + Audio::update()
|
||||||
|
Dir->>SDLM: clear() (= GPU.beginFrame)
|
||||||
|
alt swapchain no disponible
|
||||||
|
SDLM-->>Dir: false → saltar draw+present
|
||||||
|
end
|
||||||
|
Dir->>SDLM: updateRenderingContext()
|
||||||
|
Dir->>Scene: draw()
|
||||||
|
Dir->>Dir: overlays.draw() (capas)
|
||||||
|
Dir->>SDLM: present() (= GPU.endFrame → bloom + postfx)
|
||||||
|
```
|
||||||
|
|
||||||
|
Puntos concretos a tener en cuenta:
|
||||||
|
|
||||||
|
- **Pivot de escena**: si no hay escena o la actual reporta `isFinished()`, se
|
||||||
|
llama a [advanceScene()](source/core/system/director.cpp#L338), que destruye la
|
||||||
|
actual y construye la siguiente con
|
||||||
|
[buildScene()](source/core/system/director.cpp#L323) según
|
||||||
|
`context_->nextScene()`.
|
||||||
|
- **Delta time**: se mide con `SDL_GetTicks()` y se **capea a 50 ms** para evitar
|
||||||
|
saltos grandes tras un stall ([director.cpp:397-400](source/core/system/director.cpp#L397-L400)).
|
||||||
|
- **Orden de update**: `Input::update()` → `current_scene_->update(dt)` →
|
||||||
|
`debug_overlay_` → `Notifier` → `ServiceMenu` → `DefineInputs` → `Audio::update()`.
|
||||||
|
- **Render por capas** (de abajo arriba, entre `clear` y `present`):
|
||||||
|
escena → `debug_overlay_` → `Notifier` (toasts) → `ServiceMenu` → `DefineInputs`
|
||||||
|
(modal de rebinding). Si el overlay de rebinding está activo, el menú de servicio
|
||||||
|
no se pinta ([director.cpp:432-439](source/core/system/director.cpp#L432-L439)).
|
||||||
|
- **Salto de frame**: si `sdl_->clear()` devuelve `false` (swapchain no disponible,
|
||||||
|
p. ej. ventana minimizada), se omiten `draw` y `present` ese frame.
|
||||||
|
|
||||||
|
El bucle de eventos vive aparte, en
|
||||||
|
[Director::handleEvent()](source/core/system/director.cpp#L354), que enruta cada
|
||||||
|
`SDL_Event` por la cadena: **ventana → GlobalEvents → F11 (debug overlay) →
|
||||||
|
escena** (ver [§9](#9-comunicación-entre-módulos)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Sistema de escenas
|
||||||
|
|
||||||
|
La interfaz base es [scene.hpp](source/core/system/scene.hpp). Como dice su
|
||||||
|
cabecera, *el frame loop vive en el Director, no en cada escena*. Cada escena
|
||||||
|
implementa cuatro métodos puros:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
virtual void handleEvent(const SDL_Event&) = 0; // eventos no-globales
|
||||||
|
virtual void update(float delta_time) = 0; // lógica
|
||||||
|
virtual void draw() = 0; // pintado (entre clear y present)
|
||||||
|
virtual auto isFinished() const -> bool = 0; // ¿transición pendiente?
|
||||||
|
```
|
||||||
|
|
||||||
|
Una escena pide transición vía `context_.setNextScene(...)`; en el siguiente frame
|
||||||
|
`isFinished()` devuelve `true` y el Director la destruye para construir la
|
||||||
|
siguiente.
|
||||||
|
|
||||||
|
### SceneContext
|
||||||
|
|
||||||
|
[scene_context.hpp](source/core/system/scene_context.hpp) es el "buzón" de
|
||||||
|
transición que el Director posee y va pasando a cada escena por referencia. Tiene:
|
||||||
|
|
||||||
|
- `SceneType` (enum): `LOGO`, `TITLE`, `GAME`, `EXIT`.
|
||||||
|
- `Option` (p. ej. `JUMP_TO_TITLE_MAIN`) consumible con `consumeOption()`.
|
||||||
|
- `MatchConfig` (jugadores activos, modo NORMAL/DEMO) para pasar a `GAME`.
|
||||||
|
- El **índice del escenario de demo** (`demoScenarioIndex()` / `advanceDemoScenario()`),
|
||||||
|
que persiste entre escenas para que cada entrada al attract mode muestre el
|
||||||
|
siguiente escenario curado (ver [§11](#11-ia-del-modo-demo-attract)).
|
||||||
|
|
||||||
|
Existe además una variable global `SceneManager::actual` que el Director mantiene
|
||||||
|
sincronizada con la escena en curso (compatibilidad hacia atrás).
|
||||||
|
|
||||||
|
### Las tres escenas (FSM jerárquica)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> LOGO
|
||||||
|
LOGO --> TITLE
|
||||||
|
TITLE --> GAME : START (1P/2P)
|
||||||
|
TITLE --> GAME : idle timeout (DEMO)
|
||||||
|
GAME --> TITLE : game over / fin demo (input)
|
||||||
|
GAME --> LOGO : fin demo (timeout/muerte)
|
||||||
|
TITLE --> [*] : EXIT
|
||||||
|
```
|
||||||
|
|
||||||
|
Cada escena tiene además su **propia** máquina de estados interna:
|
||||||
|
|
||||||
|
- **[LogoScene](source/game/scenes/logo_scene.hpp)** — `AnimationState`:
|
||||||
|
`PRE_ANIMATION → ANIMATION → POST_ANIMATION → EXPLOSION → POST_EXPLOSION`. Anima
|
||||||
|
el logo JAILGAMES y lo hace explotar en fragmentos (debris).
|
||||||
|
- **[TitleScene](source/game/scenes/title_scene.hpp)** — `TitleState`:
|
||||||
|
`STARFIELD_FADE_IN → STARFIELD → MAIN → PLAYER_JOIN_PHASE → BLACK_SCREEN →
|
||||||
|
DEMO_DIVE → DEMO_CURTAIN`. Naves 3D flotantes (vía
|
||||||
|
[ShipAnimator](source/game/title/ship_animator.hpp)), selección 1P/2P, y un
|
||||||
|
`idle_timer_` en el estado `MAIN` que dispara el attract mode por inactividad.
|
||||||
|
- **[GameScene](source/game/scenes/game_scene.hpp)** — es el núcleo del juego y se
|
||||||
|
detalla en [§10](#10-lógica-del-juego).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Renderizado: de la lógica al píxel
|
||||||
|
|
||||||
|
Este es el subsistema más denso. La idea central: **toda la geometría son líneas**
|
||||||
|
(la estética es vectorial). El juego acumula líneas en CPU durante `draw()`, y al
|
||||||
|
final del frame se envían a la GPU en un único batch, se rasterizan a una textura
|
||||||
|
*offscreen*, y un par de pases de post-procesado (bloom + flicker/fondo) componen
|
||||||
|
la imagen final sobre la swapchain.
|
||||||
|
|
||||||
|
### 5.1 Capas del subsistema
|
||||||
|
|
||||||
|
| Fichero | Rol |
|
||||||
|
|---|---|
|
||||||
|
| [sdl_manager.hpp/.cpp](source/core/rendering/sdl_manager.hpp) | Crea la ventana SDL, posee el `GpuFrameRenderer`, gestiona zoom/fullscreen/letterbox. Expone `clear()` / `present()` / `getRenderer()`. |
|
||||||
|
| [gpu/gpu_frame_renderer.hpp/.cpp](source/core/rendering/gpu/gpu_frame_renderer.hpp) | Orquestador del frame GPU: `beginFrame` → `pushLine`/`pushRect` → `endFrame` (`flushBatch` + `bloomPass` + `compositePass`). |
|
||||||
|
| [gpu/gpu_device](source/core/rendering/gpu/gpu_device.hpp) | Wrapper del `SDL_GPUDevice` (claim de ventana, formato de swapchain). |
|
||||||
|
| [gpu/gpu_line_pipeline](source/core/rendering/gpu/gpu_line_pipeline.hpp) | Pipeline de líneas: dibuja cada línea como un quad (2 triángulos) con antialias geométrico. |
|
||||||
|
| [gpu/gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp) | Blur gaussiano separable (pase H + pase V) sobre dos texturas ping-pong. |
|
||||||
|
| [gpu/gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp) | Composite final: mezcla escena + bloom + flicker + fondo pulsante. |
|
||||||
|
| [line_renderer.hpp/.cpp](source/core/rendering/line_renderer.hpp) | API que usa el juego: `Rendering::linea(...)` y `lineaGlow(...)`. |
|
||||||
|
| [shape_renderer.hpp/.cpp](source/core/rendering/shape_renderer.hpp) | `renderShape(...)`: dibuja una `Shape` aplicando transformación y, opcionalmente, glow multipase. |
|
||||||
|
|
||||||
|
### 5.2 Una `Shape` y cómo se carga
|
||||||
|
|
||||||
|
Una "shape" es una figura vectorial: un conjunto de **polilíneas** y **líneas**
|
||||||
|
([shape.hpp](source/core/graphics/shape.hpp)). Los ficheros viven en `data/shapes/`
|
||||||
|
con extensión `.shp` y un formato de texto tipo clave:valor. Ejemplo real
|
||||||
|
([data/shapes/ship/arrow.shp](data/shapes/ship/arrow.shp)):
|
||||||
|
|
||||||
|
```
|
||||||
|
name: arrow
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||||
|
```
|
||||||
|
|
||||||
|
> Nota: el formato real usa directivas `name:`, `scale:`, `center:`,
|
||||||
|
> `polyline:` y `line:` (Y negativo = arriba). No es la sintaxis
|
||||||
|
> `POLYLINE: (x,y)` que podría suponerse de otros motores.
|
||||||
|
|
||||||
|
La carga la centraliza [shape_loader.hpp](source/core/graphics/shape_loader.hpp)
|
||||||
|
(`Graphics::ShapeLoader::load(filename)`), con caché de `std::shared_ptr<Shape>`.
|
||||||
|
Todas las shapes se precargan en el boot del Director.
|
||||||
|
|
||||||
|
### 5.3 El flujo de un frame de render
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A["Scene::draw()<br/>(acumula en CPU)"] --> B["Rendering::linea / renderShape"]
|
||||||
|
B --> C["GpuFrameRenderer::pushLine()<br/>extruye quad → vertices_ / indices_"]
|
||||||
|
C -.repetido N veces.-> C
|
||||||
|
A --> D["SDLManager::present()<br/>= GpuFrameRenderer::endFrame()"]
|
||||||
|
D --> E["flushBatch()<br/>sube VBO/IBO, dibuja sobre OFFSCREEN"]
|
||||||
|
E --> F["bloomPass()<br/>H: high-pass+blur → bloom_a<br/>V: blur → bloom_b"]
|
||||||
|
F --> G["compositePass()<br/>offscreen + bloom_b + flicker + fondo<br/>→ swapchain (letterbox)"]
|
||||||
|
G --> H["SubmitGPUCommandBuffer + present"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Paso a paso, con anclas reales:
|
||||||
|
|
||||||
|
1. **Emisión (juego).** Durante `current_scene_->draw()`, el juego llama a
|
||||||
|
[Rendering::linea()](source/core/rendering/line_renderer.hpp#L33) (y
|
||||||
|
`renderShape`, `VectorText`, `Playfield`, etc.). Las coordenadas son **lógicas
|
||||||
|
(1280×720)**. El color por defecto si `alpha==0` es el verde fósforo CRT
|
||||||
|
`DEFAULT_LINE_COLOR = {100,255,100,255}`.
|
||||||
|
2. **Acumulación (CPU).** `linea()` pre-multiplica el brillo y llama a
|
||||||
|
[GpuFrameRenderer::pushLine()](source/core/rendering/gpu/gpu_frame_renderer.hpp#L88),
|
||||||
|
que **extruye** la línea en un quad (4 vértices, 6 índices) y lo acumula en
|
||||||
|
`vertices_` / `indices_`. Si el antialias está activo, añade ~0.5 px de padding y
|
||||||
|
marca `edge_dist` para el fade del fragment shader.
|
||||||
|
3. **Flush (GPU).** En `endFrame()`, `flushBatch()` sube el batch a un VBO/IBO,
|
||||||
|
abre un render pass sobre el `offscreen_texture_` (R8G8B8A8, tamaño físico
|
||||||
|
configurable, independiente del lógico) y dibuja con el `line_pipeline_`. El
|
||||||
|
vertex shader transforma píxeles lógicos → NDC; el fragment shader aplica
|
||||||
|
`smoothstep` sobre `edge_dist` para el suavizado.
|
||||||
|
4. **Bloom.** `bloomPass()` hace un blur separable: pase H (high-pass por
|
||||||
|
luminancia + blur horizontal → `bloom_texture_a_`) y pase V (blur vertical →
|
||||||
|
`bloom_texture_b_`). Parámetros en `PostFxParams`
|
||||||
|
([gpu_frame_renderer.hpp:33-51](source/core/rendering/gpu/gpu_frame_renderer.hpp#L33-L51)).
|
||||||
|
5. **Composite.** `compositePass()` dibuja un triángulo *fullscreen* sobre la
|
||||||
|
swapchain, muestreando offscreen + bloom, aplicando flicker temporal y un fondo
|
||||||
|
verde pulsante. Aquí se aplica el **letterbox** vía el viewport físico
|
||||||
|
(`setViewport`).
|
||||||
|
|
||||||
|
El interruptor maestro de post-proceso es **F6** (`setPostFxEnabled`): cuando está
|
||||||
|
OFF, la escena offscreen sale tal cual (passthrough), útil para A/B testing.
|
||||||
|
|
||||||
|
### 5.4 Texto, 3D y elementos de escena
|
||||||
|
|
||||||
|
- **[VectorText](source/core/graphics/vector_text.hpp)** — renderiza texto donde
|
||||||
|
cada carácter es una `Shape` precargada.
|
||||||
|
- **[Camera3D](source/core/graphics/camera3d.hpp)** + **[Wireframe3D](source/core/graphics/wireframe3d.hpp)**
|
||||||
|
— proyección perspectiva en CPU de mallas 3D (vértices + aristas) a líneas 2D.
|
||||||
|
Lo usan el starfield 3D y las naves del título.
|
||||||
|
- **[Starfield](source/core/graphics/starfield.hpp)** (campo de estrellas 3D que
|
||||||
|
vienen hacia la cámara) y **[StarfieldParallax](source/core/graphics/starfield_parallax.hpp)**
|
||||||
|
(capas 2D de fondo con parallax).
|
||||||
|
- **[Playfield](source/core/graphics/playfield.hpp)** — rejilla de fondo con
|
||||||
|
animación de construcción y *ripples* (ondas) que reaccionan a la nave y a las
|
||||||
|
explosiones.
|
||||||
|
- **[Border](source/core/graphics/border.hpp)** — marco de 4 lados que se desplaza
|
||||||
|
al recibir impactos.
|
||||||
|
- **[Curtain](source/core/graphics/curtain.hpp)** — cortinilla negra para
|
||||||
|
transiciones; se pinta siempre la última.
|
||||||
|
|
||||||
|
### 5.5 Shaders: fuentes, compilación y selección
|
||||||
|
|
||||||
|
Las fuentes GLSL viven en [shaders/](shaders/): `line.vert.glsl`, `line.frag.glsl`,
|
||||||
|
`postfx.vert.glsl`, `postfx.frag.glsl`, `bloom.frag.glsl`. **No se cargan de disco en
|
||||||
|
runtime**: se embeben como arrays/strings en el binario.
|
||||||
|
|
||||||
|
**Pipeline de compilación (SPIR-V, Linux/Windows).** Lo orquesta
|
||||||
|
[CMakeLists.txt:139-187](CMakeLists.txt#L139). La lógica clave:
|
||||||
|
|
||||||
|
- Para cada `.glsl` hay un header destino en
|
||||||
|
[gpu/spv/](source/core/rendering/gpu/spv/) (p. ej. `line_vert_spv.h`).
|
||||||
|
- CMake busca `glslc` (`find_program(GLSLC_EXE ...)`). Hay **tres caminos**:
|
||||||
|
1. `glslc` presente → un `add_custom_command` regenera los headers SPV cuando
|
||||||
|
cambian los `.glsl`, vía el target `shaders` del que depende el ejecutable.
|
||||||
|
2. `glslc` ausente pero **los headers ya están commiteados** → se usan tal cual
|
||||||
|
(los `.spv.h` están versionados en el repo).
|
||||||
|
3. `glslc` ausente **y** faltan headers → `FATAL_ERROR` pidiendo instalar
|
||||||
|
`shaderc`/`vulkan-sdk`.
|
||||||
|
- La conversión binario→header la hace el script
|
||||||
|
[tools/shaders/compile_spirv.cmake](tools/shaders/compile_spirv.cmake): invoca
|
||||||
|
`glslc -O -fshader-stage=<vert|frag>` para producir el `.spv`, lee el binario como
|
||||||
|
hex (`file(READ ... HEX)`) y escribe un header con
|
||||||
|
`static const uint8_t LINE_VERT_SPV[] = { 0x.., ... };` y su `_SIZE`. Es
|
||||||
|
multiplataforma puro CMake (no necesita `bash` ni `xxd`).
|
||||||
|
|
||||||
|
**MSL (macOS).** Los headers Metal en [gpu/msl/](source/core/rendering/gpu/msl/)
|
||||||
|
(`line_vert.msl.h`, etc.) están **escritos a mano** (no los genera CMake), como
|
||||||
|
strings literales C++.
|
||||||
|
|
||||||
|
**Selección SPV vs MSL: es _compile-time_, no runtime.** La hace
|
||||||
|
[shader_factory.hpp](source/core/rendering/gpu/shader_factory.hpp) con `#ifdef __APPLE__`:
|
||||||
|
en Apple expone `createShaderMSL(...)` (`SDL_GPU_SHADERFORMAT_MSL`), y en el resto
|
||||||
|
`createShaderSPIRV(...)` (`SDL_GPU_SHADERFORMAT_SPIRV`). Cada pipeline llama al helper
|
||||||
|
disponible con el header embebido correspondiente. (Es decir: no es `GpuDevice` quien
|
||||||
|
elige el backend de shader, sino el preprocesador al compilar.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Entrada
|
||||||
|
|
||||||
|
El subsistema de input ([core/input/](source/core/input/)) es un **singleton**
|
||||||
|
(`Input::init()` / `Input::get()` / `Input::destroy()`) que unifica teclado,
|
||||||
|
gamepads y ratón.
|
||||||
|
|
||||||
|
- **Acciones**: enum `InputAction` (`LEFT`, `RIGHT`, `THRUST`, `SHOOT`, `START`,
|
||||||
|
`MENU`, ...) en [input_types.hpp](source/core/input/input_types.hpp).
|
||||||
|
- **Bindings por jugador**: hay bindings separados de teclado y de gamepad para P1
|
||||||
|
y P2, que se cargan de la config con `applyPlayer1Bindings()` /
|
||||||
|
`applyPlayer2Bindings()` (llamados desde el constructor del Director).
|
||||||
|
- **Captura por frame**: `Input::update()` lee `SDL_GetKeyboardState()` y los ejes
|
||||||
|
y botones del gamepad, y hace *edge-detection* para distinguir `just_pressed` de
|
||||||
|
`is_held`. La consulta es `checkAction(...)` / `checkActionPlayer1/2(...)`.
|
||||||
|
- **Hotplug**: `Input::handleEvent()` procesa `SDL_EVENT_GAMEPAD_ADDED/REMOVED`
|
||||||
|
(`addGamepad` / `removeGamepad`) y notifica con un toast vía `Notifier`.
|
||||||
|
- **Ratón**: [mouse.hpp](source/core/input/mouse.hpp) auto-oculta el cursor.
|
||||||
|
- **Rebinding en runtime**: [define_inputs.hpp](source/core/input/define_inputs.hpp)
|
||||||
|
es un modal singleton que captura una secuencia de acciones, persiste en config y
|
||||||
|
reaplica bindings sin reiniciar.
|
||||||
|
|
||||||
|
El enrutado de input ocurre en dos sitios: los eventos **globales** pasan por
|
||||||
|
`GlobalEvents::handle()` (que primero deja a `Input` procesar el hotplug), y la
|
||||||
|
lógica de juego consulta directamente `Input::get()->checkAction...` durante
|
||||||
|
`update()` (p. ej. [Ship::processInput](source/game/entities/ship.hpp)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Audio
|
||||||
|
|
||||||
|
[core/audio/](source/core/audio/) es otro singleton (`Audio::init/get/destroy`)
|
||||||
|
con un motor de bajo nivel propio:
|
||||||
|
|
||||||
|
- **[Audio](source/core/audio/audio.hpp)** — capa lógica: `playMusic()`,
|
||||||
|
`playSound()`, volúmenes por grupo (`GAME`, `INTERFACE`), `playSoundWithEcho/Reverb`.
|
||||||
|
- **[jail_audio.hpp](source/core/audio/jail_audio.hpp)** (`Ja::Engine`) — motor
|
||||||
|
sobre SDL3 audio: streaming de **OGG** (vía `stb_vorbis`) para música, **WAV**
|
||||||
|
descomprimido para efectos, mezcla en N canales.
|
||||||
|
- **[audio_adapter.hpp](source/core/audio/audio_adapter.hpp)** —
|
||||||
|
`AudioResource::getMusic/getSound`: caché *lazy* que carga bytes vía
|
||||||
|
`Resource::Helper` y los decodifica una sola vez.
|
||||||
|
- **[audio_effects.hpp](source/core/audio/audio_effects.hpp)** — DSP de echo y
|
||||||
|
reverb; presets en `data/config/sounds.yaml`
|
||||||
|
([sound_effects_config.hpp](source/core/audio/sound_effects_config.hpp)).
|
||||||
|
|
||||||
|
El Director precarga toda la música y todos los sonidos en el boot, y llama a
|
||||||
|
`Audio::update()` una vez por frame.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Recursos
|
||||||
|
|
||||||
|
[core/resources/](source/core/resources/) abstrae de dónde salen los bytes:
|
||||||
|
|
||||||
|
- **[resource_pack](source/core/resources/resource_pack.hpp)** (`Resource::Pack`)
|
||||||
|
— lee un fichero empaquetado con cabecera *magic* `"ORNI"` y entradas con CRC32
|
||||||
|
para validación de integridad.
|
||||||
|
- **[resource_loader](source/core/resources/resource_loader.hpp)**
|
||||||
|
(`Resource::Loader`, singleton Meyers) — `loadResource()`, `resourceExists()`,
|
||||||
|
`listResources(prefix)`, `validatePack()`.
|
||||||
|
- **[resource_helper](source/core/resources/resource_helper.hpp)** — wrappers de
|
||||||
|
conveniencia (`initializeResourceSystem`, `listResources`, `loadFile`).
|
||||||
|
|
||||||
|
**Estrategia dual** (decidida en el constructor del Director,
|
||||||
|
[director.cpp:64-93](source/core/system/director.cpp#L64-L93)):
|
||||||
|
|
||||||
|
- **Release** (`RELEASE_BUILD`): `resources.pack` es **obligatorio** y se valida su
|
||||||
|
integridad; si falla, el juego aborta. No hay fallback (ver memoria de proyecto
|
||||||
|
*"No fallback a SDL_Renderer"* — aquí es la política equivalente para recursos).
|
||||||
|
- **Dev**: intenta el pack; si no está, hace **fallback al directorio `data/`** del
|
||||||
|
filesystem, escaneándolo según prefijo (`music/`, `sounds/`, `shapes/`).
|
||||||
|
|
||||||
|
El formato de datos de juego:
|
||||||
|
|
||||||
|
- **Entidades** (`data/entities/<nombre>/<nombre>.yaml`) — YAML declarativo con
|
||||||
|
`shape`, `physics`, `ai`, `animation`, `wounded`, `spawn`, `colors`, `score`,
|
||||||
|
`events`. Ejemplo: [data/entities/square/square.yaml](data/entities/square/square.yaml).
|
||||||
|
- **Stages** (`data/stages/stages.yaml`) — oleadas (`waves`) con `spawn`,
|
||||||
|
`spawn_interval`, `next` y multiplicadores de dificultad por stage.
|
||||||
|
- **Shapes** (`data/shapes/**/*.shp`) — figuras vectoriales (ver [§5.2](#52-una-shape-y-cómo-se-carga)).
|
||||||
|
|
||||||
|
El parser YAML usado es [fkyaml](source/external/fkyaml_node.hpp) (cabecera única),
|
||||||
|
envuelto por [config_yaml](source/game/config_yaml.hpp).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Comunicación entre módulos
|
||||||
|
|
||||||
|
No hay un sistema de mensajería desacoplado. La comunicación es:
|
||||||
|
|
||||||
|
1. **Eventos SDL → cadena del Director.** Por cada `SDL_Event`,
|
||||||
|
[Director::handleEvent](source/core/system/director.cpp#L354) intenta, en orden:
|
||||||
|
`SDLManager::handleWindowEvent` → `GlobalEvents::handle` → F11 (debug overlay) →
|
||||||
|
`current_scene_->handleEvent`.
|
||||||
|
|
||||||
|
2. **GlobalEvents** ([global_events.cpp](source/core/system/global_events.cpp)) es
|
||||||
|
el orquestador de la entrada global. Su `handle()` hace, en orden:
|
||||||
|
`Input::get()->handleEvent` (hotplug) → `consumeIfDefineActive` (si el modal de
|
||||||
|
rebinding está activo, **engulle todo**) → `SDL_EVENT_QUIT` → ratón → botón MENU
|
||||||
|
del mando → reenvío al `ServiceMenu` si está abierto → teclas de función:
|
||||||
|
|
||||||
|
| Tecla | Acción |
|
||||||
|
|---|---|
|
||||||
|
| F1 / F2 | reducir / aumentar tamaño de ventana |
|
||||||
|
| F3 | fullscreen |
|
||||||
|
| F4 | VSync |
|
||||||
|
| F5 | antialias geométrico |
|
||||||
|
| F6 | post-procesado (bloom/flicker/fondo) |
|
||||||
|
| F7 | idioma ca ↔ en (hot-swap de `Locale`) |
|
||||||
|
| F11 | debug overlay (gestionado en el Director, no en GlobalEvents) |
|
||||||
|
| F12 | menú de servicio |
|
||||||
|
| ESC | doble pulsación para salir (la 1ª muestra un toast de confirmación) |
|
||||||
|
|
||||||
|
3. **Singletons compartidos.** `Input`, `Audio`, `Locale`, `Notifier`,
|
||||||
|
`ServiceMenu`, `DefineInputs` se acceden globalmente vía `::get()`. Muchos
|
||||||
|
comprueban `nullptr` para degradar con elegancia (p. ej. el hotplug notifica
|
||||||
|
solo si `Notifier::get() != nullptr`).
|
||||||
|
|
||||||
|
4. **Paso por referencia.** Las escenas reciben `SDLManager&` y `SceneContext&`; el
|
||||||
|
render se propaga como `Rendering::Renderer*`. Los sistemas de juego reciben un
|
||||||
|
struct `Context` con punteros a los pools (ver [§10](#10-lógica-del-juego)).
|
||||||
|
|
||||||
|
**Overlays de sistema** (todos singletons, todos por encima de la escena):
|
||||||
|
|
||||||
|
- **[Notifier](source/core/system/notifier.hpp)** — toasts deslizantes centrados
|
||||||
|
(`notifyInfo/Warn/Exit`), con máquina de animación HIDDEN/ENTERING/HOLDING/EXITING.
|
||||||
|
- **[ServiceMenu](source/core/system/service_menu.hpp)** — menú de configuración
|
||||||
|
(F12) con pila de páginas (vídeo, audio, controles, sistema...).
|
||||||
|
- **[DebugOverlay](source/core/system/debug_overlay.hpp)** — HUD de FPS/VSync (F11).
|
||||||
|
- **[Relaunch](source/core/system/relaunch.hpp)** — reinicio en caliente vía
|
||||||
|
`execv` (lo solicita el ServiceMenu, lo ejecuta `SDL_AppQuit`).
|
||||||
|
|
||||||
|
**Lo que NO existe** (verificado): no hay event bus genérico, ni cola de mensajes
|
||||||
|
desacoplada, ni un FSM genérico reutilizable fuera de las máquinas de estado
|
||||||
|
concretas de cada escena/sistema, ni un ECS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Lógica del juego
|
||||||
|
|
||||||
|
Toda la partida vive en [GameScene](source/game/scenes/game_scene.hpp). Es la clase
|
||||||
|
más grande del juego y actúa como orquestador. Posee:
|
||||||
|
|
||||||
|
- El mundo físico [Physics::PhysicsWorld](source/core/physics/physics_world.hpp)
|
||||||
|
(integración cinemática + colisiones físicas).
|
||||||
|
- Pools de tamaño **fijo**: `std::array<Ship, 2>`,
|
||||||
|
`std::array<Enemy, MAX_ORNIS>` (15), `std::array<Bullet, MAX_BULLETS_TOTAL>` (6:
|
||||||
|
P1=[0,1,2], P2=[3,4,5]).
|
||||||
|
- Estado de partida: vidas, score y *death timers* por jugador, máquina de
|
||||||
|
game over (`GameOverState`: `NONE/CONTINUE/GAME_OVER`), continues usados.
|
||||||
|
- El stage system, los efectos visuales, y los `DemoPilot` (uno por nave).
|
||||||
|
|
||||||
|
### 10.1 Orquestación por frame
|
||||||
|
|
||||||
|
[GameScene::update()](source/game/scenes/game_scene.cpp) es un orquestador delgado;
|
||||||
|
cada paso es una función privada (descompuesto para reducir complejidad cognitiva):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void GameScene::update(float dt) {
|
||||||
|
if (ServiceMenu abierto) return; // pausa global (draw sí sigue)
|
||||||
|
stepPhysics(dt);
|
||||||
|
if (mode == DEMO) { if (stepDemo(dt)) return; }
|
||||||
|
else if (game_over_state_ == NONE) { stepShootingInput(); stepMidGameJoin(); }
|
||||||
|
if (stepContinueScreen(dt)) return;
|
||||||
|
if (stepGameOver(dt)) return;
|
||||||
|
stepDeathSequence(dt);
|
||||||
|
stepStageStateMachine(dt);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
El corazón del gameplay es
|
||||||
|
[stepStageStateMachine](source/game/scenes/game_scene.hpp#L166), que despacha según
|
||||||
|
el estado del stage; en `PLAYING`,
|
||||||
|
[runStagePlaying](source/game/scenes/game_scene.hpp#L169) ejecuta: WaveRunner
|
||||||
|
(spawns) → IA de cada enemigo → control de naves
|
||||||
|
([updateShipsControl](source/game/scenes/game_scene.cpp), que en demo usa
|
||||||
|
`applyMovement` con el control del pilot y fuera de demo usa `processInput`) →
|
||||||
|
detección de colisiones ([runCollisionDetections](source/game/scenes/game_scene.hpp#L176)).
|
||||||
|
|
||||||
|
`draw()` despacha de forma análoga según `GameOverState` y el estado del stage, y
|
||||||
|
siempre pinta la cortinilla al final.
|
||||||
|
|
||||||
|
### 10.2 Entidades
|
||||||
|
|
||||||
|
Las tres heredan de `Entities::Entity` ([entity.hpp](source/core/entities/entity.hpp)):
|
||||||
|
|
||||||
|
- **[Ship](source/game/entities/ship.hpp)** — nave del jugador. `processInput()`
|
||||||
|
(humano) y `applyMovement()` (usado por la IA demo). Estados: activa,
|
||||||
|
invulnerable (parpadeo tras spawn), herida (`hurt`). Al morir genera debris con
|
||||||
|
la inercia heredada.
|
||||||
|
- **[Enemy](source/game/entities/enemy.hpp)** — 5 tipos (`EnemyType`: `PENTAGON`,
|
||||||
|
`SQUARE`, `PINWHEEL`, `STAR`, `ORB`). Toda su config (físicas, IA, animación,
|
||||||
|
eventos) viene del **YAML** vía [EnemyRegistry](source/game/entities/enemy_registry.hpp).
|
||||||
|
Tiene salud (la mayoría HP=1; `ORB` HP=10) y estado *wounded* (parpadeo).
|
||||||
|
- **[Bullet](source/game/entities/bullet.hpp)** — con `owner_id` (0=P1, 1=P2,
|
||||||
|
≥16=enemigo) y `prev_position` para colisión *swept* (la bala que cruza un enemigo
|
||||||
|
entre dos frames). Config en [BulletRegistry](source/game/entities/bullet_registry.hpp).
|
||||||
|
|
||||||
|
### 10.3 IA de enemigos: declarativa
|
||||||
|
|
||||||
|
Los enemigos **no** tienen comportamiento hardcoded. El YAML describe:
|
||||||
|
|
||||||
|
- Una **primitiva de movimiento** (`MovementType` en
|
||||||
|
[enemy_ai.hpp](source/game/entities/enemy_ai.hpp)): `ZIGZAG`, `TRACKING`,
|
||||||
|
`RECTILINEAR_PROXIMITY`, `WANDER`, `CHASE`, `FLEE`.
|
||||||
|
- **Acciones de tick** periódicas (p. ej. `SHOOT`).
|
||||||
|
- **Eventos** (`on_hit`, `on_no_health`, `on_hurt_end`, `on_destroy`) con acciones
|
||||||
|
(`APPLY_IMPULSE`, `DECREASE_HEALTH`, `CREATE_DEBRIS`, `ADD_SCORE`, `FLASH`,
|
||||||
|
`FIRE_BULLET`, `DESTROY`, ...).
|
||||||
|
|
||||||
|
Dos sistemas los ejecutan:
|
||||||
|
|
||||||
|
- **[EnemyAiSystem](source/game/systems/enemy_ai_system.hpp)** — `move()` aplica la
|
||||||
|
primitiva de movimiento; `tick()` añade las acciones periódicas. Helper
|
||||||
|
`findNearestShipPosition()` para las primitivas que buscan al jugador.
|
||||||
|
- **[EnemyEventDispatcher](source/game/systems/enemy_event_dispatcher.hpp)** —
|
||||||
|
ejecuta las acciones declarativas cuando se dispara un evento.
|
||||||
|
|
||||||
|
### 10.4 Colisiones
|
||||||
|
|
||||||
|
[CollisionSystem](source/game/systems/collision_system.hpp) recibe un struct
|
||||||
|
`Context` (punteros a ships/enemies/bullets, managers de efectos, timers, scores,
|
||||||
|
vidas y un callback `on_player_hit`) que GameScene construye en
|
||||||
|
[buildCollisionContext](source/game/scenes/game_scene.hpp#L174). Detecta:
|
||||||
|
bala↔enemigo, nave↔enemigo, bala↔jugador (fuego amigo / autodisparo), bala
|
||||||
|
enemiga↔nave, y balas fuera del área. Reglas observadas: el primer impacto deja al
|
||||||
|
enemigo *wounded*; el segundo lo destruye y suma score. La nave entra en `hurt` al
|
||||||
|
primer toque y muere al segundo durante ese estado.
|
||||||
|
|
||||||
|
### 10.5 Stages y oleadas
|
||||||
|
|
||||||
|
- **[StageManager](source/game/stage_system/stage_manager.hpp)** — FSM del stage
|
||||||
|
(`EstatStage`): `INIT_HUD` (anima el HUD, 3 s) → `LEVEL_START` ("ENEMY INCOMING",
|
||||||
|
3 s, arranca `game.ogg`) → `PLAYING` → `LEVEL_COMPLETED` ("GOOD JOB COMMANDER!",
|
||||||
|
3 s) → siguiente stage. `initDemo(stage_id)` arranca directamente en `PLAYING`
|
||||||
|
para el attract mode.
|
||||||
|
- **[WaveRunner](source/game/stage_system/wave_runner.hpp)** — emite los enemigos de
|
||||||
|
cada oleada según `spawn_interval` y avanza cuando se cumple `next` (`all_dead`,
|
||||||
|
`timeout`, o ambos).
|
||||||
|
- **[StageConfig](source/game/stage_system/stage_config.hpp)** /
|
||||||
|
[StageLoader](source/game/stage_system/stage_loader.hpp) — modelo y carga del
|
||||||
|
YAML de stages.
|
||||||
|
|
||||||
|
### 10.6 Dos capas de colisión: física vs gameplay
|
||||||
|
|
||||||
|
Conviene no confundirlas, porque conviven:
|
||||||
|
|
||||||
|
**1. Física** — [PhysicsWorld](source/core/physics/physics_world.hpp) /
|
||||||
|
[physics_world.cpp](source/core/physics/physics_world.cpp). Es un mundo 2D
|
||||||
|
minimalista de arcade. Cada frame, `update(dt)` hace tres pasos:
|
||||||
|
|
||||||
|
1. **Integración** semi-implícita de Euler con damping exponencial
|
||||||
|
(`v += (F·invMass)·dt; v *= exp(-damping·dt); x += v·dt`) sobre cada
|
||||||
|
[RigidBody](source/core/physics/rigid_body.hpp) no estático. Un cuerpo con
|
||||||
|
`mass=0` (`inverse_mass=0`) es estático (masa infinita).
|
||||||
|
2. **Rebote contra los bordes** del `PLAYAREA` (`resolveBoundsCollisions`): reposiciona
|
||||||
|
el cuerpo dentro del rect y refleja la componente normal de la velocidad por su
|
||||||
|
`restitution`. Antes de reflejar, invoca un `BoundsHitCallback` opcional con la
|
||||||
|
velocidad de impacto entrante (lo usa GameScene para los efectos de borde).
|
||||||
|
3. **Colisiones cuerpo-cuerpo** (`resolveBodyCollisions`): broadphase trivial
|
||||||
|
**O(n²)** (suficiente para ~23 cuerpos), círculo-círculo, con corrección posicional
|
||||||
|
de penetración + **impulso elástico** `j = -(1+e)(v_rel·n) / (1/mₐ + 1/m_b)`
|
||||||
|
(referencia Box2D / Chris Hecker, en `resolveBodyPair`). Los cuerpos con `radius=0`
|
||||||
|
(las balas, cinemáticas puras) **no** participan aquí.
|
||||||
|
|
||||||
|
Los `RigidBody` los poseen las entidades; el mundo solo guarda punteros no-owning
|
||||||
|
(`addBody`/`removeBody`).
|
||||||
|
|
||||||
|
**2. Gameplay** — [collision_system.cpp](source/game/systems/collision_system.cpp)
|
||||||
|
(ver [§10.4](#104-colisiones)), que decide *qué pasa* (daño, score, muerte). Usa los
|
||||||
|
helpers de [collision.hpp](source/core/physics/collision.hpp): `checkCollision`
|
||||||
|
(círculo-círculo discreto, distancia al cuadrado sin `sqrt`) y `checkCollisionSwept`
|
||||||
|
(segment-círculo, para que una bala rápida no atraviese un enemigo entre frames —
|
||||||
|
*anti-tunneling*). Estos checks usan el `collision_radius` de la **entidad**
|
||||||
|
(con amplificador opcional de hitbox), no el `radius` del body.
|
||||||
|
|
||||||
|
En resumen: la **física** mueve y rebota los cuerpos; el **gameplay** detecta los
|
||||||
|
contactos relevantes para las reglas. Una bala no rebota físicamente (radius 0) pero sí
|
||||||
|
provoca daño vía el check *swept*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. IA del modo demo (attract)
|
||||||
|
|
||||||
|
El attract mode es una partida que se juega sola para atraer al jugador. Se activa
|
||||||
|
desde [TitleScene](source/game/scenes/title_scene.hpp) cuando el `idle_timer_` en el
|
||||||
|
estado `MAIN` supera el umbral de inactividad, y desde
|
||||||
|
[GameScene](source/game/scenes/game_scene.hpp) cuando `match_config_.mode == DEMO`.
|
||||||
|
|
||||||
|
La IA vive en [DemoPilot](source/game/systems/demo_pilot.hpp) /
|
||||||
|
[demo_pilot.cpp](source/game/systems/demo_pilot.cpp). Su diseño es explícito en la
|
||||||
|
cabecera: busca **parecer humano, no ser óptimo**. Características clave:
|
||||||
|
|
||||||
|
- **Solo lectura**: `DemoPilot::compute(ship, enemies, bullets, play_area, dt)`
|
||||||
|
devuelve un `Control{left,right,thrust,shoot}`. No lee `Input` ni muta entidades;
|
||||||
|
GameScene aplica el resultado vía `Ship::applyMovement` + `fireBullet`.
|
||||||
|
- **Escenarios curados**: hay 4 (`SCENARIOS` en
|
||||||
|
[demo_pilot.hpp:36-42](source/game/systems/demo_pilot.hpp#L36-L42)): stages
|
||||||
|
`{5,8,6,10}` con 1 o 2 naves IA. El `SceneContext` recuerda el índice y rota al
|
||||||
|
siguiente en cada entrada al demo.
|
||||||
|
|
||||||
|
**Lógica de decisión por prioridad** (verificado en `demo_pilot.cpp`, con sus
|
||||||
|
constantes):
|
||||||
|
|
||||||
|
1. **Esquiva de bala** — si una bala enemiga entrante está dentro de
|
||||||
|
`DODGE_SCAN_RADIUS = 190 px` y viene hacia la nave (`DODGE_HEADING_MIN = 0.25`),
|
||||||
|
maniobra perpendicular a la bala con sesgo al centro (`WALL_BIAS = 0.6`); no
|
||||||
|
dispara mientras esquiva.
|
||||||
|
2. **Sin enemigos** — deriva tranquila (giro lento).
|
||||||
|
3. **Peligro cercano** — si el objetivo está a menos de `DANGER_RADIUS = 95 px`, se
|
||||||
|
aleja con sesgo al centro.
|
||||||
|
4. **Combate** — apuntado con *lead* (`LEAD_TIME = 0.30 s`) más un error humano
|
||||||
|
(`AIM_JITTER_MAX = 0.10 rad`); dispara si el error es menor que
|
||||||
|
`FIRE_TOLERANCE = 0.18 rad` y el cooldown (`FIRE_COOLDOWN = 0.32 s`) lo permite;
|
||||||
|
se acerca si está más lejos que `APPROACH_RADIUS = 250 px`.
|
||||||
|
|
||||||
|
Temporización "humana": reevalúa el objetivo cada `RETARGET_INTERVAL = 0.15 s` y usa
|
||||||
|
una zona muerta de rotación (`ROTATE_DEADZONE = 0.05 rad`) para no oscilar. La demo
|
||||||
|
se rompe con cualquier input (vuelve a TITLE) o por timeout/muerte (vuelve a LOGO),
|
||||||
|
gestionado en [stepDemo](source/game/scenes/game_scene.hpp#L157).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Efectos visuales
|
||||||
|
|
||||||
|
Viven en [game/effects/](source/game/effects/) y son managers con pools:
|
||||||
|
|
||||||
|
- **[DebrisManager](source/game/effects/debris_manager.hpp)** — rompe una shape en
|
||||||
|
fragmentos que vuelan radialmente, heredando inercia del cuerpo y, opcionalmente,
|
||||||
|
el impulso de la bala que causó la muerte. Notifica al `Border` (bump) y al
|
||||||
|
`Playfield` (ripple). Lo usan muerte de nave/enemigo, balas fuera de área y las
|
||||||
|
explosiones del logo.
|
||||||
|
- **[FireworkManager](source/game/effects/firework_manager.hpp)** — bursts de fuegos
|
||||||
|
artificiales.
|
||||||
|
- **[FloatingScoreManager](source/game/effects/floating_score_manager.hpp)** —
|
||||||
|
números de puntuación flotantes ("+150").
|
||||||
|
- **[TrailManager](source/game/effects/trail_manager.hpp)** — estela tras las naves.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Configuración, constantes y convenciones
|
||||||
|
|
||||||
|
**Configuración:**
|
||||||
|
|
||||||
|
- **[EngineConfig](source/core/config/engine_config.hpp)** — struct POD con
|
||||||
|
ventana, rendering, audio, bindings de jugadores, locale, console. Es la config
|
||||||
|
persistente (`config.yaml`), gestionada por
|
||||||
|
[config_yaml](source/game/config_yaml.hpp) (`ConfigYaml::engine_config`,
|
||||||
|
`loadFromFile`/`saveToFile`).
|
||||||
|
- **[PostFxConfig](source/core/config/postfx_config.hpp)** — carga los `PostFxParams`
|
||||||
|
(bloom/flicker/fondo) desde YAML.
|
||||||
|
- **[GameConfig::MatchConfig](source/core/system/game_config.hpp)** — config no
|
||||||
|
persistente de la partida (jugadores activos, modo NORMAL/DEMO).
|
||||||
|
|
||||||
|
**Constantes y tipos:**
|
||||||
|
|
||||||
|
- **[core/types.hpp](source/core/types.hpp)** — `Vec2` / `Vec3` (agregados con
|
||||||
|
operadores y helpers como `length()`, `normalized()`, `dot()`, `cross()`).
|
||||||
|
- **[core/defaults/](source/core/defaults/)** — un fichero por dominio
|
||||||
|
(`window.hpp`, `rendering.hpp`, `audio.hpp`, `entities.hpp`, `notifier.hpp`...)
|
||||||
|
con todas las constantes por defecto. `game/constants.hpp` reexporta varias como
|
||||||
|
alias (`MAX_ORNIS`, `MAX_BULLETS`, `PI`) y añade helpers de área de juego.
|
||||||
|
|
||||||
|
**Convenciones de código** (de `.clang-tidy`, confirmadas en memoria de proyecto):
|
||||||
|
|
||||||
|
- Métodos en `camelBack`, tipos en `CamelCase`, constantes en `UPPER_CASE`.
|
||||||
|
- Comentarios mayormente en **catalán** (algunos en castellano); el código y los
|
||||||
|
identificadores mezclan catalán/castellano/inglés.
|
||||||
|
- Patrón recurrente: **singletons** con `init/get/destroy` y comprobación de
|
||||||
|
`nullptr` para degradación elegante.
|
||||||
|
- Patrón recurrente: descomposición de funciones grandes (`update`/`draw`) en
|
||||||
|
sub-pasos privados (`stepX`/`runX`/`drawXState`) para mantener baja la complejidad
|
||||||
|
cognitiva.
|
||||||
|
- Análisis estático (cppcheck/clang-tidy) corre vía git hooks
|
||||||
|
([.githooks/](.githooks/)); la política es **arreglar la causa**, no suprimir el
|
||||||
|
diagnóstico.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Guía de navegación
|
||||||
|
|
||||||
|
| Si quieres tocar… | Mira… |
|
||||||
|
|---|---|
|
||||||
|
| El arranque, orden de init, o el bucle de frame | [director.cpp](source/core/system/director.cpp) (`Director::iterate` / `handleEvent`) |
|
||||||
|
| Las callbacks de SDL | [main.cpp](source/main.cpp) |
|
||||||
|
| Añadir/cambiar una escena o una transición | [scene.hpp](source/core/system/scene.hpp), [scene_context.hpp](source/core/system/scene_context.hpp), `Director::buildScene` |
|
||||||
|
| Cómo se dibuja una línea / el frame de render | [line_renderer.cpp](source/core/rendering/line_renderer.cpp) → [gpu_frame_renderer.cpp](source/core/rendering/gpu/gpu_frame_renderer.cpp) |
|
||||||
|
| Bloom / flicker / fondo (post-proceso) | [gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp), [gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp), shaders en [shaders/](shaders/) |
|
||||||
|
| Crear/editar una figura vectorial | `data/shapes/**/*.shp` + [shape_loader.hpp](source/core/graphics/shape_loader.hpp) |
|
||||||
|
| El texto en pantalla | [vector_text.hpp](source/core/graphics/vector_text.hpp) |
|
||||||
|
| Eventos globales (teclas F, ESC, hotplug) | [global_events.cpp](source/core/system/global_events.cpp) |
|
||||||
|
| Controles, bindings, rebinding | [input.cpp](source/core/input/input.cpp), [define_inputs.cpp](source/core/input/define_inputs.cpp) |
|
||||||
|
| Reproducir música/efectos | [audio.hpp](source/core/audio/audio.hpp), [audio_adapter.hpp](source/core/audio/audio_adapter.hpp) |
|
||||||
|
| Cómo se cargan los recursos / el pack | [resource_loader.cpp](source/core/resources/resource_loader.cpp), [resource_pack.cpp](source/core/resources/resource_pack.cpp) |
|
||||||
|
| Reglas de la partida, vidas, game over | [game_scene.cpp](source/game/scenes/game_scene.cpp) |
|
||||||
|
| Comportamiento de un enemigo | su YAML en `data/entities/<tipo>/` + [enemy_ai_system.cpp](source/game/systems/enemy_ai_system.cpp) |
|
||||||
|
| Definir oleadas / dificultad de un nivel | [data/stages/stages.yaml](data/stages/stages.yaml) + [stage_manager.cpp](source/game/stage_system/stage_manager.cpp) |
|
||||||
|
| Colisiones | [collision_system.cpp](source/game/systems/collision_system.cpp) |
|
||||||
|
| La IA del modo demo | [demo_pilot.cpp](source/game/systems/demo_pilot.cpp) |
|
||||||
|
| Explosiones / partículas | [debris_manager.cpp](source/game/effects/debris_manager.cpp) |
|
||||||
|
| El menú de servicio (F12) | [service_menu.cpp](source/core/system/service_menu.cpp) |
|
||||||
|
| Textos traducibles | `data/locale/*.yaml` + [locale.cpp](source/core/locale/locale.cpp) |
|
||||||
|
| Constantes por defecto | [core/defaults/](source/core/defaults/) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Notas de honestidad sobre la cobertura
|
||||||
|
|
||||||
|
- Todas las secciones se verificaron leyendo directamente los ficheros y firmas
|
||||||
|
citados, incluyendo el **pipeline de compilación de shaders**
|
||||||
|
([§5.5](#55-shaders-fuentes-compilación-y-selección): `CMakeLists.txt` +
|
||||||
|
`tools/shaders/compile_spirv.cmake` + `shader_factory.hpp`) y el interior de la
|
||||||
|
**física** ([§10.6](#106-dos-capas-de-colisión-física-vs-gameplay):
|
||||||
|
`physics_world.cpp` + `collision.hpp` + `rigid_body.hpp`).
|
||||||
|
- Lo que **no** se ha trazado a fondo y queda como lectura directa del código si hace
|
||||||
|
falta: los detalles finos de animación de cada overlay (curvas de easing del
|
||||||
|
`Notifier`/`ServiceMenu`) y la coreografía interna completa de `LogoScene` y
|
||||||
|
`TitleScene` (más allá de sus estados). Son descriptivos, no estructurales.
|
||||||
|
</content>
|
||||||
|
</invoke>
|
||||||
@@ -84,9 +84,18 @@ else
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: all debug release _windows-release _macos-release _linux-release \
|
.PHONY: all debug release _windows-release _macos-release _linux-release \
|
||||||
run run-debug clean rebuild show-version pack \
|
run run-debug clean rebuild show-version pack controllerdb \
|
||||||
format format-check tidy tidy-fix cppcheck hooks-install help
|
format format-check tidy tidy-fix cppcheck hooks-install help
|
||||||
|
|
||||||
|
# Còpia del gamecontrollerdb.txt (si existeix) al directori de build, perquè
|
||||||
|
# director.cpp el resolgui via resource_base = directori de l'executable.
|
||||||
|
# Silenciós si el fitxer no existeix (l'usuari encara no ha fet `make controllerdb`).
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
CP_CONTROLLERDB = @powershell -Command "if (Test-Path 'gamecontrollerdb.txt') { Copy-Item 'gamecontrollerdb.txt' -Destination '$(BUILDDIR)' -Force }"
|
||||||
|
else
|
||||||
|
CP_CONTROLLERDB = @if [ -f gamecontrollerdb.txt ]; then cp gamecontrollerdb.txt $(BUILDDIR)/; fi
|
||||||
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓ
|
# COMPILACIÓ
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -98,10 +107,12 @@ endif
|
|||||||
all:
|
all:
|
||||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
$(CP_CONTROLLERDB)
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
$(CP_CONTROLLERDB)
|
||||||
|
|
||||||
run: all
|
run: all
|
||||||
@./$(BUILDDIR)/$(PROJECT)
|
@./$(BUILDDIR)/$(PROJECT)
|
||||||
@@ -138,6 +149,7 @@ _linux-release:
|
|||||||
|
|
||||||
# Còpia de fitxers
|
# Còpia de fitxers
|
||||||
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
|
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||||
cp README.md "$(RELEASE_FOLDER)"
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
|
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
||||||
@@ -166,6 +178,7 @@ _windows-release:
|
|||||||
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
||||||
|
|
||||||
@powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||||
|
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
|
@powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
|
||||||
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }"
|
@powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }"
|
||||||
@@ -189,7 +202,7 @@ _macos-release:
|
|||||||
|
|
||||||
# Compila la versió Apple Silicon
|
# Compila la versió Apple Silicon
|
||||||
@cmake -S . -B $(BUILDDIR)/arm $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release \
|
@cmake -S . -B $(BUILDDIR)/arm $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 \
|
||||||
-DMACOS_BUNDLE=ON $(CMAKE_DEFS)
|
-DMACOS_BUNDLE=ON $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR)/arm -j$(JOBS)
|
@cmake --build $(BUILDDIR)/arm -j$(JOBS)
|
||||||
|
|
||||||
@@ -206,6 +219,7 @@ _macos-release:
|
|||||||
|
|
||||||
# Còpia de recursos i metadades del bundle
|
# Còpia de recursos i metadades del bundle
|
||||||
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||||
cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
||||||
@@ -274,6 +288,19 @@ pack:
|
|||||||
@cmake --build $(BUILDDIR) --target pack_resources
|
@cmake --build $(BUILDDIR) --target pack_resources
|
||||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# DESCÀRREGA DE GAMECONTROLLERDB
|
||||||
|
# ==============================================================================
|
||||||
|
# Descarrega l'última versió de gamecontrollerdb.txt (mappings de gamepads
|
||||||
|
# mantinguts per la comunitat) a l'arrel del projecte. SDL el carrega via
|
||||||
|
# filesystem real (no dins resources.pack) i s'ha de copiar al costat del binari
|
||||||
|
# en cada build (gestionat per CP_CONTROLLERDB a `all`/`debug` i pels release targets).
|
||||||
|
controllerdb:
|
||||||
|
@echo "Descargando gamecontrollerdb.txt..."
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
|
||||||
|
-o gamecontrollerdb.txt
|
||||||
|
@echo "gamecontrollerdb.txt actualizado"
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CODE QUALITY (delegats a cmake)
|
# CODE QUALITY (delegats a cmake)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@@ -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/basic.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,21 @@
|
|||||||
|
name: bullet_double
|
||||||
|
|
||||||
|
# Variant de bala "anular" (dos cercles concèntrics, aspecte d'aura de plasma).
|
||||||
|
# Pensada per a contra-atacs d'enemic (ex: orb dispara una bullet_double al
|
||||||
|
# jugador quan rep un impacte). Mateixa física que la bala bàsica del player;
|
||||||
|
# canvien la forma (cercle doble) i el color per llegir-se com a tret enemic
|
||||||
|
# distintiu (groc verdós vs. el verd laser del player o el roig de bullet_long).
|
||||||
|
shape:
|
||||||
|
path: bullet/double.shp
|
||||||
|
scale: 1.5
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 0.5
|
||||||
|
restitution: 0.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
impact_momentum_factor: 4.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [200, 255, 80] # groc verdós (chartreuse) — contra-atac de l'orb
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: bullet_long
|
||||||
|
|
||||||
|
# Variant de bala més llarga, pensada per a bales d'enemic: més visible per al
|
||||||
|
# jugador i amb prou marge per reaccionar. La velocitat NO viu aquí: es passa
|
||||||
|
# a Bullet::fire() i la decideix qui dispara (l'AiTickAction).
|
||||||
|
shape:
|
||||||
|
path: bullet/long.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 0.5
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 0.5
|
||||||
|
restitution: 0.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
impact_momentum_factor: 3.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 100, 100] # roig clar — diferencia visualment del verd laser del player
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
name: orb
|
||||||
|
ai_type: orb # Validat contra el directori; mapeja a EnemyType::ORB.
|
||||||
|
|
||||||
|
# Shape circular pròpia (anell exterior + anell interior + 6 radis + nucli),
|
||||||
|
# pensada per llegir-se com a "reactor / orb" amb més detall que els enemics
|
||||||
|
# petits.
|
||||||
|
shape:
|
||||||
|
path: enemy/orb.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 50.0 # Molt pesat: una bala el frena un poc però no el "envia a passejar".
|
||||||
|
speed: 50.0 # Avança decidit cap al ship (no és lent passiu, és amenaça constant).
|
||||||
|
rotation_delta_min: 0.3
|
||||||
|
rotation_delta_max: 1.5
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
ai:
|
||||||
|
# Persecució contínua del ship més proper. chase_strength alt (1.0 = ~1s
|
||||||
|
# per realinear-se) perquè, encara que una bala l'empentja lateralment,
|
||||||
|
# ràpidament torna a posar la seua proa cap al jugador.
|
||||||
|
movement:
|
||||||
|
type: chase
|
||||||
|
chase_strength: 1.0
|
||||||
|
|
||||||
|
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.5 # Una mica més llarg que els altres (és un boss).
|
||||||
|
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: 54.0 # 1.5× del normal (alineat amb scale 1.5).
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 140, 110] # taronja rosat (coral) — distintiu del boss orb.
|
||||||
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
|
score: 500 # 5x un enemic normal: aguanta 10x més.
|
||||||
|
|
||||||
|
# Estrenant el sistema HP: 10 unitats. Cada bala fa decrease_health + flash
|
||||||
|
# + create_debris_partial (xip a 0.3x) + create_fireworks_small (espurna).
|
||||||
|
# Al 10è hit (HP=0), on_no_health encadena destroy directe — sense passar
|
||||||
|
# per wounded (com Star). 10 HP ja és prou dificultat sense afegir un hit
|
||||||
|
# extra.
|
||||||
|
health: 10
|
||||||
|
|
||||||
|
events:
|
||||||
|
on_hit:
|
||||||
|
- action: fire_bullet # contra-atac: dispara bullet_double dirigida al jugador
|
||||||
|
bullet: bullet_double
|
||||||
|
bullet_speed: 200.0
|
||||||
|
aim_mode: aimed
|
||||||
|
- action: decrease_health # primer: si arriba a 0 dispara on_no_health
|
||||||
|
#- action: flash # feedback visual de damage parcial
|
||||||
|
- action: create_debris_partial # xip a 0.3x mida (sense ser letal)
|
||||||
|
#- action: create_fireworks_small # espurna a cada hit (12 punts, lent)
|
||||||
|
- action: apply_impulse # empenta el cos (skip si will_die)
|
||||||
|
on_no_health:
|
||||||
|
- action: destroy # mort directa, sense wounded
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris # explosió completa
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
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:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
|
# decrease_health primer perquè si la mort cau aquí (segon hit durant wounded),
|
||||||
|
# el dispatcher salta la resta del chain (incloent apply_impulse) sobre el
|
||||||
|
# cos ja destruït.
|
||||||
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
|
- action: set_hurt
|
||||||
|
on_hurt_end:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
|
- 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 ("ship/wedge.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/arrow.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, 0, 0] # roig pur (estat ferit)
|
||||||
|
|
||||||
|
weapon:
|
||||||
|
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
ai:
|
||||||
|
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
|
||||||
|
movement:
|
||||||
|
type: chase
|
||||||
|
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
|
||||||
|
|
||||||
|
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:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
|
- action: set_hurt
|
||||||
|
on_hurt_end:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
name: star
|
||||||
|
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: enemy/star.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
|
||||||
|
|
||||||
|
ai:
|
||||||
|
# Movement: zigzag esquivador (com Pentagon).
|
||||||
|
movement:
|
||||||
|
type: zigzag
|
||||||
|
angle_change_max: 1.0
|
||||||
|
zigzag_prob_per_second: 0.8
|
||||||
|
# Accions periòdiques: cada ~2.5s dispara una bala apuntada al ship més proper.
|
||||||
|
tick:
|
||||||
|
- action: shoot
|
||||||
|
interval: 2.5
|
||||||
|
aim_mode: aimed # apunta al ship més proper (atan2)
|
||||||
|
jitter_rad: 0.0 # sense soroll: tret perfecte
|
||||||
|
bullet: bullet_long # variant més visible per al jugador
|
||||||
|
bullet_speed: 150.0 # px/s — prou lenta per reaccionar
|
||||||
|
|
||||||
|
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.
|
||||||
|
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
|
||||||
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
|
on_no_health:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# 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"
|
||||||
|
screenshot: "IMATGE {file} GUARDADA A {folder}"
|
||||||
|
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"
|
||||||
|
|
||||||
|
demo:
|
||||||
|
banner: "MODE DEMO - PREMEU START"
|
||||||
|
|
||||||
|
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,115 @@
|
|||||||
|
# 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"
|
||||||
|
screenshot: "IMAGE {file} SAVED AT {folder}"
|
||||||
|
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"
|
||||||
|
|
||||||
|
demo:
|
||||||
|
banner: "DEMO MODE - PRESS START"
|
||||||
|
|
||||||
|
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,23 +0,0 @@
|
|||||||
# bullet.shp - Projectil (petit pentàgon)
|
|
||||||
# © 1999 Visente i Sergi (versió Pascal)
|
|
||||||
# © 2025 Port a C++20 amb SDL3
|
|
||||||
|
|
||||||
name: bullet
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# Cercle (octàgon regular radi=3)
|
|
||||||
# 8 punts equidistants (45° entre ells) per aproximar un cercle
|
|
||||||
# Començant a angle=-90° (amunt), rotant sentit horari
|
|
||||||
#
|
|
||||||
# Conversió polar→cartesià (radi=3, SDL: Y creix cap avall):
|
|
||||||
# angle=-90°: (0.00, -3.00)
|
|
||||||
# angle=-45°: (2.12, -2.12)
|
|
||||||
# angle=0°: (3.00, 0.00)
|
|
||||||
# angle=45°: (2.12, 2.12)
|
|
||||||
# angle=90°: (0.00, 3.00)
|
|
||||||
# angle=135°: (-2.12, 2.12)
|
|
||||||
# angle=180°: (-3.00, 0.00)
|
|
||||||
# angle=225°: (-2.12, -2.12)
|
|
||||||
|
|
||||||
polyline: 0,-3 2.12,-2.12 3,0 2.12,2.12 0,3 -2.12,2.12 -3,0 -2.12,-2.12 0,-3
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# bullet/basic.shp - Projectil (octàgon, radi=3)
|
||||||
|
|
||||||
|
name: basic
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
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: 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,32 @@
|
|||||||
|
# bullet/long.shp - Bala allargada vertical (dos mig-octàgons + dos costats)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Càpsula orientada al llarg de l'eix Y: la bala viatja segons el seu angle
|
||||||
|
# de moviment (angle=0 = Y negatiu), i així s'estira en la direcció de vol.
|
||||||
|
# Es dibuixen només els segments exteriors per evitar veure la unió interna
|
||||||
|
# dels dos cercles; el resultat visual són dos "mig-octàgons" separats per
|
||||||
|
# un petit gap al centre, units pels dos costats verticals.
|
||||||
|
#
|
||||||
|
# Geometria:
|
||||||
|
# Mig-octàgon superior (radi 3) centrat a (0, -3)
|
||||||
|
# Mig-octàgon inferior (radi 3) centrat a (0, 3)
|
||||||
|
# Punt extrem superior: (0, -6)
|
||||||
|
# Punt extrem inferior: (0, 6)
|
||||||
|
# Bounding radius natiu = 6 (extrem vertical a y=±6).
|
||||||
|
# collision_factor al YAML compensa el bounding doble (0.5 → hitbox ≈ 3).
|
||||||
|
|
||||||
|
name: long
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Mig-octàgon superior (5 vèrtexs: del cantó dret cap al punt extrem i a l'esquerre)
|
||||||
|
polyline: 3,-3 2.12,-5.12 0,-6 -2.12,-5.12 -3,-3
|
||||||
|
|
||||||
|
# Mig-octàgon inferior
|
||||||
|
polyline: 3,3 2.12,5.12 0,6 -2.12,5.12 -3,3
|
||||||
|
|
||||||
|
# Costat dret (uneix extrem inferior del mig superior amb extrem superior del mig inferior)
|
||||||
|
polyline: 3,-3 3,3
|
||||||
|
|
||||||
|
# Costat esquerre
|
||||||
|
polyline: -3,-3 -3,3
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# star.shp - Estrella per a starfield
|
# effect/starfield.shp - Estrella per a starfield
|
||||||
# © 2025 Orni Attack
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: star
|
name: starfield
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# effect/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
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# enemy/orb.shp - ORNI enemic gegant (orb circular, doble anell amb radis)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Forma "reactor / boss circular" — més detall que els enemics petits perquè
|
||||||
|
# es renderitza a escala 1.5x i ha de llegir-se com a amenaça gran.
|
||||||
|
# - Anell exterior: dodecàgon (12 vèrtexs) — aparença circular suau, radi 20.
|
||||||
|
# - Anell interior: hexàgon (6 vèrtexs, rotat 30°) — radi 10.
|
||||||
|
# - 6 radis curts que connecten l'anell interior amb l'exterior.
|
||||||
|
# - Petit "+" central com a nucli.
|
||||||
|
# Bounding radius natiu = 20 (alineat amb la resta d'enemics).
|
||||||
|
|
||||||
|
name: orb
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Anell exterior (dodecàgon, vèrtex apuntant amunt)
|
||||||
|
polyline: 0,-20 10,-17.32 17.32,-10 20,0 17.32,10 10,17.32 0,20 -10,17.32 -17.32,10 -20,0 -17.32,-10 -10,-17.32 0,-20
|
||||||
|
|
||||||
|
# Anell interior (hexàgon, vèrtex apuntant a la dreta — rotat 30° respecte l'exterior)
|
||||||
|
polyline: 5,-8.66 10,0 5,8.66 -5,8.66 -10,0 -5,-8.66 5,-8.66
|
||||||
|
|
||||||
|
# 6 radis: del vèrtex de l'hexàgon interior al vèrtex corresponent del dodecàgon exterior
|
||||||
|
line: 5,-8.66 10,-17.32
|
||||||
|
line: 10,0 20,0
|
||||||
|
line: 5,8.66 10,17.32
|
||||||
|
line: -5,8.66 -10,17.32
|
||||||
|
line: -10,0 -20,0
|
||||||
|
line: -5,-8.66 -10,-17.32
|
||||||
|
|
||||||
|
# Nucli central: petit "+" (2 segments creuats, radi 3)
|
||||||
|
line: -3,0 3,0
|
||||||
|
line: 0,-3 0,3
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# enemy/pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
|
||||||
|
|
||||||
|
name: pentagon
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Pentàgon exterior (vèrtex apuntant amunt, radi 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,7 +1,7 @@
|
|||||||
# 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: pinwheel
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# enemy/square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
|
||||||
|
|
||||||
|
name: square
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Rombe exterior
|
||||||
|
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,15 @@
|
|||||||
|
# enemy/star.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
|
||||||
|
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
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# enemy_pentagon.shp - ORNI enemic (pentàgon regular)
|
|
||||||
# © 1999 Visente i Sergi (versió Pascal)
|
|
||||||
# © 2025 Port a C++20 amb SDL3
|
|
||||||
|
|
||||||
name: enemy_pentagon
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# Pentàgon regular radi=20
|
|
||||||
# 5 punts equidistants al voltant d'un cercle (72° entre ells)
|
|
||||||
# Començant a angle=-90° (amunt), rotant sentit antihorari
|
|
||||||
#
|
|
||||||
# Angles: -90°, -18°, 54°, 126°, 198°
|
|
||||||
# Conversió polar→cartesià (SDL: Y creix cap avall):
|
|
||||||
# angle=-90°: (0.00, -20.00)
|
|
||||||
# angle=-18°: (19.02, -6.18)
|
|
||||||
# angle=54°: (11.76, 16.18)
|
|
||||||
# angle=126°: (-11.76, 16.18)
|
|
||||||
# angle=198°: (-19.02, -6.18)
|
|
||||||
|
|
||||||
polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# enemy_square.shp - ORNI enemic (quadrat regular)
|
|
||||||
# © 2025 Port a C++20 amb SDL3
|
|
||||||
|
|
||||||
name: enemy_square
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# Quadrat regular radi=20 (circumscrit)
|
|
||||||
# 4 punts equidistants al voltant d'un cercle (90° entre ells)
|
|
||||||
# Començant a angle=-90° (amunt), rotant sentit horari
|
|
||||||
#
|
|
||||||
# Angles: -90°, 0°, 90°, 180°
|
|
||||||
# Conversió polar→cartesià (SDL: Y creix cap avall):
|
|
||||||
# angle=-90°: (0.00, -20.00)
|
|
||||||
# angle=0°: (20.00, 0.00)
|
|
||||||
# angle=90°: (0.00, 20.00)
|
|
||||||
# angle=180°: (-20.00, 0.00)
|
|
||||||
|
|
||||||
polyline: 0,-20 20,0 0,20 -20,0 0,-20
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# char_underscore.shp - Símbolo _ (barra baja)
|
||||||
|
# Dimensiones: 20×40 (blocky display)
|
||||||
|
|
||||||
|
name: char_underscore
|
||||||
|
scale: 1.0
|
||||||
|
center: 10, 20
|
||||||
|
|
||||||
|
# Línea horizontal abajo (bajo la baseline de las letras)
|
||||||
|
line: 3,33 17,33
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# ship.shp - Nau del jugador 1 (triangle amb base còncava - punta de fletxa)
|
|
||||||
# © 1999 Visente i Sergi (versió Pascal)
|
|
||||||
# © 2025 Port a C++20 amb SDL3
|
|
||||||
|
|
||||||
name: ship
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# Triangle amb base còncava tipus "punta de fletxa"
|
|
||||||
# Punts originals (polar):
|
|
||||||
# p1: r=12, angle=270° (3π/2) → punta amunt
|
|
||||||
# p2: r=12, angle=45° (π/4) → base dreta-darrere
|
|
||||||
# p3: r=12, angle=135° (3π/4) → base esquerra-darrere
|
|
||||||
#
|
|
||||||
# MODIFICACIÓ: afegit p4 al mig de la base, desplaçat cap al centre
|
|
||||||
# p4: (0, 4) → punt central de la base, cap endins
|
|
||||||
#
|
|
||||||
# Conversió polar→cartesià (angle-90° perquè origen visual és amunt):
|
|
||||||
# p1: (0, -12) → punta
|
|
||||||
# p2: (8.49, 8.49) → base dreta
|
|
||||||
# p4: (0, 4) → base centre (cap endins)
|
|
||||||
# p3: (-8.49, 8.49) → base esquerra
|
|
||||||
|
|
||||||
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# ship/arrow.shp - Nau del jugador 1 (triangle amb base còncava, punta de fletxa)
|
||||||
|
|
||||||
|
name: arrow
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
|
# ship/interceptor.shp - Interceptor amb ales laterals pronunciades
|
||||||
# © 2025 Orni Attack - Jugador 2
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: ship2
|
name: interceptor
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# ship/wedge.shp - Nau del jugador 2 (triangle amb cercle central)
|
||||||
|
|
||||||
|
name: wedge
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
|
||||||
|
|
||||||
|
# Octàgon central (radi=2.5)
|
||||||
|
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# ship2.shp - Nau del jugador 2 (triangle amb circulito central)
|
|
||||||
# © 1999 Visente i Sergi (versió Pascal)
|
|
||||||
# © 2025 Port a C++20 amb SDL3
|
|
||||||
|
|
||||||
name: ship2
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# Triangle amb base còncava tipus "punta de fletxa"
|
|
||||||
# (Mateix que ship.shp)
|
|
||||||
# Punts originals (polar):
|
|
||||||
# p1: r=12, angle=270° (3π/2) → punta amunt
|
|
||||||
# p2: r=12, angle=45° (π/4) → base dreta-darrere
|
|
||||||
# p3: r=12, angle=135° (3π/4) → base esquerra-darrere
|
|
||||||
#
|
|
||||||
# MODIFICACIÓ: afegit p4 al mig de la base, desplaçat cap al centre
|
|
||||||
# p4: (0, 4) → punt central de la base, cap endins
|
|
||||||
#
|
|
||||||
# Conversió polar→cartesià (angle-90° perquè origen visual és amunt):
|
|
||||||
# p1: (0, -12) → punta
|
|
||||||
# p2: (8.49, 8.49) → base dreta
|
|
||||||
# p4: (0, 4) → base centre (cap endins)
|
|
||||||
# p3: (-8.49, 8.49) → base esquerra
|
|
||||||
|
|
||||||
#polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
|
||||||
polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
|
|
||||||
|
|
||||||
# Circulito central (octàgon r=2.5)
|
|
||||||
# Distintiu visual del jugador 2
|
|
||||||
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
|
|
||||||
@@ -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,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
|
|
||||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+175
-160
@@ -1,168 +1,183 @@
|
|||||||
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
|
# stages.yaml - Configuració de les fases d'Orni Attack
|
||||||
# © 2025 Orni Attack
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Format basat en onades (waves). Cada wave:
|
||||||
|
# - spawn: list d'enemics a generar, en ordre.
|
||||||
|
# - spawn_interval: segons entre spawns interns (default 0 = simultanis).
|
||||||
|
# - next: condició per avançar a la wave següent.
|
||||||
|
# - "all_dead" / "end" → quan tots els enemics de l'arena han mort.
|
||||||
|
# - { timeout: T } → quan han passat T segons des de l'inici de la wave.
|
||||||
|
# - { all_dead: true, timeout: T } → el que arribe abans (amuntegament si vas lent).
|
||||||
|
#
|
||||||
|
# Tipus d'enemic: pentagon, square (alias: cuadrado), pinwheel (alias: molinillo), star, orb.
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
version: "1.0"
|
version: "2.0"
|
||||||
total_stages: 10
|
total_stages: 10
|
||||||
description: "Progressive difficulty curve from novice to expert"
|
description: "Wave-based progression"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
# STAGE 1: Tutorial - Only pentagons, slow speed
|
# STAGE 1 — Tutorial: contacte amb pentagons i un cuadrado.
|
||||||
|
# (Test: també hi ha un orb a la primera onada per provar el contra-atac.)
|
||||||
- stage_id: 1
|
- stage_id: 1
|
||||||
total_enemies: 5
|
multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, orb]
|
||||||
initial_delay: 2.0
|
|
||||||
spawn_interval: 3.0
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 100
|
|
||||||
quadrat: 0
|
|
||||||
molinillo: 0
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 0.7
|
|
||||||
rotation_multiplier: 0.8
|
|
||||||
tracking_strength: 0.0
|
|
||||||
|
|
||||||
# STAGE 2: Introduction to tracking enemies
|
|
||||||
- stage_id: 2
|
|
||||||
total_enemies: 7
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 1.5
|
|
||||||
spawn_interval: 2.5
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 70
|
|
||||||
quadrat: 30
|
|
||||||
molinillo: 0
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 0.85
|
|
||||||
rotation_multiplier: 0.9
|
|
||||||
tracking_strength: 0.3
|
|
||||||
|
|
||||||
# STAGE 3: All enemy types, normal speed
|
|
||||||
- stage_id: 3
|
|
||||||
total_enemies: 10
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 1.0
|
|
||||||
spawn_interval: 2.0
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 50
|
|
||||||
quadrat: 30
|
|
||||||
molinillo: 20
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.0
|
|
||||||
rotation_multiplier: 1.0
|
|
||||||
tracking_strength: 0.5
|
|
||||||
|
|
||||||
# STAGE 4: Increased count, faster enemies
|
|
||||||
- stage_id: 4
|
|
||||||
total_enemies: 12
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 0.8
|
|
||||||
spawn_interval: 1.8
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 40
|
|
||||||
quadrat: 35
|
|
||||||
molinillo: 25
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.1
|
|
||||||
rotation_multiplier: 1.15
|
|
||||||
tracking_strength: 0.6
|
|
||||||
|
|
||||||
# STAGE 5: Maximum count reached
|
|
||||||
- stage_id: 5
|
|
||||||
total_enemies: 15
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 0.5
|
|
||||||
spawn_interval: 1.5
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 35
|
|
||||||
quadrat: 35
|
|
||||||
molinillo: 30
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.2
|
|
||||||
rotation_multiplier: 1.25
|
|
||||||
tracking_strength: 0.7
|
|
||||||
|
|
||||||
# STAGE 6: Molinillo becomes dominant
|
|
||||||
- stage_id: 6
|
|
||||||
total_enemies: 15
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 0.3
|
|
||||||
spawn_interval: 1.3
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 30
|
|
||||||
quadrat: 30
|
|
||||||
molinillo: 40
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.3
|
|
||||||
rotation_multiplier: 1.4
|
|
||||||
tracking_strength: 0.8
|
|
||||||
|
|
||||||
# STAGE 7: High intensity, fast spawns
|
|
||||||
- stage_id: 7
|
|
||||||
total_enemies: 15
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 0.2
|
|
||||||
spawn_interval: 1.0
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 25
|
|
||||||
quadrat: 30
|
|
||||||
molinillo: 45
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.4
|
|
||||||
rotation_multiplier: 1.5
|
|
||||||
tracking_strength: 0.9
|
|
||||||
|
|
||||||
# STAGE 8: Expert level, 50% molinillos
|
|
||||||
- stage_id: 8
|
|
||||||
total_enemies: 15
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 0.1
|
|
||||||
spawn_interval: 0.8
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 20
|
|
||||||
quadrat: 30
|
|
||||||
molinillo: 50
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.5
|
|
||||||
rotation_multiplier: 1.6
|
|
||||||
tracking_strength: 1.0
|
|
||||||
|
|
||||||
# STAGE 9: Near-maximum difficulty
|
|
||||||
- stage_id: 9
|
|
||||||
total_enemies: 15
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 0.0
|
|
||||||
spawn_interval: 0.6
|
spawn_interval: 0.6
|
||||||
enemy_distribution:
|
next: all_dead
|
||||||
pentagon: 15
|
- spawn: [pentagon, pentagon, square]
|
||||||
quadrat: 25
|
|
||||||
molinillo: 60
|
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.6
|
|
||||||
rotation_multiplier: 1.7
|
|
||||||
tracking_strength: 1.1
|
|
||||||
|
|
||||||
# STAGE 10: Final challenge, 70% molinillos
|
|
||||||
- stage_id: 10
|
|
||||||
total_enemies: 15
|
|
||||||
spawn_config:
|
|
||||||
mode: "progressive"
|
|
||||||
initial_delay: 0.0
|
|
||||||
spawn_interval: 0.5
|
spawn_interval: 0.5
|
||||||
enemy_distribution:
|
next: all_dead
|
||||||
pentagon: 10
|
- spawn: [pentagon, pentagon, square, square]
|
||||||
quadrat: 20
|
spawn_interval: 0.4
|
||||||
molinillo: 70
|
next: end
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.8
|
# STAGE 2 — Apareixen molinillos.
|
||||||
rotation_multiplier: 2.0
|
- stage_id: 2
|
||||||
tracking_strength: 1.2
|
multipliers: { velocity: 0.95, rotation: 1.0, tracking: 0.4 }
|
||||||
|
waves:
|
||||||
|
- spawn: [pentagon, pentagon, pentagon]
|
||||||
|
spawn_interval: 0.5
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [pinwheel]
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [pentagon, square, pinwheel]
|
||||||
|
spawn_interval: 0.6
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [pinwheel, pinwheel, pentagon]
|
||||||
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
|
# STAGE 3 — Primer orb (HP=10).
|
||||||
|
- stage_id: 3
|
||||||
|
multipliers: { velocity: 1.0, rotation: 1.0, tracking: 0.5 }
|
||||||
|
waves:
|
||||||
|
- spawn: [pentagon, pentagon, square]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [orb]
|
||||||
|
next: { all_dead: true, timeout: 12.0 }
|
||||||
|
- spawn: [pinwheel, pinwheel]
|
||||||
|
spawn_interval: 0.5
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [pentagon, square, pinwheel, pinwheel]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|
||||||
|
# STAGE 4 — Pressió creixent: timeouts curts que poden encavalcar onades.
|
||||||
|
- stage_id: 4
|
||||||
|
multipliers: { velocity: 1.05, rotation: 1.1, tracking: 0.6 }
|
||||||
|
waves:
|
||||||
|
- spawn: [pentagon, pentagon, pentagon]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
|
- spawn: [square, square]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [orb, pentagon, pentagon]
|
||||||
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
|
# STAGE 5 — Apareix la star (zigzag clon del pentagon).
|
||||||
|
- stage_id: 5
|
||||||
|
multipliers: { velocity: 1.1, rotation: 1.2, tracking: 0.7 }
|
||||||
|
waves:
|
||||||
|
- spawn: [star, star]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [pentagon, square, star]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
|
- spawn: [pinwheel, pinwheel, star, star]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [orb, square, square]
|
||||||
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
|
# STAGE 6 — Densitat alta, mix amb timeouts agressius.
|
||||||
|
- stage_id: 6
|
||||||
|
multipliers: { velocity: 1.15, rotation: 1.25, tracking: 0.8 }
|
||||||
|
waves:
|
||||||
|
- spawn: [pentagon, pinwheel, pentagon, pinwheel]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
|
- spawn: [square, square, star]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [orb, pinwheel, pinwheel]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|
||||||
|
# STAGE 7 — Tiradors i agressivitat.
|
||||||
|
- stage_id: 7
|
||||||
|
multipliers: { velocity: 1.25, rotation: 1.35, tracking: 0.9 }
|
||||||
|
waves:
|
||||||
|
- spawn: [square, square, square]
|
||||||
|
spawn_interval: 0.5
|
||||||
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
|
- spawn: [pinwheel, pinwheel, pentagon, pentagon]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
|
- spawn: [star, star, star]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: all_dead
|
||||||
|
- spawn: [orb, pinwheel, pinwheel, square]
|
||||||
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
|
# STAGE 8 — Pressió constant.
|
||||||
|
- stage_id: 8
|
||||||
|
multipliers: { velocity: 1.35, rotation: 1.45, tracking: 1.0 }
|
||||||
|
waves:
|
||||||
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
|
- spawn: [square, square, star, star]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
|
- spawn: [orb]
|
||||||
|
next: { all_dead: true, timeout: 8.0 }
|
||||||
|
- spawn: [pinwheel, pinwheel, square, star, pentagon]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: end
|
||||||
|
|
||||||
|
# STAGE 9 — Quasi-final.
|
||||||
|
- stage_id: 9
|
||||||
|
multipliers: { velocity: 1.5, rotation: 1.6, tracking: 1.1 }
|
||||||
|
waves:
|
||||||
|
- spawn: [pinwheel, pinwheel, star, star]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
|
- spawn: [orb, square, square]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: { all_dead: true, timeout: 8.0 }
|
||||||
|
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
|
- spawn: [orb, pinwheel, pinwheel, square, star]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|
||||||
|
# STAGE 10 — Repte final.
|
||||||
|
- stage_id: 10
|
||||||
|
multipliers: { velocity: 1.7, rotation: 1.8, tracking: 1.2 }
|
||||||
|
waves:
|
||||||
|
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
|
||||||
|
spawn_interval: 0.25
|
||||||
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
|
- spawn: [orb, square, star]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
|
- spawn: [pinwheel, pinwheel, star, star, square]
|
||||||
|
spawn_interval: 0.3
|
||||||
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
|
- spawn: [orb, orb, pinwheel, pinwheel, star]
|
||||||
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 361 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 537 KiB |
@@ -29,11 +29,11 @@
|
|||||||
<key>CSResourcesFileMapped</key>
|
<key>CSResourcesFileMapped</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.15</string>
|
<string>13.3</string>
|
||||||
<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: '-*'
|
|
||||||
+235
-113
@@ -1,183 +1,305 @@
|
|||||||
#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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Silencia o restaura un grup de sons concret sense alterar config_ (el volum
|
||||||
|
// que l'usuari va triar) ni els altres grups. Silenciar posa la ganancia del
|
||||||
|
// grup a 0; restaurar-la torna al volum efectiu normal (que ja aplica els gates
|
||||||
|
// master/sound i el volum de l'usuari). A diferència de setSoundVolume, no
|
||||||
|
// xafa config_.sound_volume, así que el menu de servei segueix mostrant i
|
||||||
|
// operant el volum real durant la demo.
|
||||||
|
void Audio::silenceGroup(Group group, bool silenced) {
|
||||||
|
const float VOL = silenced ? 0.0F : effectiveVolume(config_.sound_volume, sound_enabled_);
|
||||||
|
engine_->setSoundVolume(VOL, static_cast<int>(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
+138
-59
@@ -1,97 +1,176 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath> // Para std::lround
|
||||||
|
#include <cstdint> // Para int8_t, uint8_t
|
||||||
|
#include <functional> // Para std::function
|
||||||
|
#include <memory> // Para std::unique_ptr
|
||||||
#include <string> // Para string
|
#include <string> // Para string
|
||||||
#include <utility> // Para move
|
|
||||||
|
|
||||||
// --- Clase Audio: gestor de audio (singleton) ---
|
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
|
||||||
|
// tres símbols (Music/Sound para el punter que exposa la API i Engine per al
|
||||||
|
// std::unique_ptr<Engine> membre) s'usen solo per punter al header, así que
|
||||||
|
// el forward-decl basta. El ~Audio() en .cpp veu la definició completa i
|
||||||
|
// instancia correctament el dtor de l'unique_ptr.
|
||||||
|
namespace Ja {
|
||||||
|
class Engine;
|
||||||
|
struct Music;
|
||||||
|
struct Sound;
|
||||||
|
} // namespace Ja
|
||||||
|
|
||||||
|
// --- Clase Audio: gestor d'àudio (singleton) ---
|
||||||
|
// Port del subsistema d'àudio del projecte ../aee, desacoblat d'Options:
|
||||||
|
// la configuración entra per la struct Audio::Config a init()/applySettings(),
|
||||||
|
// en lloc de llegir directament 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:
|
||||||
|
// --- Configuración injectada (Options la construeix via buildAudioConfig) ---
|
||||||
|
struct Config {
|
||||||
|
bool enabled{true};
|
||||||
|
float volume{1.0F}; // Master 0..1
|
||||||
|
bool music_enabled{true};
|
||||||
|
float music_volume{0.8F};
|
||||||
|
bool sound_enabled{true};
|
||||||
|
float sound_volume{1.0F};
|
||||||
|
};
|
||||||
|
|
||||||
// --- Enums ---
|
// --- Enums ---
|
||||||
enum class Group : int {
|
enum class Group : std::int8_t {
|
||||||
ALL = -1, // Todos los grupos
|
ALL = -1, // Tots los grups
|
||||||
GAME = 0, // Sonidos del juego
|
GAME = 0, // Sons del joc
|
||||||
INTERFACE = 1 // Sonidos de la interfaz
|
INTERFACE = 1 // Sons de la interfície
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class MusicState {
|
enum class MusicState : std::uint8_t {
|
||||||
PLAYING, // Reproduciendo música
|
PLAYING, // Reproduint música
|
||||||
PAUSED, // Música pausada
|
PAUSED, // Música pausada
|
||||||
STOPPED, // Música detenida
|
STOPPED, // Música aturada
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Constantes ---
|
// --- Constants ---
|
||||||
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo
|
static constexpr float MAX_VOLUME = 1.0F; // Volum màxim (float 0..1)
|
||||||
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo
|
static constexpr float MIN_VOLUME = 0.0F; // Volum mínim (float 0..1)
|
||||||
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
|
|
||||||
|
|
||||||
// --- Singleton ---
|
// --- Singleton ---
|
||||||
static void init(); // Inicializa el objeto Audio
|
static void init(const Config& config); // Inicialitza con la configuración rebuda
|
||||||
static void destroy(); // Libera el objeto Audio
|
static void destroy(); // Allibera l'objecte Audio
|
||||||
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
|
static auto get() -> Audio*; // Obté el punter a l'objecte Audio
|
||||||
Audio(const Audio&) = delete; // Evitar copia
|
~Audio(); // Destructor (públic para std::unique_ptr)
|
||||||
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
|
Audio(const Audio&) = delete; // Evitar còpia
|
||||||
|
Audio(Audio&&) = delete;
|
||||||
|
auto operator=(const Audio&) -> Audio& = delete; // Evitar assignació
|
||||||
|
auto operator=(Audio&&) -> Audio& = delete;
|
||||||
|
|
||||||
static void update(); // Actualización del sistema de audio
|
static void update(); // Actualització del sistema d'àudio
|
||||||
|
|
||||||
// --- Control de música ---
|
// --- Control de música ---
|
||||||
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
|
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproduir música per nom (amb crossfade opcional)
|
||||||
void pauseMusic(); // Pausar reproducción de música
|
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproduir música per punter (amb crossfade opcional)
|
||||||
|
void pauseMusic(); // Pausar la reproducció de música
|
||||||
void resumeMusic(); // Continua la música pausada
|
void resumeMusic(); // Continua la música pausada
|
||||||
void stopMusic(); // Detener completamente la música
|
void stopMusic(); // Aturar completament la música
|
||||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
|
||||||
|
void setOnMusicEnded(std::function<void()> callback); // Callback disparat cuando la pista actual acaba de drenar (CONV-03)
|
||||||
|
// Multiplicador de velocitat de la música actual. 1.0 = normal,
|
||||||
|
// 1.5 = un 50% més ràpid (efecte "chipmunk" — también puja el to).
|
||||||
|
// Es reseteja a 1.0 implícitament a cada `playMusic`. No-op si no
|
||||||
|
// hay música activa.
|
||||||
|
void setMusicSpeed(float ratio);
|
||||||
|
|
||||||
// --- Control de sonidos ---
|
// --- Control de sons ---
|
||||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
||||||
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
||||||
void stopAllSounds() const; // Detener todos los sonidos
|
// Reprodueix un so con la velocitat (i to) escalats per `speed`:
|
||||||
|
// 1.0 = normal, 0.95 ≈ -5% (més greu i lent), 1.05 ≈ +5% (més
|
||||||
|
// agut i ràpid). Mateixa semàntica que `setMusicSpeed`. Útil per a
|
||||||
|
// variacions subtils que eviten la fatiga d'escoltar el mismo
|
||||||
|
// sample idèntic (p.ex. obertures de sarcòfag, picks d'ítems).
|
||||||
|
void playSound(const std::string& name, Group group, float speed);
|
||||||
|
// Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
|
||||||
|
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||||
|
// o el motor está al sin de canals con efecte, fa fallback a playSound
|
||||||
|
// sec — l'usuari sent el so igualment, sin la cua.
|
||||||
|
void playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||||
|
void playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||||
|
void stopAllSounds(); // Aturar tots los sons (muta globals de Ja)
|
||||||
|
|
||||||
// --- Control de volumen ---
|
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||||
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
||||||
void setMusicVolume(float volume) const; // Ajustar volumen de música
|
void setMusicVolume(float volume); // Ajusta el volum de la música
|
||||||
|
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
|
||||||
|
|
||||||
|
// Getters dels volums actuals (lectura de la config_ cachejada). Reflexen
|
||||||
|
// el valor que l'usuari ha triat l'última vegada, independent del gating
|
||||||
|
// d'enabled/channel.
|
||||||
|
[[nodiscard]] auto getMasterVolume() const -> float { return config_.volume; }
|
||||||
|
[[nodiscard]] auto getSoundVolume() const -> float { return config_.sound_volume; }
|
||||||
|
[[nodiscard]] auto getMusicVolume() const -> float { return config_.music_volume; }
|
||||||
|
|
||||||
|
// --- Helpers de conversió para la capa de presentació ---
|
||||||
|
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||||
|
// No són constexpr porque std::lround no ho es en C++20; s'usen en runtime.
|
||||||
|
static auto toPercent(float volume) -> int {
|
||||||
|
return static_cast<int>(std::lround(volume * 100.0F));
|
||||||
|
}
|
||||||
|
static auto fromPercent(int percent) -> float {
|
||||||
|
return static_cast<float>(percent) / 100.0F;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Configuración general ---
|
// --- Configuración general ---
|
||||||
void enable(bool value); // Establecer estado general
|
void enable(bool value); // Estableix l'estat general (reaplica volums)
|
||||||
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
|
void toggleEnabled() { enable(!enabled_); } // Alterna l'estat general (reaplica volums)
|
||||||
void applySettings(); // Aplica la configuración
|
void applySettings(const Config& config); // Aplica una nueva configuración
|
||||||
|
|
||||||
// --- Configuración de sonidos ---
|
// --- Configuración de sons ---
|
||||||
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
|
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
|
||||||
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
|
void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (reaplica volum)
|
||||||
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
|
// Silencia (o restaura) un únic grup de sons sense tocar el volum cachejat
|
||||||
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
|
// de l'usuari ni la resta de grups. Pensat per a l'attract/demo: vol callar
|
||||||
|
// els SFX de joc (Group::GAME) pero mantenir els del menu de servei
|
||||||
|
// (Group::INTERFACE) i la música. En restaurar, reaplica el volum efectiu
|
||||||
|
// normal del canal (que ja respecta els gates master/sound).
|
||||||
|
void silenceGroup(Group group, bool silenced);
|
||||||
|
|
||||||
// --- Configuración de música ---
|
// --- Configuración de música ---
|
||||||
void enableMusic() { music_enabled_ = true; } // Habilitar música
|
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
|
||||||
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
|
void toggleMusic() { enableMusic(!music_enabled_); } // Alterna l'estat de la música (reaplica volum)
|
||||||
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
|
|
||||||
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
|
|
||||||
|
|
||||||
// --- Consultas de estado ---
|
// --- Consultes d'estat ---
|
||||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||||
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
||||||
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
||||||
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
|
[[nodiscard]] static auto getMusicState() -> MusicState; // Estat real consultat a Ja::
|
||||||
[[nodiscard]] static auto getRealMusicState() -> MusicState;
|
|
||||||
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
||||||
|
// Duración de la pista resolta per nom (mil·lisegons). 0 si la pista no
|
||||||
|
// existeix al cache de recursos o si el seu header OGG no permet
|
||||||
|
// calcular-la. Pensat para clients que necessiten un timeline
|
||||||
|
// determinista (p. ex. RoomFsm) sin dependre de callbacks de fi.
|
||||||
|
[[nodiscard]] static auto getMusicDurationMs(const std::string& name) -> int;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// --- Tipos anidados ---
|
// --- Tipus anidats ---
|
||||||
struct Music {
|
struct Music {
|
||||||
MusicState state{MusicState::STOPPED}; // Estado actual de la música
|
std::string name; // Última pista de música reproduïda (buida si es va passar per punter sin filename)
|
||||||
std::string name; // Última pista de música reproducida
|
bool loop{false}; // Si el play actual es en bucle
|
||||||
bool loop{false}; // Indica si se reproduce en bucle
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Métodos ---
|
// --- Mètodes ---
|
||||||
Audio(); // Constructor privado
|
explicit Audio(const Config& config); // Constructor privat: rep la config
|
||||||
~Audio(); // Destructor privado
|
void initSDLAudio(); // Inicialitza SDL Audio
|
||||||
void initSDLAudio(); // Inicializa SDL Audio
|
void playMusicInternal(Ja::Music* music, int loop, int crossfade_ms); // Camí comú dels dos overloads de playMusic
|
||||||
|
[[nodiscard]] auto effectiveVolume(float volume, bool channel_enabled) const -> float; // Gate master+channel: 0 si algun está off, clamp 0..1 altrament
|
||||||
|
|
||||||
// --- Variables miembro ---
|
// --- Variables membre ---
|
||||||
static Audio* instance; // Instancia única de Audio
|
static std::unique_ptr<Audio> instance; // Instància única d'Audio
|
||||||
|
|
||||||
Music music_; // Estado de la música
|
std::unique_ptr<Ja::Engine> engine_; // Motor de baix nivell (owned); viu mentre Audio viu.
|
||||||
bool enabled_{true}; // Estado general del audio
|
Config config_{}; // Configuración injectada (volums, enables)
|
||||||
bool sound_enabled_{true}; // Estado de los efectos de sonido
|
Music music_; // Estat de la música (nom + loop cachejats)
|
||||||
bool music_enabled_{true}; // Estado de la música
|
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,50 @@
|
|||||||
|
// 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* ENEMY_EXPLOSION = "effects/enemy_explosion.wav"; // Explosió d'enemic (debris default)
|
||||||
|
constexpr const char* ENEMY_HIT = "effects/enemy_hit.wav"; // Impacte parcial a enemic (debris_partial — HP > 1)
|
||||||
|
constexpr const char* PLAYER_EXPLOSION = "effects/player_explosion.wav"; // Explosió de la nau del jugador
|
||||||
|
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
|
||||||
|
constexpr const char* BULLET_ZAP = "effects/bullet_zap.wav"; // Bala desintegrant-se (qualsevol impacte o eixida de playarea)
|
||||||
|
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,45 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Visual {
|
||||||
|
|
||||||
|
// Duració del "flash" que dispara l'acció FLASH (feedback per impacte
|
||||||
|
// parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop,
|
||||||
|
// no com una transició.
|
||||||
|
constexpr float FLASH_DURATION = 0.08F;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Visual
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Debris {
|
||||||
|
|
||||||
|
// Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte
|
||||||
|
// en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat".
|
||||||
|
constexpr float PARTIAL_PIECE_SCALE = 0.3F;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Debris
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Fireworks {
|
||||||
|
|
||||||
|
// Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL
|
||||||
|
// (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa
|
||||||
|
// velocitat: una espurna breu, no una explosió.
|
||||||
|
constexpr int SMALL_N_POINTS = 20;
|
||||||
|
constexpr float SMALL_SPEED = 250.0F;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Fireworks
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// entities.hpp - Configuració d'objectes del joc (límits i radis de col·lisió)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace Defaults::Entities {
|
||||||
|
|
||||||
|
constexpr int MAX_ORNIS = 15;
|
||||||
|
constexpr int MAX_BULLETS = 50; // per jugador (P1 + P2 = 2× aquest valor)
|
||||||
|
constexpr int MAX_ENEMY_BULLETS = 50; // pool reservat per a bales d'enemic
|
||||||
|
|
||||||
|
// Total real de slots a l'array global bullets_: zona P1, zona P2 i zona enemic.
|
||||||
|
// Reservar zones impedeix que les bales d'enemic ocupin slots del jugador.
|
||||||
|
constexpr int MAX_BULLETS_TOTAL = (MAX_BULLETS * 2) + MAX_ENEMY_BULLETS;
|
||||||
|
constexpr int ENEMY_BULLET_START_IDX = MAX_BULLETS * 2;
|
||||||
|
|
||||||
|
// Convenció d'owner_id per a Bullet::fire:
|
||||||
|
// 0..1 = players (P1, P2)
|
||||||
|
// ENEMY_OWNER_BASE + index = enemic concret (per identificar el seu autoimpacte)
|
||||||
|
// Una bala mata a qualsevol col·lisió excepte amb el seu propi creador.
|
||||||
|
constexpr uint8_t ENEMY_OWNER_BASE = 128;
|
||||||
|
|
||||||
|
// 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,115 @@
|
|||||||
|
// 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 MAX_VIDES = 3; // Vides màximes per jugador (font única; el HUD en deriva els slots)
|
||||||
|
constexpr int STARTING_LIVES = MAX_VIDES; // S'arrenca amb les vides al màxim
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Attract mode: transició TÍTOL → DEMO. Primer un "dive" de càmera cap al
|
||||||
|
// punt de fuga (deixa enrere títol/naus/logo) i després una cortinilla negra
|
||||||
|
// que cau per tapar; a la demo, la cortinilla segueix caient i destapa.
|
||||||
|
namespace Dive {
|
||||||
|
constexpr float DURATION = 0.55F; // Durada del dive (s), amb acceleració (ease-in)
|
||||||
|
constexpr float CAMERA_DISTANCE = 450.0F; // Avanç de la càmera en +Z (passa les naus, a Z≈323)
|
||||||
|
constexpr float ZOOM_MAX = 7.0F; // Zoom final dels elements 2D (logo + peu) en travessar-los
|
||||||
|
} // namespace Dive
|
||||||
|
namespace Curtain {
|
||||||
|
constexpr float COVER_DURATION = 0.35F; // TÍTOL: la tela cau i tapa
|
||||||
|
constexpr float REVEAL_DURATION = 0.45F; // DEMO: la tela segueix caient i destapa
|
||||||
|
} // namespace Curtain
|
||||||
|
|
||||||
|
// 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,84 @@
|
|||||||
|
// hud.hpp - Configuració visual del HUD (marcador, etc.)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Mode de presentació de les vides al marcador (no es canvia en calent;
|
||||||
|
// es defineix ací mentre no estiga decidit si el nombre de vides serà fix).
|
||||||
|
// SLOTS → naus en miniatura en posicions fixes (s'encenen/atenuen).
|
||||||
|
// DIGITS → número de 2 dígits (mateixa regla que el nivell: zeros a
|
||||||
|
// l'esquerra atenuats, dígit significatiu en endavant encès).
|
||||||
|
enum class LivesDisplay : std::uint8_t { SLOTS,
|
||||||
|
DIGITS };
|
||||||
|
constexpr LivesDisplay LIVES_DISPLAY = LivesDisplay::DIGITS;
|
||||||
|
|
||||||
|
// Ajust fi de l'alçada dels slots de vides respecte a l'alçada del glif del
|
||||||
|
// dígit: la silueta de la nau ompli menys que un dígit, així que un xicotet
|
||||||
|
// factor >1 la fa casar visualment amb les xifres (calibrat a ull).
|
||||||
|
constexpr float LIFE_SLOT_HEIGHT_FACTOR = 1.2F;
|
||||||
|
|
||||||
|
// Esquema de color del marcador: "per jugador + sistema". Cada jugador usa
|
||||||
|
// el SEU color (parella brillant/atenuat) en tot el seu bloc (punts + vides);
|
||||||
|
// el nivell central va sempre en verd de sistema. Colors plans i purs: el
|
||||||
|
// glow/bloom el posa el shader de postpro, NO s'horneja al color. Amb
|
||||||
|
// alpha=255 el line_renderer usa el color directament sense caure al fallback
|
||||||
|
// verd (Rendering::DEFAULT_LINE_COLOR).
|
||||||
|
namespace Colors {
|
||||||
|
// Jugador 1 → cian.
|
||||||
|
constexpr SDL_Color P1_BRIGHT = {.r = 41, .g = 231, .b = 255, .a = 255}; // #29E7FF
|
||||||
|
constexpr SDL_Color P1_DIM = {.r = 12, .g = 90, .b = 102, .a = 255}; // #0C5A66
|
||||||
|
// Jugador 2 → groc.
|
||||||
|
constexpr SDL_Color P2_BRIGHT = {.r = 255, .g = 226, .b = 58, .a = 255}; // #FFE23A
|
||||||
|
constexpr SDL_Color P2_DIM = {.r = 90, .g = 82, .b = 16, .a = 255}; // #5A5210
|
||||||
|
// Nivell / sistema → verd.
|
||||||
|
constexpr SDL_Color LEVEL_BRIGHT = {.r = 77, .g = 255, .b = 102, .a = 255}; // #4DFF66
|
||||||
|
constexpr SDL_Color LEVEL_DIM = {.r = 29, .g = 107, .b = 44, .a = 255}; // #1D6B2C
|
||||||
|
} // namespace Colors
|
||||||
|
|
||||||
|
// Les vides es dibuixen com a slots fixos de naus en miniatura (NUM_SLOTS =
|
||||||
|
// MAX_VIDES − 1). Mida i pas dels slots es deriven de la mètrica del glif del
|
||||||
|
// dígit a init_hud_animator, no de constants soltes.
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// line_renderer::linea() cau a DEFAULT_LINE_COLOR (verd fòsfor fallback).
|
||||||
|
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,64 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
// Velocitat de la bala traspassada a cada fragment de debris al moment
|
||||||
|
// de l'impacte. Separat de la inèrcia del cos (velocitat_objecte): permet
|
||||||
|
// que els trossos volin "amb la força de la bala" encara que el cos pesi
|
||||||
|
// molt i amb prou feines es mogui. 0.4 a 700 px/s = ~280 px/s extra per
|
||||||
|
// fragment, molt visible sense ser excessiu.
|
||||||
|
constexpr float BULLET_IMPULSE_FACTOR = 0.4F;
|
||||||
|
|
||||||
|
// 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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user