Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
645db217fa | ||
|
|
cd961cf718 | ||
|
|
c0214b233b | ||
|
|
d054d1b246 | ||
|
|
cd5bb94223 | ||
|
|
178ce486c2 | ||
|
|
811c6c482d | ||
|
|
59cd9b524c | ||
|
|
28e55a0615 | ||
|
|
5c5cd1d5da | ||
|
|
377e5d0b97 | ||
|
|
648a5932f8 | ||
|
|
b54f3fc622 | ||
|
|
63bb41425a | ||
|
|
9a2c677153 | ||
|
|
4a9a037cd8 | ||
|
|
67b06c0a10 | ||
|
|
368f9d2d6a | ||
|
|
651db50a93 | ||
|
|
decf9f5d90 | ||
|
|
f921f6bf2b | ||
|
|
abf52be7e0 | ||
|
|
3e379ce129 | ||
|
|
c38c80753a | ||
|
|
4667e0abf9 | ||
|
|
7f78f556f3 | ||
|
|
e8407ce35e | ||
|
|
188f035333 | ||
|
|
3646e6a4aa | ||
|
|
f3f5fa5e92 | ||
|
|
3a750822f6 | ||
|
|
06ff11d563 | ||
|
|
f0a0515f28 | ||
|
|
4758d5d8a4 | ||
|
|
64390f8ad7 | ||
|
|
c4caa9021e | ||
|
|
a64f33fe43 | ||
|
|
cd50626b88 | ||
|
|
7b09a076b2 | ||
|
|
91614fc545 | ||
|
|
c77f90f6b6 | ||
|
|
4f1a3260a5 | ||
|
|
1af43ef052 | ||
|
|
d5a73a7dad | ||
|
|
578916bc58 | ||
|
|
ae8b9d8fe6 | ||
|
|
50aae790b1 | ||
|
|
ea0f6d98a9 | ||
|
|
291cbdc957 | ||
|
|
858e444842 | ||
|
|
511f8d5b09 | ||
|
|
e22f3240da | ||
|
|
1cc2e28b1b | ||
|
|
820b9f2d21 | ||
|
|
f3977170b7 | ||
|
|
996ff0053e | ||
|
|
e5b8d7731b | ||
|
|
a725b76684 | ||
|
|
4e9d5df872 | ||
|
|
74dad71cc2 | ||
|
|
5c70019eb5 | ||
|
|
8aa4ec3daf | ||
|
|
6c1c042637 | ||
|
|
dc8a7723e3 | ||
|
|
82b229222f | ||
|
|
67ecb704d2 | ||
|
|
936adf1405 | ||
|
|
0d914cc6dd | ||
|
|
1117264758 | ||
|
|
d0503ff8b1 | ||
|
|
e3cf2eb773 | ||
|
|
0f56ce7567 | ||
|
|
87b08a7c7c | ||
|
|
00eb696859 | ||
|
|
48ff1839bc | ||
|
|
01145464ed | ||
|
|
f30bb1fb0f | ||
|
|
05cdcfa660 | ||
|
|
a612a8255f | ||
|
|
329bf692e6 | ||
|
|
219f8033eb | ||
|
|
6d8423764d | ||
|
|
459d18b49c | ||
|
|
243ba06351 | ||
|
|
4da05b3d76 | ||
|
|
aeed0e01c2 | ||
|
|
9d256b7f2d |
No files matched your search
@@ -8,12 +8,7 @@ Checks:
|
|||||||
- -bugprone-integer-division
|
- -bugprone-integer-division
|
||||||
- -bugprone-easily-swappable-parameters
|
- -bugprone-easily-swappable-parameters
|
||||||
- -bugprone-narrowing-conversions
|
- -bugprone-narrowing-conversions
|
||||||
- -modernize-avoid-c-arrays,-warnings-as-errors
|
- -modernize-avoid-c-arrays
|
||||||
# Fals positiu recurrent en codi C de tercers (source/external/stb_vorbis.c).
|
|
||||||
# HeaderFilterRegex no aplica a clang-analyzer-*, així que cal desactivar
|
|
||||||
# explícitament. El check inferència sobre variables locals de legacy C
|
|
||||||
# genera reports que no reflecteixen bugs reals.
|
|
||||||
- -clang-analyzer-core.UndefinedBinaryOperatorResult
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -24,61 +19,89 @@ CheckOptions:
|
|||||||
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
|
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
|
||||||
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
|
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
|
||||||
|
|
||||||
# Variables locales en snake_case
|
# =====================================================================
|
||||||
|
# CONSTANTES → UPPER_CASE (compile-time y runtime, en cualquier scope)
|
||||||
|
# =====================================================================
|
||||||
|
# Todo lo que sea const o constexpr se identifica visualmente en UPPER_CASE,
|
||||||
|
# sin importar si es global, local, miembro o static.
|
||||||
|
|
||||||
|
# constexpr en cualquier scope (globales y locales)
|
||||||
|
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Constantes globales (const no-constexpr)
|
||||||
|
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Constantes locales (const en función)
|
||||||
|
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Static const a nivel de archivo/namespace
|
||||||
|
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Miembros static const/constexpr de clase (p.ej. static constexpr int MAX = 100;)
|
||||||
|
- { key: readability-identifier-naming.ClassConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Miembros const no-static de clase (p.ej. const int limit;)
|
||||||
|
- { key: readability-identifier-naming.ConstantMemberCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Valores de enums
|
||||||
|
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# NOTA: Los parámetros const NO se tratan como constantes aquí.
|
||||||
|
# Un parámetro sigue siendo un parámetro aunque sea const → hereda ParameterCase.
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# VARIABLES NO-CONST
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
# Variables locales
|
||||||
- { key: readability-identifier-naming.VariableCase, value: lower_case }
|
- { key: readability-identifier-naming.VariableCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.LocalVariableCase, value: lower_case }
|
||||||
|
|
||||||
# Miembros privados en snake_case con sufijo _
|
# Parámetros de función
|
||||||
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||||
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
|
||||||
|
|
||||||
# Miembros protegidos en snake_case con sufijo _
|
# Variables estáticas no-const (static locales, static file-scope,
|
||||||
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
# y static members no-const de clase como el instance_ de un Singleton).
|
||||||
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
# Sufijo _ para marcar que tienen storage estático.
|
||||||
|
|
||||||
# Miembros públicos en snake_case (sin sufijo)
|
|
||||||
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
|
||||||
|
|
||||||
# Namespaces en CamelCase
|
|
||||||
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
|
||||||
|
|
||||||
# Variables estáticas privadas como miembros privados
|
|
||||||
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
||||||
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
||||||
|
|
||||||
# Constantes estáticas sin sufijo
|
# =====================================================================
|
||||||
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
# MIEMBROS DE CLASE NO-CONST
|
||||||
|
# =====================================================================
|
||||||
|
# Privados: snake_case con sufijo _
|
||||||
|
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
||||||
|
|
||||||
# Constantes globales en UPPER_CASE
|
# Protegidos: snake_case con sufijo _
|
||||||
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||||
|
|
||||||
# Variables constexpr globales en UPPER_CASE
|
# Públicos: snake_case sin sufijo
|
||||||
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
||||||
|
|
||||||
# Constantes locales en UPPER_CASE
|
# =====================================================================
|
||||||
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
# TIPOS
|
||||||
|
# =====================================================================
|
||||||
# Constexpr miembros en UPPER_CASE (sin sufijo)
|
|
||||||
- { key: readability-identifier-naming.ConstexprMemberCase, value: UPPER_CASE }
|
|
||||||
|
|
||||||
# Constexpr miembros privados/protegidos con sufijo _
|
|
||||||
- { key: readability-identifier-naming.ConstexprMethodCase, value: UPPER_CASE }
|
|
||||||
|
|
||||||
# Clases, structs y enums en CamelCase
|
|
||||||
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
||||||
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
||||||
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
|
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.UnionCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.TypeAliasCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.TypedefCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase }
|
||||||
|
|
||||||
# Valores de enums en UPPER_CASE
|
# Namespaces
|
||||||
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
||||||
|
|
||||||
# Métodos en camelBack (sin sufijos)
|
# =====================================================================
|
||||||
|
# FUNCIONES Y MÉTODOS (incluyendo constexpr)
|
||||||
|
# =====================================================================
|
||||||
|
# Un método/función constexpr es un invocable, no una constante → camelBack.
|
||||||
|
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
||||||
|
- { key: readability-identifier-naming.ConstexprFunctionCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.MethodCase, value: camelBack }
|
- { key: readability-identifier-naming.MethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
||||||
|
- { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack }
|
||||||
# Funciones en camelBack
|
|
||||||
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
|
||||||
|
|
||||||
# Parámetros en lower_case
|
|
||||||
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
|
||||||
@@ -47,6 +47,7 @@ set(ENTITY_SOURCES
|
|||||||
source/game/entities/concrete/rolling_rock.cpp
|
source/game/entities/concrete/rolling_rock.cpp
|
||||||
source/game/entities/concrete/wall_trap.cpp
|
source/game/entities/concrete/wall_trap.cpp
|
||||||
source/game/entities/concrete/floor_trap.cpp
|
source/game/entities/concrete/floor_trap.cpp
|
||||||
|
source/game/entities/concrete/dust.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(APP_SOURCES
|
set(APP_SOURCES
|
||||||
@@ -56,6 +57,7 @@ set(APP_SOURCES
|
|||||||
source/core/audio/audio_adapter.cpp
|
source/core/audio/audio_adapter.cpp
|
||||||
source/core/audio/jail_audio.cpp
|
source/core/audio/jail_audio.cpp
|
||||||
source/core/locale/locale.cpp
|
source/core/locale/locale.cpp
|
||||||
|
source/core/ui/ui_config.cpp
|
||||||
source/core/input/key_config.cpp
|
source/core/input/key_config.cpp
|
||||||
source/core/input/global_inputs.cpp
|
source/core/input/global_inputs.cpp
|
||||||
source/core/input/input_state.cpp
|
source/core/input/input_state.cpp
|
||||||
@@ -66,6 +68,9 @@ set(APP_SOURCES
|
|||||||
source/core/rendering/notifier.cpp
|
source/core/rendering/notifier.cpp
|
||||||
source/core/rendering/render_info.cpp
|
source/core/rendering/render_info.cpp
|
||||||
source/core/rendering/gif.cpp
|
source/core/rendering/gif.cpp
|
||||||
|
source/core/rendering/screenshot.cpp
|
||||||
|
source/external/stb_image_write_impl.cpp
|
||||||
|
source/external/stb_vorbis_impl.cpp
|
||||||
source/core/rendering/surface.cpp
|
source/core/rendering/surface.cpp
|
||||||
source/core/rendering/text.cpp
|
source/core/rendering/text.cpp
|
||||||
source/core/rendering/sprite/sprite.cpp
|
source/core/rendering/sprite/sprite.cpp
|
||||||
@@ -76,6 +81,7 @@ set(APP_SOURCES
|
|||||||
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_cache.cpp
|
source/core/resources/resource_cache.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
source/core/resources/palette_loader.cpp
|
source/core/resources/palette_loader.cpp
|
||||||
source/utils/easing.cpp
|
source/utils/easing.cpp
|
||||||
source/utils/utils.cpp
|
source/utils/utils.cpp
|
||||||
@@ -83,14 +89,22 @@ set(APP_SOURCES
|
|||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
source/game/options_apply.cpp
|
source/game/options_apply.cpp
|
||||||
source/game/ui/service_menu.cpp
|
source/game/ui/service_menu.cpp
|
||||||
|
source/game/ui/hud.cpp
|
||||||
source/game/scenes/placeholder_input.cpp
|
source/game/scenes/placeholder_input.cpp
|
||||||
source/game/scenes/logo_scene.cpp
|
source/game/scenes/logo_scene.cpp
|
||||||
source/game/scenes/title_scene.cpp
|
source/game/scenes/title_scene.cpp
|
||||||
source/game/scenes/intro_scene.cpp
|
source/game/scenes/intro_scene.cpp
|
||||||
source/game/scenes/game_scene.cpp
|
source/game/scenes/game_scene.cpp
|
||||||
|
source/game/scenes/room_fsm.cpp
|
||||||
source/game/entities/player.cpp
|
source/game/entities/player.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
source/game/map/tilemap.cpp
|
source/game/map/tilemap.cpp
|
||||||
|
source/game/map/tilemap_writer.cpp
|
||||||
|
source/game/map/paint_layer.cpp
|
||||||
|
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
|
||||||
|
# En Release el fitxer compila a un object buit i l'enllaçador no
|
||||||
|
# arrossega cap símbol del joc final.
|
||||||
|
source/game/editor/editor.cpp
|
||||||
${ENTITY_SOURCES}
|
${ENTITY_SOURCES}
|
||||||
# Fitxer només del binari principal: `loadFromLevel` depèn de
|
# Fitxer només del binari principal: `loadFromLevel` depèn de
|
||||||
# Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar
|
# Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar
|
||||||
@@ -160,24 +174,63 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
|||||||
|
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
||||||
|
|
||||||
# Còpia recursiva de data/ al costat del binari perquè les rutes relatives
|
# --- EINA STANDALONE: pack_resources ---
|
||||||
# ("data/player/player.gif") funcionin sense desplegament. Es defineix com
|
# Executable auxiliar que empaqueta `data/` a `build/resources.pack`.
|
||||||
# a custom target amb `ALL` — s'executa a cada `cmake --build` encara que
|
# EXCLUDE_FROM_ALL: només es compila quan algun target en depèn (ho fa
|
||||||
# el binari no hagi canviat, de manera que editar YAML/GIF sense tocar codi
|
# `resource_pack`). Build manual: `cmake --build build --target pack_resources`.
|
||||||
# igualment sincronitza el build. Abans era un POST_BUILD, i si només es
|
add_executable(pack_resources EXCLUDE_FROM_ALL
|
||||||
# modificava una dada la còpia no es disparava.
|
tools/pack_resources/pack_resources.cpp
|
||||||
add_custom_target(sync_data ALL
|
source/core/resources/resource_pack.cpp
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
"${CMAKE_SOURCE_DIR}/data"
|
|
||||||
"${CMAKE_BINARY_DIR}/data"
|
|
||||||
COMMENT "Copying data/ next to the binary"
|
|
||||||
)
|
)
|
||||||
add_dependencies(${PROJECT_NAME} sync_data)
|
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||||
|
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
|
||||||
|
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
||||||
|
# A cada build re-empaquetem data/ si algun fitxer dins ha canviat. Evita
|
||||||
|
# debugar amb un pack obsolet. CONFIGURE_DEPENDS força CMake a re-globbar
|
||||||
|
# a la pròxima invocació (recull fitxers nous afegits a data/).
|
||||||
|
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
|
||||||
|
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${RESOURCE_PACK}
|
||||||
|
COMMAND $<TARGET_FILE:pack_resources>
|
||||||
|
"${CMAKE_SOURCE_DIR}/data"
|
||||||
|
"${RESOURCE_PACK}"
|
||||||
|
DEPENDS pack_resources ${DATA_FILES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
COMMENT "Empaquetant data/ → build/resources.pack"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
|
||||||
|
add_dependencies(${PROJECT_NAME} resource_pack)
|
||||||
|
|
||||||
|
# --- CÒPIA DE gamecontrollerdb.txt AL COSTAT DEL BINARI ---
|
||||||
|
# SDL_AddGamepadMappingsFromFile només llegeix del filesystem real (no del
|
||||||
|
# pack), així que el fitxer ha de viure al directori del binari. Es copia
|
||||||
|
# només si existeix per no fallar la build d'algú que encara no ha fet
|
||||||
|
# `make controllerdb`.
|
||||||
|
if(EXISTS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt")
|
||||||
|
set(CONTROLLER_DB "${CMAKE_BINARY_DIR}/gamecontrollerdb.txt")
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${CONTROLLER_DB}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
|
||||||
|
"${CONTROLLER_DB}"
|
||||||
|
DEPENDS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
|
||||||
|
COMMENT "Copiant gamecontrollerdb.txt → build/"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(controller_db ALL DEPENDS ${CONTROLLER_DB})
|
||||||
|
add_dependencies(${PROJECT_NAME} controller_db)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||||
$<$<CONFIG:Debug>:_DEBUG>
|
$<$<CONFIG:Debug>:_DEBUG>
|
||||||
$<$<CONFIG:Release>:RELEASE_BUILD>
|
$<$<CONFIG:Release>:RELEASE_BUILD>
|
||||||
|
# En Debug el fallback a disc apunta al source tree (sense re-packejar).
|
||||||
|
AEE_SOURCE_DIR="${CMAKE_SOURCE_DIR}/"
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- STATIC ANALYSIS / FORMAT TARGETS ---
|
# --- STATIC ANALYSIS / FORMAT TARGETS ---
|
||||||
@@ -271,6 +324,7 @@ if(BUILD_TESTS)
|
|||||||
add_executable(test_entities_smoke
|
add_executable(test_entities_smoke
|
||||||
tests/test_entities_smoke.cpp
|
tests/test_entities_smoke.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
${ENTITY_SOURCES}
|
${ENTITY_SOURCES}
|
||||||
)
|
)
|
||||||
@@ -295,6 +349,7 @@ if(BUILD_TESTS)
|
|||||||
source/game/scenes/placeholder_input.cpp
|
source/game/scenes/placeholder_input.cpp
|
||||||
source/core/input/key_config.cpp
|
source/core/input/key_config.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(test_scene_flow PRIVATE
|
target_include_directories(test_scene_flow PRIVATE
|
||||||
"${CMAKE_SOURCE_DIR}/source"
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
@@ -305,4 +360,43 @@ if(BUILD_TESTS)
|
|||||||
add_test(NAME scene_flow
|
add_test(NAME scene_flow
|
||||||
COMMAND test_scene_flow
|
COMMAND test_scene_flow
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
|
# Smoke del writer surgical del tilemap (escriu el bloc `data:` sense
|
||||||
|
# tocar la resta del fitxer). Pura manipulació de text, zero
|
||||||
|
# dependències: no cal ni SDL ni el Resource::Cache.
|
||||||
|
add_executable(test_tilemap_writer
|
||||||
|
tests/test_tilemap_writer.cpp
|
||||||
|
source/game/map/tilemap_writer.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_tilemap_writer PRIVATE
|
||||||
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
)
|
||||||
|
target_compile_options(test_tilemap_writer PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
add_test(NAME tilemap_writer
|
||||||
|
COMMAND test_tilemap_writer
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
|
# Smoke de la FSM de sala (RoomFsm). Cobreix loadConfig (timings i
|
||||||
|
# corbes des de YAML), advance (transicions entre estats), i
|
||||||
|
# formatRoomTitle (locale + substitució `{n}`). Stand-alone: no
|
||||||
|
# arrossega Audio/Screen/Player/EntityManager — només els helpers
|
||||||
|
# purs de la FSM, Locale i UiConfig.
|
||||||
|
add_executable(test_game_fsm
|
||||||
|
tests/test_game_fsm.cpp
|
||||||
|
source/game/scenes/room_fsm.cpp
|
||||||
|
source/core/locale/locale.cpp
|
||||||
|
source/core/ui/ui_config.cpp
|
||||||
|
source/core/resources/resource_helper.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
|
source/utils/easing.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_game_fsm PRIVATE
|
||||||
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(test_game_fsm PRIVATE SDL3::SDL3)
|
||||||
|
target_compile_options(test_game_fsm PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
add_test(NAME game_fsm
|
||||||
|
COMMAND test_game_fsm
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
endif()
|
endif()
|
||||||
@@ -14,7 +14,7 @@ endif
|
|||||||
|
|
||||||
CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH)
|
CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH)
|
||||||
|
|
||||||
.PHONY: all debug release run run-debug clean rebuild show-version format format-check tidy tidy-fix cppcheck hooks-install help
|
.PHONY: all debug release run run-debug clean rebuild show-version pack controllerdb format format-check tidy tidy-fix cppcheck hooks-install help
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓ
|
# COMPILACIÓ
|
||||||
@@ -53,6 +53,23 @@ clean:
|
|||||||
|
|
||||||
rebuild: clean all
|
rebuild: clean all
|
||||||
|
|
||||||
|
# Empaqueta data/ a $(BUILDDIR)/resources.pack (format AE26). Força un rebuild
|
||||||
|
# del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa
|
||||||
|
# automàticament a cada `make`/`make debug` via el target CMake `resource_pack`.
|
||||||
|
pack:
|
||||||
|
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
|
@cmake --build $(BUILDDIR) --target pack_resources
|
||||||
|
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||||
|
|
||||||
|
# Descàrrega de la base de dades de mappings de mandos. SDL només pot
|
||||||
|
# obrir-la des del filesystem (no del pack), per això viu a l'arrel del
|
||||||
|
# projecte i CMake la copia al costat del binari a cada build.
|
||||||
|
controllerdb:
|
||||||
|
@echo "Descarregant gamecontrollerdb.txt..."
|
||||||
|
@curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
|
||||||
|
-o gamecontrollerdb.txt
|
||||||
|
@echo "gamecontrollerdb.txt actualitzat"
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CODE QUALITY (delegados a cmake)
|
# CODE QUALITY (delegados a cmake)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -100,6 +117,10 @@ help:
|
|||||||
@echo " Release:"
|
@echo " Release:"
|
||||||
@echo " make release - Release + empaquetat (TODO pendent)"
|
@echo " make release - Release + empaquetat (TODO pendent)"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo " Recursos:"
|
||||||
|
@echo " make pack - Empaquetar data/ a $(BUILDDIR)/resources.pack (format AE26)"
|
||||||
|
@echo " make controllerdb - Descarregar gamecontrollerdb.txt actualitzat (arrel)"
|
||||||
|
@echo ""
|
||||||
@echo " Qualitat de codi:"
|
@echo " Qualitat de codi:"
|
||||||
@echo " make format - Formatar codi amb clang-format"
|
@echo " make format - Formatar codi amb clang-format"
|
||||||
@echo " make format-check - Verificar format sense modificar"
|
@echo " make format-check - Verificar format sense modificar"
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ sprite:
|
|||||||
path: door.gif
|
path: door.gif
|
||||||
size: [48, 32]
|
size: [48, 32]
|
||||||
tile_size: [3, 2]
|
tile_size: [3, 2]
|
||||||
|
collision:
|
||||||
|
# Caixa de col·lisió: 16×24 centrada al sprite 48×32. Coincideix amb el
|
||||||
|
# tile central transitable (els 16 px del passadís de sortida) i es
|
||||||
|
# restringeix a la franja del mur trasser (24 px d'alçada). Així
|
||||||
|
# `checkPlayerCollisions` només dispara `onPlayerContact` quan el
|
||||||
|
# jugador entra al forat real, no als marcs flanquejants ni als 8 px
|
||||||
|
# superiors que queden tapats pel HUD.
|
||||||
|
size: [16, 24]
|
||||||
|
offset: [16, 8]
|
||||||
|
# Porta = mur infranquejable. Igual que el sarc, alçada efectiva
|
||||||
|
# infinita per impedir que un jugador la salte per damunt.
|
||||||
|
height: 999
|
||||||
flags:
|
flags:
|
||||||
collides: true
|
collides: true
|
||||||
interactable: true
|
interactable: true
|
||||||
|
|||||||
|
After Width: | Height: | Size: 306 B |
@@ -0,0 +1,22 @@
|
|||||||
|
name: dust
|
||||||
|
type: effect
|
||||||
|
sprite:
|
||||||
|
path: dust.gif
|
||||||
|
size: [40, 16]
|
||||||
|
|
||||||
|
# El polvet és purament visual: no col·lisiona, no interactua, no fa
|
||||||
|
# dany. Només viu mentre dura l'animació triada (`jump` o `land`) i
|
||||||
|
# desapareix per `markDead` quan acaba.
|
||||||
|
flags:
|
||||||
|
collides: false
|
||||||
|
interactable: false
|
||||||
|
|
||||||
|
# Dues animacions, no-loop: una per a cada moment del salt. El spawner
|
||||||
|
# (GameScene, en resposta a events del bus) instancia la variant
|
||||||
|
# apropiada via `setVariant` abans del primer tick.
|
||||||
|
# jump: frames 0..6 (7 frames) → reproduït al despegue.
|
||||||
|
# land: frames 7..12 (6 frames) → reproduït a l'aterratge.
|
||||||
|
# fps: 10 = 100 ms per frame.
|
||||||
|
animations:
|
||||||
|
jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 15, loop: false }
|
||||||
|
land: { frames: [7, 8, 9, 10, 11, 12], fps: 15, loop: false }
|
||||||
@@ -4,6 +4,11 @@ sprite:
|
|||||||
path: floor_trap.gif
|
path: floor_trap.gif
|
||||||
size: [16, 16]
|
size: [16, 16]
|
||||||
tile_size: [1, 1]
|
tile_size: [1, 1]
|
||||||
|
collision:
|
||||||
|
# Trampa al sòl: alçada 0 → qualsevol salt l'esquiva. (Avui amb
|
||||||
|
# `collides: false` la trampa no entra al filtre de contacte, però
|
||||||
|
# declarem el camp per quan es cablege la detecció via àrea.)
|
||||||
|
height: 0
|
||||||
flags:
|
flags:
|
||||||
collides: false
|
collides: false
|
||||||
interactable: false
|
interactable: false
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -2,22 +2,27 @@ name: mummy
|
|||||||
type: dynamic_hazard
|
type: dynamic_hazard
|
||||||
sprite:
|
sprite:
|
||||||
path: mummy.gif
|
path: mummy.gif
|
||||||
size: [12, 24]
|
size: [16, 32]
|
||||||
tile_size: [1, 1]
|
tile_size: [1, 1]
|
||||||
# La momia usa falsa perspectiva: el sprite fa 12×24 (dos 12×12: cap i
|
# Variant 16×32 de la momia: el sprite fa 16×32 (dos 16×16: cap i peus)
|
||||||
# peus) però la caja de col·lisió amb el mapa és 16×16, concèntrica amb
|
# i la caja de col·lisió amb el mapa és 16×16, coincident amb el
|
||||||
# el rectangle-base 12×12 dels peus (igual que fa el jugador). Així la
|
# rectangle-base 16×16 dels peus. Com que `size` i `base_size` són
|
||||||
# caixa s'estén 2 px a cada costat i 2 px per sota dels peus visuals,
|
# iguals, el padding és zero i la caja s'alinea exactament amb la
|
||||||
# i amb `anchor: feet` s'alinea exactament amb el tile 16×16 on naix —
|
# silueta inferior del sprite; amb `anchor: feet` ocupa la cel·la
|
||||||
# la momia va pel centre del corredor i la caja ocupa la cel·la completa.
|
# 16×16 del corredor on naix.
|
||||||
anchor: feet
|
anchor: feet
|
||||||
collision:
|
collision:
|
||||||
size: [16, 16]
|
size: [16, 16]
|
||||||
base_size: [12, 12]
|
base_size: [16, 16]
|
||||||
|
# Alçada del cos en píxels per al filtre de col·lisió per salt: un
|
||||||
|
# jugador amb jump_z > height no col·lisiona amb la momia (la salta
|
||||||
|
# per damunt). Amb impulse=240 + gravity=600 el pic ronda els 48 px,
|
||||||
|
# així que 24 deixa marge per esquivar amb un salt no perfecte.
|
||||||
|
height: 24
|
||||||
flags:
|
flags:
|
||||||
collides: true
|
collides: true
|
||||||
interactable: false
|
interactable: false
|
||||||
# Strip de 12 frames en graella 12×1 (mateix esquema que el jugador):
|
# Strip de 12 frames en graella 4×3 (mateix esquema que el jugador):
|
||||||
# 0..3 → walk_down (cara avall)
|
# 0..3 → walk_down (cara avall)
|
||||||
# 4..7 → walk_up (cara amunt)
|
# 4..7 → walk_up (cara amunt)
|
||||||
# 8..11 → walk (lateral; flip horitzontal segons el sentit)
|
# 8..11 → walk (lateral; flip horitzontal segons el sentit)
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ sprite:
|
|||||||
path: rolling_rock.gif
|
path: rolling_rock.gif
|
||||||
size: [16, 16]
|
size: [16, 16]
|
||||||
tile_size: [1, 1]
|
tile_size: [1, 1]
|
||||||
|
collision:
|
||||||
|
# `size`/`base_size` no calen per a la roca (tot el sprite és cos),
|
||||||
|
# però sí declarem `height` per al filtre de salt: 16 px → fàcil
|
||||||
|
# d'esquivar amb un salt curt.
|
||||||
|
height: 16
|
||||||
flags:
|
flags:
|
||||||
collides: true
|
collides: true
|
||||||
interactable: false
|
interactable: false
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ sprite:
|
|||||||
# són "alçada" en falsa perspectiva, sense col·lisió. Perímetre ortogonal
|
# són "alçada" en falsa perspectiva, sense col·lisió. Perímetre ortogonal
|
||||||
# de 8 tiles (3 N + 3 S + 1 O + 1 E).
|
# de 8 tiles (3 N + 3 S + 1 O + 1 E).
|
||||||
tile_size: [3, 1]
|
tile_size: [3, 1]
|
||||||
|
collision:
|
||||||
|
# Sarcòfag = obstacle massís: alçada efectiva infinita (999) per al
|
||||||
|
# filtre del salt. Un jugador no el pot saltar per damunt encara que
|
||||||
|
# el salt arribe al punt àlgid. La caixa de col·lisió real (logical
|
||||||
|
# 48×16) la calcula `placeSarcophagus` a partir de la geometria del
|
||||||
|
# mapa, no des d'aquest YAML.
|
||||||
|
height: 999
|
||||||
flags:
|
flags:
|
||||||
collides: true
|
collides: true
|
||||||
interactable: true
|
interactable: true
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ sprite:
|
|||||||
path: torch.gif
|
path: torch.gif
|
||||||
size: [16, 16]
|
size: [16, 16]
|
||||||
tile_size: [1, 1]
|
tile_size: [1, 1]
|
||||||
|
collision:
|
||||||
|
# Decoratiu, no col·lisiona. Camp `height` declarat per coherència
|
||||||
|
# amb la resta d'entitats; no té efecte mentre `collides: false`.
|
||||||
|
height: 0
|
||||||
flags:
|
flags:
|
||||||
collides: false
|
collides: false
|
||||||
interactable: false
|
interactable: false
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ sprite:
|
|||||||
path: wall_trap.gif
|
path: wall_trap.gif
|
||||||
size: [16, 16]
|
size: [16, 16]
|
||||||
tile_size: [1, 1]
|
tile_size: [1, 1]
|
||||||
|
collision:
|
||||||
|
# Trampa de paret: ix d'una vora alta i no es pot saltar per damunt.
|
||||||
|
# 999 = infranquejable. Igual que floor_trap, el filtre només té efecte
|
||||||
|
# quan la detecció de contacte hi entre.
|
||||||
|
height: 999
|
||||||
flags:
|
flags:
|
||||||
collides: false
|
collides: false
|
||||||
interactable: false
|
interactable: false
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
# Font: ari-w9500-bold — generado desde ari-w9500-bold.ttf size 11
|
|
||||||
# Generado con tools/font_gen/font_gen.py
|
|
||||||
|
|
||||||
box_width 13
|
|
||||||
box_height 11
|
|
||||||
columns 15
|
|
||||||
|
|
||||||
# codepoint_decimal ancho_visual
|
|
||||||
32 3 # U+0020
|
|
||||||
33 3 # !
|
|
||||||
34 6 # "
|
|
||||||
35 9 # #
|
|
||||||
36 7 # $
|
|
||||||
37 12 # %
|
|
||||||
38 8 # &
|
|
||||||
39 3 # '
|
|
||||||
40 5 # (
|
|
||||||
41 5 # )
|
|
||||||
42 5 # *
|
|
||||||
43 7 # +
|
|
||||||
44 3 # ,
|
|
||||||
45 5 # -
|
|
||||||
46 3 # .
|
|
||||||
47 6 # /
|
|
||||||
48 7 # 0
|
|
||||||
49 6 # 1
|
|
||||||
50 7 # 2
|
|
||||||
51 7 # 3
|
|
||||||
52 7 # 4
|
|
||||||
53 7 # 5
|
|
||||||
54 7 # 6
|
|
||||||
55 7 # 7
|
|
||||||
56 7 # 8
|
|
||||||
57 7 # 9
|
|
||||||
58 3 # :
|
|
||||||
59 3 # ;
|
|
||||||
60 7 # <
|
|
||||||
61 7 # =
|
|
||||||
62 7 # >
|
|
||||||
63 7 # ?
|
|
||||||
64 13 # @
|
|
||||||
65 9 # A
|
|
||||||
66 8 # B
|
|
||||||
67 8 # C
|
|
||||||
68 8 # D
|
|
||||||
69 7 # E
|
|
||||||
70 7 # F
|
|
||||||
71 8 # G
|
|
||||||
72 8 # H
|
|
||||||
73 3 # I
|
|
||||||
74 6 # J
|
|
||||||
75 8 # K
|
|
||||||
76 7 # L
|
|
||||||
77 10 # M
|
|
||||||
78 9 # N
|
|
||||||
79 9 # O
|
|
||||||
80 8 # P
|
|
||||||
81 9 # Q
|
|
||||||
82 8 # R
|
|
||||||
83 8 # S
|
|
||||||
84 7 # T
|
|
||||||
85 8 # U
|
|
||||||
86 8 # V
|
|
||||||
87 11 # W
|
|
||||||
88 9 # X
|
|
||||||
89 9 # Y
|
|
||||||
90 9 # Z
|
|
||||||
91 5 # [
|
|
||||||
92 6 # \
|
|
||||||
93 5 # ]
|
|
||||||
94 6 # ^
|
|
||||||
95 7 # _
|
|
||||||
96 4 # `
|
|
||||||
97 7 # a
|
|
||||||
98 7 # b
|
|
||||||
99 7 # c
|
|
||||||
100 7 # d
|
|
||||||
101 7 # e
|
|
||||||
102 5 # f
|
|
||||||
103 7 # g
|
|
||||||
104 7 # h
|
|
||||||
105 3 # i
|
|
||||||
106 3 # j
|
|
||||||
107 7 # k
|
|
||||||
108 3 # l
|
|
||||||
109 9 # m
|
|
||||||
110 7 # n
|
|
||||||
111 7 # o
|
|
||||||
112 7 # p
|
|
||||||
113 7 # q
|
|
||||||
114 5 # r
|
|
||||||
115 6 # s
|
|
||||||
116 5 # t
|
|
||||||
117 7 # u
|
|
||||||
118 7 # v
|
|
||||||
119 9 # w
|
|
||||||
120 7 # x
|
|
||||||
121 7 # y
|
|
||||||
122 7 # z
|
|
||||||
123 6 # {
|
|
||||||
124 3 # |
|
|
||||||
125 6 # }
|
|
||||||
126 8 # ~
|
|
||||||
192 9 # À
|
|
||||||
193 9 # Á
|
|
||||||
200 7 # È
|
|
||||||
201 7 # É
|
|
||||||
204 3 # Ì
|
|
||||||
205 3 # Í
|
|
||||||
210 9 # Ò
|
|
||||||
211 9 # Ó
|
|
||||||
217 8 # Ù
|
|
||||||
218 8 # Ú
|
|
||||||
209 9 # Ñ
|
|
||||||
199 8 # Ç
|
|
||||||
224 7 # à
|
|
||||||
225 7 # á
|
|
||||||
232 7 # è
|
|
||||||
233 7 # é
|
|
||||||
236 3 # ì
|
|
||||||
237 4 # í
|
|
||||||
242 7 # ò
|
|
||||||
243 7 # ó
|
|
||||||
249 7 # ù
|
|
||||||
250 7 # ú
|
|
||||||
241 7 # ñ
|
|
||||||
231 7 # ç
|
|
||||||
161 3 # ¡
|
|
||||||
191 6 # ¿
|
|
||||||
171 9 # «
|
|
||||||
187 9 # »
|
|
||||||
183 3 # ·
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,132 +0,0 @@
|
|||||||
# Font: ari-w9500-condensed-bold — generado desde ari-w9500-condensed-bold.ttf size 11
|
|
||||||
# Generado con tools/font_gen/font_gen.py
|
|
||||||
|
|
||||||
box_width 12
|
|
||||||
box_height 11
|
|
||||||
columns 15
|
|
||||||
|
|
||||||
# codepoint_decimal ancho_visual
|
|
||||||
32 2 # U+0020
|
|
||||||
33 3 # !
|
|
||||||
34 6 # "
|
|
||||||
35 9 # #
|
|
||||||
36 6 # $
|
|
||||||
37 10 # %
|
|
||||||
38 7 # &
|
|
||||||
39 3 # '
|
|
||||||
40 5 # (
|
|
||||||
41 5 # )
|
|
||||||
42 5 # *
|
|
||||||
43 7 # +
|
|
||||||
44 3 # ,
|
|
||||||
45 5 # -
|
|
||||||
46 3 # .
|
|
||||||
47 5 # /
|
|
||||||
48 6 # 0
|
|
||||||
49 5 # 1
|
|
||||||
50 6 # 2
|
|
||||||
51 6 # 3
|
|
||||||
52 6 # 4
|
|
||||||
53 6 # 5
|
|
||||||
54 6 # 6
|
|
||||||
55 6 # 7
|
|
||||||
56 6 # 8
|
|
||||||
57 6 # 9
|
|
||||||
58 3 # :
|
|
||||||
59 3 # ;
|
|
||||||
60 7 # <
|
|
||||||
61 7 # =
|
|
||||||
62 7 # >
|
|
||||||
63 6 # ?
|
|
||||||
64 12 # @
|
|
||||||
65 8 # A
|
|
||||||
66 7 # B
|
|
||||||
67 7 # C
|
|
||||||
68 7 # D
|
|
||||||
69 6 # E
|
|
||||||
70 6 # F
|
|
||||||
71 7 # G
|
|
||||||
72 7 # H
|
|
||||||
73 3 # I
|
|
||||||
74 6 # J
|
|
||||||
75 7 # K
|
|
||||||
76 7 # L
|
|
||||||
77 8 # M
|
|
||||||
78 8 # N
|
|
||||||
79 8 # O
|
|
||||||
80 7 # P
|
|
||||||
81 8 # Q
|
|
||||||
82 7 # R
|
|
||||||
83 7 # S
|
|
||||||
84 7 # T
|
|
||||||
85 7 # U
|
|
||||||
86 7 # V
|
|
||||||
87 9 # W
|
|
||||||
88 7 # X
|
|
||||||
89 7 # Y
|
|
||||||
90 7 # Z
|
|
||||||
91 5 # [
|
|
||||||
92 5 # \
|
|
||||||
93 5 # ]
|
|
||||||
94 6 # ^
|
|
||||||
95 6 # _
|
|
||||||
96 4 # `
|
|
||||||
97 6 # a
|
|
||||||
98 6 # b
|
|
||||||
99 6 # c
|
|
||||||
100 6 # d
|
|
||||||
101 6 # e
|
|
||||||
102 5 # f
|
|
||||||
103 6 # g
|
|
||||||
104 6 # h
|
|
||||||
105 3 # i
|
|
||||||
106 3 # j
|
|
||||||
107 6 # k
|
|
||||||
108 3 # l
|
|
||||||
109 8 # m
|
|
||||||
110 6 # n
|
|
||||||
111 6 # o
|
|
||||||
112 6 # p
|
|
||||||
113 6 # q
|
|
||||||
114 5 # r
|
|
||||||
115 6 # s
|
|
||||||
116 5 # t
|
|
||||||
117 6 # u
|
|
||||||
118 6 # v
|
|
||||||
119 8 # w
|
|
||||||
120 6 # x
|
|
||||||
121 6 # y
|
|
||||||
122 6 # z
|
|
||||||
123 6 # {
|
|
||||||
124 3 # |
|
|
||||||
125 6 # }
|
|
||||||
126 8 # ~
|
|
||||||
192 8 # À
|
|
||||||
193 8 # Á
|
|
||||||
200 6 # È
|
|
||||||
201 6 # É
|
|
||||||
204 3 # Ì
|
|
||||||
205 3 # Í
|
|
||||||
210 8 # Ò
|
|
||||||
211 8 # Ó
|
|
||||||
217 7 # Ù
|
|
||||||
218 7 # Ú
|
|
||||||
209 8 # Ñ
|
|
||||||
199 7 # Ç
|
|
||||||
224 6 # à
|
|
||||||
225 6 # á
|
|
||||||
232 6 # è
|
|
||||||
233 6 # é
|
|
||||||
236 3 # ì
|
|
||||||
237 4 # í
|
|
||||||
242 6 # ò
|
|
||||||
243 6 # ó
|
|
||||||
249 6 # ù
|
|
||||||
250 6 # ú
|
|
||||||
241 6 # ñ
|
|
||||||
231 6 # ç
|
|
||||||
161 3 # ¡
|
|
||||||
191 6 # ¿
|
|
||||||
171 9 # «
|
|
||||||
187 9 # »
|
|
||||||
183 3 # ·
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,132 +0,0 @@
|
|||||||
# Font: ari-w9500-condensed — generado desde ari-w9500-condensed.ttf size 11
|
|
||||||
# Generado con tools/font_gen/font_gen.py
|
|
||||||
|
|
||||||
box_width 9
|
|
||||||
box_height 11
|
|
||||||
columns 15
|
|
||||||
|
|
||||||
# codepoint_decimal ancho_visual
|
|
||||||
32 2 # U+0020
|
|
||||||
33 2 # !
|
|
||||||
34 4 # "
|
|
||||||
35 8 # #
|
|
||||||
36 5 # $
|
|
||||||
37 8 # %
|
|
||||||
38 6 # &
|
|
||||||
39 2 # '
|
|
||||||
40 4 # (
|
|
||||||
41 4 # )
|
|
||||||
42 4 # *
|
|
||||||
43 6 # +
|
|
||||||
44 2 # ,
|
|
||||||
45 4 # -
|
|
||||||
46 2 # .
|
|
||||||
47 4 # /
|
|
||||||
48 5 # 0
|
|
||||||
49 4 # 1
|
|
||||||
50 5 # 2
|
|
||||||
51 5 # 3
|
|
||||||
52 5 # 4
|
|
||||||
53 5 # 5
|
|
||||||
54 5 # 6
|
|
||||||
55 5 # 7
|
|
||||||
56 5 # 8
|
|
||||||
57 5 # 9
|
|
||||||
58 2 # :
|
|
||||||
59 2 # ;
|
|
||||||
60 4 # <
|
|
||||||
61 6 # =
|
|
||||||
62 4 # >
|
|
||||||
63 5 # ?
|
|
||||||
64 9 # @
|
|
||||||
65 6 # A
|
|
||||||
66 6 # B
|
|
||||||
67 6 # C
|
|
||||||
68 6 # D
|
|
||||||
69 6 # E
|
|
||||||
70 6 # F
|
|
||||||
71 6 # G
|
|
||||||
72 6 # H
|
|
||||||
73 2 # I
|
|
||||||
74 4 # J
|
|
||||||
75 6 # K
|
|
||||||
76 6 # L
|
|
||||||
77 6 # M
|
|
||||||
78 6 # N
|
|
||||||
79 7 # O
|
|
||||||
80 6 # P
|
|
||||||
81 7 # Q
|
|
||||||
82 6 # R
|
|
||||||
83 6 # S
|
|
||||||
84 6 # T
|
|
||||||
85 6 # U
|
|
||||||
86 6 # V
|
|
||||||
87 8 # W
|
|
||||||
88 6 # X
|
|
||||||
89 6 # Y
|
|
||||||
90 6 # Z
|
|
||||||
91 4 # [
|
|
||||||
92 4 # \
|
|
||||||
93 4 # ]
|
|
||||||
94 6 # ^
|
|
||||||
95 5 # _
|
|
||||||
96 3 # `
|
|
||||||
97 5 # a
|
|
||||||
98 5 # b
|
|
||||||
99 5 # c
|
|
||||||
100 5 # d
|
|
||||||
101 5 # e
|
|
||||||
102 4 # f
|
|
||||||
103 5 # g
|
|
||||||
104 5 # h
|
|
||||||
105 2 # i
|
|
||||||
106 2 # j
|
|
||||||
107 5 # k
|
|
||||||
108 2 # l
|
|
||||||
109 6 # m
|
|
||||||
110 5 # n
|
|
||||||
111 5 # o
|
|
||||||
112 5 # p
|
|
||||||
113 5 # q
|
|
||||||
114 4 # r
|
|
||||||
115 5 # s
|
|
||||||
116 4 # t
|
|
||||||
117 5 # u
|
|
||||||
118 5 # v
|
|
||||||
119 6 # w
|
|
||||||
120 5 # x
|
|
||||||
121 5 # y
|
|
||||||
122 5 # z
|
|
||||||
123 5 # {
|
|
||||||
124 2 # |
|
|
||||||
125 5 # }
|
|
||||||
126 6 # ~
|
|
||||||
192 6 # À
|
|
||||||
193 6 # Á
|
|
||||||
200 6 # È
|
|
||||||
201 6 # É
|
|
||||||
204 2 # Ì
|
|
||||||
205 2 # Í
|
|
||||||
210 7 # Ò
|
|
||||||
211 7 # Ó
|
|
||||||
217 6 # Ù
|
|
||||||
218 6 # Ú
|
|
||||||
209 6 # Ñ
|
|
||||||
199 6 # Ç
|
|
||||||
224 5 # à
|
|
||||||
225 5 # á
|
|
||||||
232 5 # è
|
|
||||||
233 5 # é
|
|
||||||
236 2 # ì
|
|
||||||
237 3 # í
|
|
||||||
242 5 # ò
|
|
||||||
243 5 # ó
|
|
||||||
249 5 # ù
|
|
||||||
250 5 # ú
|
|
||||||
241 5 # ñ
|
|
||||||
231 5 # ç
|
|
||||||
161 2 # ¡
|
|
||||||
191 5 # ¿
|
|
||||||
171 7 # «
|
|
||||||
187 7 # »
|
|
||||||
183 2 # ·
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,132 +0,0 @@
|
|||||||
# Font: ari-w9500 — generado desde ari-w9500.ttf size 11
|
|
||||||
# Generado con tools/font_gen/font_gen.py
|
|
||||||
|
|
||||||
box_width 12
|
|
||||||
box_height 11
|
|
||||||
columns 15
|
|
||||||
|
|
||||||
# codepoint_decimal ancho_visual
|
|
||||||
32 3 # U+0020
|
|
||||||
33 2 # !
|
|
||||||
34 4 # "
|
|
||||||
35 9 # #
|
|
||||||
36 6 # $
|
|
||||||
37 10 # %
|
|
||||||
38 7 # &
|
|
||||||
39 2 # '
|
|
||||||
40 4 # (
|
|
||||||
41 4 # )
|
|
||||||
42 4 # *
|
|
||||||
43 6 # +
|
|
||||||
44 2 # ,
|
|
||||||
45 4 # -
|
|
||||||
46 2 # .
|
|
||||||
47 5 # /
|
|
||||||
48 6 # 0
|
|
||||||
49 5 # 1
|
|
||||||
50 6 # 2
|
|
||||||
51 6 # 3
|
|
||||||
52 6 # 4
|
|
||||||
53 6 # 5
|
|
||||||
54 6 # 6
|
|
||||||
55 6 # 7
|
|
||||||
56 6 # 8
|
|
||||||
57 6 # 9
|
|
||||||
58 2 # :
|
|
||||||
59 2 # ;
|
|
||||||
60 6 # <
|
|
||||||
61 6 # =
|
|
||||||
62 6 # >
|
|
||||||
63 6 # ?
|
|
||||||
64 12 # @
|
|
||||||
65 8 # A
|
|
||||||
66 7 # B
|
|
||||||
67 8 # C
|
|
||||||
68 8 # D
|
|
||||||
69 7 # E
|
|
||||||
70 7 # F
|
|
||||||
71 8 # G
|
|
||||||
72 7 # H
|
|
||||||
73 2 # I
|
|
||||||
74 5 # J
|
|
||||||
75 7 # K
|
|
||||||
76 7 # L
|
|
||||||
77 8 # M
|
|
||||||
78 7 # N
|
|
||||||
79 8 # O
|
|
||||||
80 7 # P
|
|
||||||
81 8 # Q
|
|
||||||
82 7 # R
|
|
||||||
83 7 # S
|
|
||||||
84 6 # T
|
|
||||||
85 7 # U
|
|
||||||
86 7 # V
|
|
||||||
87 10 # W
|
|
||||||
88 8 # X
|
|
||||||
89 8 # Y
|
|
||||||
90 8 # Z
|
|
||||||
91 4 # [
|
|
||||||
92 5 # \
|
|
||||||
93 4 # ]
|
|
||||||
94 6 # ^
|
|
||||||
95 6 # _
|
|
||||||
96 3 # `
|
|
||||||
97 6 # a
|
|
||||||
98 6 # b
|
|
||||||
99 6 # c
|
|
||||||
100 6 # d
|
|
||||||
101 6 # e
|
|
||||||
102 4 # f
|
|
||||||
103 6 # g
|
|
||||||
104 6 # h
|
|
||||||
105 2 # i
|
|
||||||
106 2 # j
|
|
||||||
107 6 # k
|
|
||||||
108 2 # l
|
|
||||||
109 8 # m
|
|
||||||
110 6 # n
|
|
||||||
111 6 # o
|
|
||||||
112 6 # p
|
|
||||||
113 6 # q
|
|
||||||
114 4 # r
|
|
||||||
115 5 # s
|
|
||||||
116 4 # t
|
|
||||||
117 6 # u
|
|
||||||
118 6 # v
|
|
||||||
119 8 # w
|
|
||||||
120 6 # x
|
|
||||||
121 6 # y
|
|
||||||
122 6 # z
|
|
||||||
123 5 # {
|
|
||||||
124 2 # |
|
|
||||||
125 5 # }
|
|
||||||
126 6 # ~
|
|
||||||
192 8 # À
|
|
||||||
193 8 # Á
|
|
||||||
200 7 # È
|
|
||||||
201 7 # É
|
|
||||||
204 2 # Ì
|
|
||||||
205 2 # Í
|
|
||||||
210 8 # Ò
|
|
||||||
211 8 # Ó
|
|
||||||
217 7 # Ù
|
|
||||||
218 7 # Ú
|
|
||||||
209 7 # Ñ
|
|
||||||
199 8 # Ç
|
|
||||||
224 6 # à
|
|
||||||
225 6 # á
|
|
||||||
232 6 # è
|
|
||||||
233 6 # é
|
|
||||||
236 2 # ì
|
|
||||||
237 3 # í
|
|
||||||
242 6 # ò
|
|
||||||
243 6 # ó
|
|
||||||
249 6 # ù
|
|
||||||
250 6 # ú
|
|
||||||
241 6 # ñ
|
|
||||||
231 6 # ç
|
|
||||||
161 2 # ¡
|
|
||||||
191 6 # ¿
|
|
||||||
171 7 # «
|
|
||||||
187 7 # »
|
|
||||||
183 2 # ·
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -29,15 +29,24 @@ keys:
|
|||||||
- id: toggle_vsync
|
- id: toggle_vsync
|
||||||
code: "F8"
|
code: "F8"
|
||||||
desc: "Activa / desactiva VSync"
|
desc: "Activa / desactiva VSync"
|
||||||
- id: menu_toggle
|
- id: screenshot
|
||||||
code: "F9"
|
code: "F9"
|
||||||
desc: "Obri / tanca el menú de servei"
|
desc: "Captura de pantalla (PNG)"
|
||||||
- id: toggle_render_info
|
- id: toggle_render_info
|
||||||
code: "F12"
|
code: "F10"
|
||||||
desc: "Cicle de l'overlay de debug: apagat / dalt / baix"
|
desc: "Cicle de l'overlay de debug: apagat / dalt / baix"
|
||||||
- id: toggle_render_time
|
- id: pause
|
||||||
code: "F11"
|
code: "F11"
|
||||||
desc: "Mostra o amaga el temps de joc dins de l'overlay"
|
desc: "Pausa el joc (o botó START del mando)"
|
||||||
|
- id: menu_toggle
|
||||||
|
code: "F12"
|
||||||
|
desc: "Obri / tanca el menú de servei"
|
||||||
|
- id: editor_toggle
|
||||||
|
code: "9"
|
||||||
|
desc: "Entra/surt del mode editor (només Debug)"
|
||||||
|
- id: editor_picker
|
||||||
|
code: "T"
|
||||||
|
desc: "Obri/tanca el tilepicker dins de l'editor"
|
||||||
- id: exit
|
- id: exit
|
||||||
code: "Escape"
|
code: "Escape"
|
||||||
desc: "Eixir (confirmar amb segona pulsació)"
|
desc: "Eixir (confirmar amb segona pulsació)"
|
||||||
@@ -14,6 +14,7 @@ input:
|
|||||||
shader_off: "Shaders desactivats"
|
shader_off: "Shaders desactivats"
|
||||||
vsync_on: "VSync activat"
|
vsync_on: "VSync activat"
|
||||||
vsync_off: "VSync desactivat"
|
vsync_off: "VSync desactivat"
|
||||||
|
screenshot: "Captura"
|
||||||
shader: "Shader"
|
shader: "Shader"
|
||||||
preset: "Preset"
|
preset: "Preset"
|
||||||
scaling_disabled: "Escalat sense"
|
scaling_disabled: "Escalat sense"
|
||||||
@@ -22,6 +23,25 @@ input:
|
|||||||
scaling_overscan: "Escalat overscan"
|
scaling_overscan: "Escalat overscan"
|
||||||
scaling_integer: "Escalat enter"
|
scaling_integer: "Escalat enter"
|
||||||
press_again_exit: "Prem una altra volta per eixir"
|
press_again_exit: "Prem una altra volta per eixir"
|
||||||
|
paused: "Pausa"
|
||||||
|
editor_on: "Editor ON"
|
||||||
|
editor_off: "Editor OFF"
|
||||||
|
editor_save_error: "Editor: error guardant el mapa"
|
||||||
|
|
||||||
|
hud:
|
||||||
|
p1: "P1"
|
||||||
|
p2: "P2"
|
||||||
|
lives: "Vides"
|
||||||
|
treasures: "Tres"
|
||||||
|
pyramid: "Piramide"
|
||||||
|
room: "Sala"
|
||||||
|
|
||||||
|
# Textos de la transició d'entrada/sortida de sala (FSM de GameScene).
|
||||||
|
# `room_intro` accepta el placeholder `{n}` que es substitueix amb
|
||||||
|
# l'índex de sala actual; veure `GameScene::formatRoomTitle`.
|
||||||
|
game:
|
||||||
|
room_intro: "Sala {n}"
|
||||||
|
room_complete: "Fase completada"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
@@ -34,6 +54,7 @@ menu:
|
|||||||
system: "Sistema"
|
system: "Sistema"
|
||||||
labels:
|
labels:
|
||||||
player_1: "Jugador 1"
|
player_1: "Jugador 1"
|
||||||
|
player_2: "Jugador 2"
|
||||||
gamepad_unavailable: "No disponible"
|
gamepad_unavailable: "No disponible"
|
||||||
items:
|
items:
|
||||||
video_submenu: "Vídeo"
|
video_submenu: "Vídeo"
|
||||||
@@ -46,6 +67,8 @@ menu:
|
|||||||
move_down: "Avall"
|
move_down: "Avall"
|
||||||
move_left: "Esquerra"
|
move_left: "Esquerra"
|
||||||
move_right: "Dreta"
|
move_right: "Dreta"
|
||||||
|
gamepad_p1: "Comandament P1"
|
||||||
|
gamepad_p2: "Comandament P2"
|
||||||
zoom: "Zoom"
|
zoom: "Zoom"
|
||||||
screen: "Pantalla completa"
|
screen: "Pantalla completa"
|
||||||
vsync: "VSync"
|
vsync: "VSync"
|
||||||
@@ -75,3 +98,5 @@ menu:
|
|||||||
render_info_off: "Apagat"
|
render_info_off: "Apagat"
|
||||||
render_info_top: "Dalt"
|
render_info_top: "Dalt"
|
||||||
render_info_bottom: "Baix"
|
render_info_bottom: "Baix"
|
||||||
|
gamepad_none: "Cap"
|
||||||
|
gamepad_disconnected: "Desconnectat"
|
||||||
@@ -14,6 +14,7 @@ input:
|
|||||||
shader_off: "Shaders off"
|
shader_off: "Shaders off"
|
||||||
vsync_on: "VSync on"
|
vsync_on: "VSync on"
|
||||||
vsync_off: "VSync off"
|
vsync_off: "VSync off"
|
||||||
|
screenshot: "Screenshot"
|
||||||
shader: "Shader"
|
shader: "Shader"
|
||||||
preset: "Preset"
|
preset: "Preset"
|
||||||
scaling_disabled: "Scaling off"
|
scaling_disabled: "Scaling off"
|
||||||
@@ -22,6 +23,25 @@ input:
|
|||||||
scaling_overscan: "Scaling overscan"
|
scaling_overscan: "Scaling overscan"
|
||||||
scaling_integer: "Scaling integer"
|
scaling_integer: "Scaling integer"
|
||||||
press_again_exit: "Press again to exit"
|
press_again_exit: "Press again to exit"
|
||||||
|
paused: "Paused"
|
||||||
|
editor_on: "Editor ON"
|
||||||
|
editor_off: "Editor OFF"
|
||||||
|
editor_save_error: "Editor: map save failed"
|
||||||
|
|
||||||
|
hud:
|
||||||
|
p1: "P1"
|
||||||
|
p2: "P2"
|
||||||
|
lives: "Lives"
|
||||||
|
treasures: "Tre"
|
||||||
|
pyramid: "Pyramid"
|
||||||
|
room: "Room"
|
||||||
|
|
||||||
|
# Room transition texts (GameScene FSM intro/outro). The `room_intro`
|
||||||
|
# string accepts a `{n}` placeholder that is replaced with the current
|
||||||
|
# room index; see `GameScene::formatRoomTitle`.
|
||||||
|
game:
|
||||||
|
room_intro: "Room {n}"
|
||||||
|
room_complete: "Stage complete"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@@ -3,18 +3,30 @@
|
|||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||||
#
|
#
|
||||||
# Símbols del grid:
|
# Format del tilemap:
|
||||||
# W = paret (tile 99 del tileset, o (3,3) en 0-indexed)
|
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||||
# F = sòl (llista = un tile aleatori entre aquests per cada casella)
|
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||||
# . = cap tile (els sarcòfags seran actors, ací deixem transparent)
|
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||||
#
|
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||||
# Índexs lineals: els tiles van d'esquerra a dreta, de dalt a baix. Amb un
|
|
||||||
# tileset de 512×256 i tiles de 16×16 hi ha 32 tiles per fila.
|
|
||||||
|
|
||||||
tileset: data/tilesets/tomb.gif
|
tileset: data/tilesets/tomb.gif
|
||||||
tile_size: 16
|
tile_size: 16
|
||||||
origin: [-8, 0]
|
origin: [-8, 0]
|
||||||
|
|
||||||
|
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||||
|
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||||
|
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||||
|
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||||
|
# per desactivar el pintat per a aquest jugador.
|
||||||
|
player1_paint_tile: 0
|
||||||
|
player2_paint_tile: 4
|
||||||
|
|
||||||
|
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||||
|
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||||
|
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||||
|
# s'anul·len (sense push, sense moviment).
|
||||||
|
push_factor: 0.30
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
# decideixen els límits del jugador i les seues caixes de col·lisió. Abans
|
# decideixen els límits del jugador i les seues caixes de col·lisió. Abans
|
||||||
@@ -30,39 +42,29 @@ geometry:
|
|||||||
hud_h: 24
|
hud_h: 24
|
||||||
back_wall_h: 24
|
back_wall_h: 24
|
||||||
side_wall_w: 24
|
side_wall_w: 24
|
||||||
|
# Columna central del tile transitable de la porta. La porta es renderitza
|
||||||
|
# com a sprite 48×32 (3 tiles d'amplada) centrat sobre aquesta columna; els
|
||||||
|
# 16 px centrals (just aquesta columna) són el "forat" del mur trasser per
|
||||||
|
# on el jugador surt de la sala. Les 2 columnes flanquejants segueixen sent
|
||||||
|
# mur visible. El spawn dels jugadors també s'alinea amb aquesta columna.
|
||||||
|
door_col: 10
|
||||||
|
|
||||||
legend:
|
data:
|
||||||
W: 292
|
- [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||||
X: 169
|
- [ 1, 91,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105, 92, 1]
|
||||||
Y: 1
|
- [ 1, 98,113,113, 95,113, 94,113,113,113,113,113,113,113, 94,113,102,113,113, 96, 1]
|
||||||
Z: 202
|
- [ 1, 98, 8, 8, 6, 8, 17, 8, 10, 8, 17, 8, 8, 8, 17, 8, 8, 8, 8, 96, 1]
|
||||||
T: 200
|
- [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
|
||||||
R: 262
|
- [ 1, 98, 17, 8, 9, 2, 8, 10, 8, 8, 8, 8, 10, 8, 8, 26, 8, 8, 17, 96, 1]
|
||||||
G: 263
|
- [ 1, 98, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
|
||||||
H: 230
|
- [ 1, 98, 17, 8, 8, 26, 8, 8, 9, 8, 8, 8, 8, 8, 10, 8, 6, 8, 17, 96, 1]
|
||||||
I: 231
|
- [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
|
||||||
J: 233
|
- [ 1, 98, 17, 8, 10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8, 10, 8, 17, 96, 1]
|
||||||
A: 2
|
- [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 6, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
|
||||||
B: 34
|
- [ 1, 98, 17, 8, 10, 8, 8, 10, 2, 8, 8, 26, 8, 8, 8, 8, 10, 8, 17, 96, 1]
|
||||||
C: 33
|
- [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
|
||||||
D: 32
|
- [ 1, 98, 8, 10, 6, 6, 17, 8, 8, 8, 17, 8, 9, 8, 17, 8, 2, 8, 8, 96, 1]
|
||||||
|
- [ 1, 99, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89,100, 1]
|
||||||
data: |
|
|
||||||
YYYYYYYYYYYYYYYYYYYYY
|
|
||||||
YHJJJJJJJJJJJJJJJJJIY
|
|
||||||
YZWWWWWWWWWWWWWWWWWTY
|
|
||||||
YZDDADDBBDDADDDDBDDTY
|
|
||||||
YZDAAADAAADAAABAAADTY
|
|
||||||
YZDDDCCDDDDDADDDBDDTY
|
|
||||||
YZDAAADAAAAAAADAAADTY
|
|
||||||
YZDDCCDDDDDADDBDDDDTY
|
|
||||||
YZDAAADAAADAAADAAADTY
|
|
||||||
YZDDDDCCDDDDADDDDDDTY
|
|
||||||
YZDAAADAAADAAADAAADTY
|
|
||||||
YZDDDABDDDDADCDDBDDTY
|
|
||||||
YZDAAADAAADAAADAAADTY
|
|
||||||
YZDDBBDDADDDBCDDDDDTY
|
|
||||||
YRXXXXXXXXXXXXXXXXXGY
|
|
||||||
|
|
||||||
# Spawns d'entitats per a aquest nivell. Es llegeixen des de
|
# Spawns d'entitats per a aquest nivell. Es llegeixen des de
|
||||||
# EntityManager::loadFromLevel al GameScene. Convencions:
|
# EntityManager::loadFromLevel al GameScene. Convencions:
|
||||||
@@ -75,7 +77,7 @@ data: |
|
|||||||
#
|
#
|
||||||
# Dos sarcòfags en cantonades oposades → perímetres aïllats (cap tile en
|
# Dos sarcòfags en cantonades oposades → perímetres aïllats (cap tile en
|
||||||
# comú). La porta va al centre inferior, on el tilemap té els tiles
|
# comú). La porta va al centre inferior, on el tilemap té els tiles
|
||||||
# d'obertura (R/G). La momia al corredor central. L'antorxa al costat
|
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
||||||
# superior esquerre per verificar render d'una entitat decorativa.
|
# superior esquerre per verificar render d'una entitat decorativa.
|
||||||
entities:
|
entities:
|
||||||
- type: torch
|
- type: torch
|
||||||
@@ -127,7 +129,9 @@ entities:
|
|||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 4]
|
sarc_position: [3, 4]
|
||||||
- type: door
|
- type: door
|
||||||
position: [9, 1]
|
# Sense `position`: la porta sempre viu al mur trasser. La columna
|
||||||
|
# transitable la dicta `geometry.door_col`; el render i la caixa
|
||||||
|
# de col·lisió es deriven d'aquesta dada al loadFromLevel.
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 7]
|
position: [10, 7]
|
||||||
- type: mummy
|
- type: mummy
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
# per a poder carregar Map sense errors, però el seu propòsit real són
|
# per a poder carregar Map sense errors, però el seu propòsit real són
|
||||||
# els spawns de sota: l'smoke verifica que EntityLoader::readSpawns els
|
# els spawns de sota: l'smoke verifica que EntityLoader::readSpawns els
|
||||||
# retorna correctament i que EntityManager pot instanciar-los i wire-ar-los.
|
# retorna correctament i que EntityManager pot instanciar-los i wire-ar-los.
|
||||||
|
#
|
||||||
|
# Format del tilemap: vegeu pyramid.yaml.
|
||||||
|
|
||||||
tileset: data/tilesets/tomb.gif
|
tileset: data/tilesets/tomb.gif
|
||||||
tile_size: 16
|
tile_size: 16
|
||||||
@@ -18,18 +20,14 @@ geometry:
|
|||||||
back_wall_h: 24
|
back_wall_h: 24
|
||||||
side_wall_w: 24
|
side_wall_w: 24
|
||||||
|
|
||||||
legend:
|
data:
|
||||||
Y: 1
|
- [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||||
X: 169
|
- [ 1,169,169,169,169,169,169,169,169,169,169,169,169, 1]
|
||||||
|
- [ 1,169,169,169,169,169,169,169,169,169,169,169,169, 1]
|
||||||
data: |
|
- [ 1,169,169,169,169,169,169,169,169,169,169,169,169, 1]
|
||||||
YYYYYYYYYYYYYY
|
- [ 1,169,169,169,169,169,169,169,169,169,169,169,169, 1]
|
||||||
YXXXXXXXXXXXXY
|
- [ 1,169,169,169,169,169,169,169,169,169,169,169,169, 1]
|
||||||
YXXXXXXXXXXXXY
|
- [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||||
YXXXXXXXXXXXXY
|
|
||||||
YXXXXXXXXXXXXY
|
|
||||||
YXXXXXXXXXXXXY
|
|
||||||
YYYYYYYYYYYYYY
|
|
||||||
|
|
||||||
entities:
|
entities:
|
||||||
- type: torch
|
- type: torch
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
JASC-PAL
|
||||||
|
0100
|
||||||
|
256
|
||||||
|
0 0 0
|
||||||
|
36 0 0
|
||||||
|
72 0 0
|
||||||
|
109 0 0
|
||||||
|
145 0 0
|
||||||
|
182 0 0
|
||||||
|
218 0 0
|
||||||
|
255 0 0
|
||||||
|
0 36 0
|
||||||
|
36 36 0
|
||||||
|
72 36 0
|
||||||
|
109 36 0
|
||||||
|
145 36 0
|
||||||
|
182 36 0
|
||||||
|
218 36 0
|
||||||
|
255 36 0
|
||||||
|
0 72 0
|
||||||
|
36 72 0
|
||||||
|
72 72 0
|
||||||
|
109 72 0
|
||||||
|
145 72 0
|
||||||
|
182 72 0
|
||||||
|
218 72 0
|
||||||
|
255 72 0
|
||||||
|
0 109 0
|
||||||
|
36 109 0
|
||||||
|
72 109 0
|
||||||
|
109 109 0
|
||||||
|
145 109 0
|
||||||
|
182 109 0
|
||||||
|
218 109 0
|
||||||
|
255 109 0
|
||||||
|
0 145 0
|
||||||
|
36 145 0
|
||||||
|
72 145 0
|
||||||
|
109 145 0
|
||||||
|
145 145 0
|
||||||
|
182 145 0
|
||||||
|
218 145 0
|
||||||
|
255 145 0
|
||||||
|
0 182 0
|
||||||
|
36 182 0
|
||||||
|
72 182 0
|
||||||
|
109 182 0
|
||||||
|
145 182 0
|
||||||
|
182 182 0
|
||||||
|
218 182 0
|
||||||
|
255 182 0
|
||||||
|
0 218 0
|
||||||
|
36 218 0
|
||||||
|
72 218 0
|
||||||
|
109 218 0
|
||||||
|
145 218 0
|
||||||
|
182 218 0
|
||||||
|
218 218 0
|
||||||
|
255 218 0
|
||||||
|
0 255 0
|
||||||
|
36 255 0
|
||||||
|
72 255 0
|
||||||
|
109 255 0
|
||||||
|
145 255 0
|
||||||
|
182 255 0
|
||||||
|
218 255 0
|
||||||
|
255 255 0
|
||||||
|
0 0 85
|
||||||
|
36 0 85
|
||||||
|
72 0 85
|
||||||
|
109 0 85
|
||||||
|
145 0 85
|
||||||
|
182 0 85
|
||||||
|
218 0 85
|
||||||
|
255 0 85
|
||||||
|
0 36 85
|
||||||
|
36 36 85
|
||||||
|
72 36 85
|
||||||
|
109 36 85
|
||||||
|
145 36 85
|
||||||
|
182 36 85
|
||||||
|
218 36 85
|
||||||
|
255 36 85
|
||||||
|
0 72 85
|
||||||
|
36 72 85
|
||||||
|
72 72 85
|
||||||
|
109 72 85
|
||||||
|
145 72 85
|
||||||
|
182 72 85
|
||||||
|
218 72 85
|
||||||
|
255 72 85
|
||||||
|
0 109 85
|
||||||
|
36 109 85
|
||||||
|
72 109 85
|
||||||
|
109 109 85
|
||||||
|
145 109 85
|
||||||
|
182 109 85
|
||||||
|
218 109 85
|
||||||
|
255 109 85
|
||||||
|
0 145 85
|
||||||
|
36 145 85
|
||||||
|
72 145 85
|
||||||
|
109 145 85
|
||||||
|
145 145 85
|
||||||
|
182 145 85
|
||||||
|
218 145 85
|
||||||
|
255 145 85
|
||||||
|
0 182 85
|
||||||
|
36 182 85
|
||||||
|
72 182 85
|
||||||
|
109 182 85
|
||||||
|
145 182 85
|
||||||
|
182 182 85
|
||||||
|
218 182 85
|
||||||
|
255 182 85
|
||||||
|
0 218 85
|
||||||
|
36 218 85
|
||||||
|
72 218 85
|
||||||
|
109 218 85
|
||||||
|
145 218 85
|
||||||
|
182 218 85
|
||||||
|
218 218 85
|
||||||
|
255 218 85
|
||||||
|
0 255 85
|
||||||
|
36 255 85
|
||||||
|
72 255 85
|
||||||
|
109 255 85
|
||||||
|
145 255 85
|
||||||
|
182 255 85
|
||||||
|
218 255 85
|
||||||
|
255 255 85
|
||||||
|
0 0 170
|
||||||
|
36 0 170
|
||||||
|
72 0 170
|
||||||
|
109 0 170
|
||||||
|
145 0 170
|
||||||
|
182 0 170
|
||||||
|
218 0 170
|
||||||
|
255 0 170
|
||||||
|
0 36 170
|
||||||
|
36 36 170
|
||||||
|
72 36 170
|
||||||
|
109 36 170
|
||||||
|
145 36 170
|
||||||
|
182 36 170
|
||||||
|
218 36 170
|
||||||
|
255 36 170
|
||||||
|
0 72 170
|
||||||
|
36 72 170
|
||||||
|
72 72 170
|
||||||
|
109 72 170
|
||||||
|
145 72 170
|
||||||
|
182 72 170
|
||||||
|
218 72 170
|
||||||
|
255 72 170
|
||||||
|
0 109 170
|
||||||
|
36 109 170
|
||||||
|
72 109 170
|
||||||
|
109 109 170
|
||||||
|
145 109 170
|
||||||
|
182 109 170
|
||||||
|
218 109 170
|
||||||
|
255 109 170
|
||||||
|
0 145 170
|
||||||
|
36 145 170
|
||||||
|
72 145 170
|
||||||
|
109 145 170
|
||||||
|
145 145 170
|
||||||
|
182 145 170
|
||||||
|
218 145 170
|
||||||
|
255 145 170
|
||||||
|
0 182 170
|
||||||
|
36 182 170
|
||||||
|
72 182 170
|
||||||
|
109 182 170
|
||||||
|
145 182 170
|
||||||
|
182 182 170
|
||||||
|
218 182 170
|
||||||
|
255 182 170
|
||||||
|
0 218 170
|
||||||
|
36 218 170
|
||||||
|
72 218 170
|
||||||
|
109 218 170
|
||||||
|
145 218 170
|
||||||
|
182 218 170
|
||||||
|
218 218 170
|
||||||
|
255 218 170
|
||||||
|
0 255 170
|
||||||
|
36 255 170
|
||||||
|
72 255 170
|
||||||
|
109 255 170
|
||||||
|
145 255 170
|
||||||
|
182 255 170
|
||||||
|
218 255 170
|
||||||
|
255 255 170
|
||||||
|
0 0 255
|
||||||
|
36 0 255
|
||||||
|
72 0 255
|
||||||
|
109 0 255
|
||||||
|
145 0 255
|
||||||
|
182 0 255
|
||||||
|
218 0 255
|
||||||
|
255 0 255
|
||||||
|
0 36 255
|
||||||
|
36 36 255
|
||||||
|
72 36 255
|
||||||
|
109 36 255
|
||||||
|
145 36 255
|
||||||
|
182 36 255
|
||||||
|
218 36 255
|
||||||
|
255 36 255
|
||||||
|
0 72 255
|
||||||
|
36 72 255
|
||||||
|
72 72 255
|
||||||
|
109 72 255
|
||||||
|
145 72 255
|
||||||
|
182 72 255
|
||||||
|
218 72 255
|
||||||
|
255 72 255
|
||||||
|
0 109 255
|
||||||
|
36 109 255
|
||||||
|
72 109 255
|
||||||
|
109 109 255
|
||||||
|
145 109 255
|
||||||
|
182 109 255
|
||||||
|
218 109 255
|
||||||
|
255 109 255
|
||||||
|
0 145 255
|
||||||
|
36 145 255
|
||||||
|
72 145 255
|
||||||
|
109 145 255
|
||||||
|
145 145 255
|
||||||
|
182 145 255
|
||||||
|
218 145 255
|
||||||
|
255 145 255
|
||||||
|
0 182 255
|
||||||
|
36 182 255
|
||||||
|
72 182 255
|
||||||
|
109 182 255
|
||||||
|
145 182 255
|
||||||
|
182 182 255
|
||||||
|
218 182 255
|
||||||
|
255 182 255
|
||||||
|
0 218 255
|
||||||
|
36 218 255
|
||||||
|
72 218 255
|
||||||
|
109 218 255
|
||||||
|
145 218 255
|
||||||
|
182 218 255
|
||||||
|
218 218 255
|
||||||
|
255 218 255
|
||||||
|
0 255 255
|
||||||
|
36 255 255
|
||||||
|
72 255 255
|
||||||
|
109 255 255
|
||||||
|
145 255 255
|
||||||
|
182 255 255
|
||||||
|
218 255 255
|
||||||
|
255 255 255
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,129 @@
|
|||||||
|
# player animation
|
||||||
|
sprite:
|
||||||
|
path: morcus.gif
|
||||||
|
size: [16, 32]
|
||||||
|
|
||||||
|
# Geometria de col·lisió del jugador (independent de l'animació):
|
||||||
|
# size → costat del quadrat de col·lisió (16×16)
|
||||||
|
# base_size → costat del quadrat visual inferior del sprite (els "peus");
|
||||||
|
# s'usa per centrar la caixa de col·lisió sobre ell.
|
||||||
|
collision:
|
||||||
|
size: 16
|
||||||
|
base_size: 16
|
||||||
|
# Caixa de contacte actor-actor (jugador↔jugador, jugador↔momia/trampa).
|
||||||
|
# Més petita que `size` perquè els contactes d'acció siguin precisos;
|
||||||
|
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
|
||||||
|
# els passadissos. Centrada en el mateix punt que la caixa base.
|
||||||
|
hit_size: 8
|
||||||
|
# Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
|
||||||
|
# · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
|
||||||
|
# d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
|
||||||
|
# d'entitats.
|
||||||
|
# · Contra altres jugadors: la col·lisió és binària per estat aeri.
|
||||||
|
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
|
||||||
|
# estan en l'aire, llavors `body_height` decideix si els rangs
|
||||||
|
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
|
||||||
|
# (ni rebot quan tots dos volen).
|
||||||
|
height: 12
|
||||||
|
|
||||||
|
# Animacions en mapa (clau = nom). Camps per animació:
|
||||||
|
# frames → llista d'índexs dins de l'spritesheet.
|
||||||
|
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
|
||||||
|
# per-frame (mateixa mida que `frames`).
|
||||||
|
# loop → true per defecte. `loop: false` = un sol tret (es queda al
|
||||||
|
# darrer frame i `animationIsCompleted()` passa a true).
|
||||||
|
# loop_from → rebota a aquest índex en passar del final (implica loop).
|
||||||
|
animations:
|
||||||
|
stand_down: { frames: [0] }
|
||||||
|
stand_up: { frames: [4] }
|
||||||
|
stand: { frames: [8] }
|
||||||
|
jump_down: { frames: [12] }
|
||||||
|
jump_up: { frames: [13] }
|
||||||
|
jump: { frames: [14] }
|
||||||
|
stun_down: { frames: [16] }
|
||||||
|
stun_up: { frames: [17] }
|
||||||
|
stun: { frames: [18] }
|
||||||
|
|
||||||
|
walk_down:
|
||||||
|
frames: [0, 1, 2, 3]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
walk_up:
|
||||||
|
frames: [4, 5, 6, 7]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
walk:
|
||||||
|
frames: [8, 9, 10, 11]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt_down:
|
||||||
|
frames: [0, 1, 2, 3]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt_up:
|
||||||
|
frames: [4, 5, 6, 7]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt:
|
||||||
|
frames: [8, 9, 10, 11]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||||
|
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||||
|
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||||
|
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
|
||||||
|
# moviment continu no ha arribat al llindar, la parada és instantània
|
||||||
|
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
|
||||||
|
# reseteja només quan no hi ha ni input ni velocitat residual, així
|
||||||
|
# els canvis de direcció enmig del coast no rearmen el llindar.
|
||||||
|
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
|
||||||
|
inertia:
|
||||||
|
brake_time_ms: 150
|
||||||
|
threshold_ms: 300
|
||||||
|
|
||||||
|
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
|
||||||
|
# intermèdia (efecte purament visual; el moviment continua sense
|
||||||
|
# alterar-se). Els girs de 90° no passen per cap intermedi.
|
||||||
|
turn:
|
||||||
|
duration_ms: 80
|
||||||
|
# Direcció intermèdia per a cada gir de 180° (sentit horari).
|
||||||
|
transitions:
|
||||||
|
west_to_east: down # passa per baix (stand_down)
|
||||||
|
east_to_west: up # passa per dalt (stand_up)
|
||||||
|
north_to_south: east # passa per la dreta (stand, sense flip)
|
||||||
|
south_to_north: west # passa per l'esquerra (stand, amb flip)
|
||||||
|
|
||||||
|
# Salt vertical (px verticals "visuals", no afecta la posició al món).
|
||||||
|
# impulse → velocitat vertical inicial (px/s) en deixar el sòl.
|
||||||
|
# gravity → acceleració descendent constant (px/s²).
|
||||||
|
# landing_stun_ms → temps en què el jugador queda paralitzat en aterrar
|
||||||
|
# (no es pot moure ni tornar a saltar).
|
||||||
|
# shadow_y_offset → desplaçament vertical de la sombra (px, positiu =
|
||||||
|
# puja). Compensa la franja transparent que els
|
||||||
|
# sprites del jugador tenen sota els peus reals
|
||||||
|
# perquè camine "pel mig del passadís" (3 px aquí).
|
||||||
|
# sound → fitxer .wav reproduït al despegue (relatiu a
|
||||||
|
# data/sounds/). Buit o absent = sense so.
|
||||||
|
# land_sound → fitxer .wav reproduït en aterrar.
|
||||||
|
# L'alçada del cos viu al bloc `collision:` (`height`), igual que la resta
|
||||||
|
# d'entitats: és una propietat de col·lisió, no de la física del salt.
|
||||||
|
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
|
||||||
|
jump:
|
||||||
|
impulse: 200
|
||||||
|
gravity: 600
|
||||||
|
landing_stun_ms: 400
|
||||||
|
shadow_y_offset: 3
|
||||||
|
sound: jump.wav
|
||||||
|
land_sound: land.wav
|
||||||
|
|
||||||
|
# Estat HURT: el jugador queda projectat sense control per un impacte
|
||||||
|
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
|
||||||
|
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
|
||||||
|
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
|
||||||
|
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
|
||||||
|
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
|
||||||
|
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
|
||||||
|
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
|
||||||
|
hurt:
|
||||||
|
speed: 120
|
||||||
|
brake_time_ms: 500
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,37 +0,0 @@
|
|||||||
# player animation
|
|
||||||
tileSetFile: player.gif
|
|
||||||
frameWidth: 12
|
|
||||||
frameHeight: 24
|
|
||||||
|
|
||||||
# Geometria de col·lisió del jugador (independent de l'animació):
|
|
||||||
# size → costat del quadrat de col·lisió (16×16)
|
|
||||||
# base_size → costat del quadrat visual inferior del sprite (els "peus");
|
|
||||||
# s'usa per centrar la caixa de col·lisió sobre ell.
|
|
||||||
collision:
|
|
||||||
size: 16
|
|
||||||
base_size: 12
|
|
||||||
|
|
||||||
animations:
|
|
||||||
- name: stand_down
|
|
||||||
frames: [0]
|
|
||||||
|
|
||||||
- name: stand_up
|
|
||||||
frames: [4]
|
|
||||||
|
|
||||||
- name: stand
|
|
||||||
frames: [8]
|
|
||||||
|
|
||||||
- name: walk_down
|
|
||||||
speed: 0.07
|
|
||||||
loopFrom: 0
|
|
||||||
frames: [0, 1, 2, 3]
|
|
||||||
|
|
||||||
- name: walk_up
|
|
||||||
speed: 0.07
|
|
||||||
loopFrom: 0
|
|
||||||
frames: [4, 5, 6, 7]
|
|
||||||
|
|
||||||
- name: walk
|
|
||||||
speed: 0.07
|
|
||||||
loopFrom: 0
|
|
||||||
frames: [8, 9, 10, 11]
|
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,129 @@
|
|||||||
|
# player animation
|
||||||
|
sprite:
|
||||||
|
path: sam.gif
|
||||||
|
size: [16, 32]
|
||||||
|
|
||||||
|
# Geometria de col·lisió del jugador (independent de l'animació):
|
||||||
|
# size → costat del quadrat de col·lisió (16×16)
|
||||||
|
# base_size → costat del quadrat visual inferior del sprite (els "peus");
|
||||||
|
# s'usa per centrar la caixa de col·lisió sobre ell.
|
||||||
|
collision:
|
||||||
|
size: 16
|
||||||
|
base_size: 16
|
||||||
|
# Caixa de contacte actor-actor (jugador↔jugador, jugador↔momia/trampa).
|
||||||
|
# Més petita que `size` perquè els contactes d'acció siguin precisos;
|
||||||
|
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
|
||||||
|
# els passadissos. Centrada en el mateix punt que la caixa base.
|
||||||
|
hit_size: 8
|
||||||
|
# Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
|
||||||
|
# · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
|
||||||
|
# d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
|
||||||
|
# d'entitats.
|
||||||
|
# · Contra altres jugadors: la col·lisió és binària per estat aeri.
|
||||||
|
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
|
||||||
|
# estan en l'aire, llavors `body_height` decideix si els rangs
|
||||||
|
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
|
||||||
|
# (ni rebot quan tots dos volen).
|
||||||
|
height: 12
|
||||||
|
|
||||||
|
# Animacions en mapa (clau = nom). Camps per animació:
|
||||||
|
# frames → llista d'índexs dins de l'spritesheet.
|
||||||
|
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
|
||||||
|
# per-frame (mateixa mida que `frames`).
|
||||||
|
# loop → true per defecte. `loop: false` = un sol tret (es queda al
|
||||||
|
# darrer frame i `animationIsCompleted()` passa a true).
|
||||||
|
# loop_from → rebota a aquest índex en passar del final (implica loop).
|
||||||
|
animations:
|
||||||
|
stand_down: { frames: [1] }
|
||||||
|
stand_up: { frames: [5] }
|
||||||
|
stand: { frames: [9] }
|
||||||
|
jump_down: { frames: [12] }
|
||||||
|
jump_up: { frames: [13] }
|
||||||
|
jump: { frames: [14] }
|
||||||
|
stun_down: { frames: [16] }
|
||||||
|
stun_up: { frames: [17] }
|
||||||
|
stun: { frames: [18] }
|
||||||
|
|
||||||
|
walk_down:
|
||||||
|
frames: [0, 1, 2, 3]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
walk_up:
|
||||||
|
frames: [4, 5, 6, 7]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
walk:
|
||||||
|
frames: [8, 9, 10, 11]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt_down:
|
||||||
|
frames: [0, 1, 2, 3]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt_up:
|
||||||
|
frames: [4, 5, 6, 7]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt:
|
||||||
|
frames: [8, 9, 10, 11]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||||
|
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||||
|
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||||
|
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
|
||||||
|
# moviment continu no ha arribat al llindar, la parada és instantània
|
||||||
|
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
|
||||||
|
# reseteja només quan no hi ha ni input ni velocitat residual, així
|
||||||
|
# els canvis de direcció enmig del coast no rearmen el llindar.
|
||||||
|
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
|
||||||
|
inertia:
|
||||||
|
brake_time_ms: 150
|
||||||
|
threshold_ms: 300
|
||||||
|
|
||||||
|
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
|
||||||
|
# intermèdia (efecte purament visual; el moviment continua sense
|
||||||
|
# alterar-se). Els girs de 90° no passen per cap intermedi.
|
||||||
|
turn:
|
||||||
|
duration_ms: 80
|
||||||
|
# Direcció intermèdia per a cada gir de 180° (sentit horari).
|
||||||
|
transitions:
|
||||||
|
west_to_east: down # passa per baix (stand_down)
|
||||||
|
east_to_west: up # passa per dalt (stand_up)
|
||||||
|
north_to_south: east # passa per la dreta (stand, sense flip)
|
||||||
|
south_to_north: west # passa per l'esquerra (stand, amb flip)
|
||||||
|
|
||||||
|
# Salt vertical (px verticals "visuals", no afecta la posició al món).
|
||||||
|
# impulse → velocitat vertical inicial (px/s) en deixar el sòl.
|
||||||
|
# gravity → acceleració descendent constant (px/s²).
|
||||||
|
# landing_stun_ms → temps en què el jugador queda paralitzat en aterrar
|
||||||
|
# (no es pot moure ni tornar a saltar).
|
||||||
|
# shadow_y_offset → desplaçament vertical de la sombra (px, positiu =
|
||||||
|
# puja). Compensa la franja transparent que els
|
||||||
|
# sprites del jugador tenen sota els peus reals
|
||||||
|
# perquè camine "pel mig del passadís" (3 px aquí).
|
||||||
|
# sound → fitxer .wav reproduït al despegue (relatiu a
|
||||||
|
# data/sounds/). Buit o absent = sense so.
|
||||||
|
# land_sound → fitxer .wav reproduït en aterrar.
|
||||||
|
# L'alçada del cos viu al bloc `collision:` (`height`), igual que la resta
|
||||||
|
# d'entitats: és una propietat de col·lisió, no de la física del salt.
|
||||||
|
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
|
||||||
|
jump:
|
||||||
|
impulse: 200
|
||||||
|
gravity: 600
|
||||||
|
landing_stun_ms: 400
|
||||||
|
shadow_y_offset: 3
|
||||||
|
sound: jump.wav
|
||||||
|
land_sound: land.wav
|
||||||
|
|
||||||
|
# Estat HURT: el jugador queda projectat sense control per un impacte
|
||||||
|
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
|
||||||
|
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
|
||||||
|
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
|
||||||
|
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
|
||||||
|
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
|
||||||
|
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
|
||||||
|
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
|
||||||
|
hurt:
|
||||||
|
speed: 120
|
||||||
|
brake_time_ms: 500
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Configuració de la GameScene: timings i corbes de la FSM de sala
|
||||||
|
# (ROOM_INTRO i ROOM_OUTRO). Tots els valors són ms i tota la geometria
|
||||||
|
# i font del text es reutilitzen del tema UI (data/ui.yaml > pause +
|
||||||
|
# notification). Si manca el fitxer o alguna clau, GameScene cau als
|
||||||
|
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
|
||||||
|
|
||||||
|
room_intro:
|
||||||
|
fade_in_ms: 500 # negre opac → món visible
|
||||||
|
text_delay_ms: 500 # espera amb el món net abans de sortir el text
|
||||||
|
text_enter_ms: 500 # text esquerra → centre
|
||||||
|
text_hold_ms: 1000 # text quiet al centre
|
||||||
|
text_exit_ms: 500 # text centre → fora per l'esquerra
|
||||||
|
|
||||||
|
room_outro:
|
||||||
|
text_enter_ms: 500
|
||||||
|
text_hold_ms: 1000
|
||||||
|
text_exit_ms: 500
|
||||||
|
fade_out_ms: 500 # món visible → negre opac
|
||||||
|
|
||||||
|
text:
|
||||||
|
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
|
||||||
|
# `data/ui.yaml`). El text entra suau i surt accelerant.
|
||||||
|
enter_curve: outCubic
|
||||||
|
exit_curve: inCubic
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,65 @@
|
|||||||
|
# Tema de la UI: índexs de paleta, timings, fonts i flags visuals. Tota
|
||||||
|
# clau absent cau al valor compilat a Defaults::UI (veure defaults.hpp).
|
||||||
|
|
||||||
|
colors:
|
||||||
|
render_info_text: 58
|
||||||
|
render_info_shadow: 16
|
||||||
|
canvas_bg: 72
|
||||||
|
notification_text: 232
|
||||||
|
notification_bg: 64
|
||||||
|
exit_text: 71
|
||||||
|
exit_bg: 2
|
||||||
|
menu_bg: 2
|
||||||
|
menu_border: 232
|
||||||
|
menu_title: 237
|
||||||
|
menu_label: 58
|
||||||
|
menu_value: 232
|
||||||
|
menu_cursor: 71
|
||||||
|
menu_disabled: 16
|
||||||
|
hud_bg: 9
|
||||||
|
hud_label: 58
|
||||||
|
hud_value: 187
|
||||||
|
hud_flash: 237
|
||||||
|
|
||||||
|
timings:
|
||||||
|
notification_ms: 4000
|
||||||
|
exit_ms: 4000
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
hud:
|
||||||
|
name: "8bithud"
|
||||||
|
gif: "data/fonts/8bithud.gif"
|
||||||
|
fnt: "data/fonts/8bithud.fnt"
|
||||||
|
kerning: 1
|
||||||
|
placeholder:
|
||||||
|
name: "ari-w9500-condensed-display"
|
||||||
|
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||||
|
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||||
|
kerning: 1
|
||||||
|
menu_title:
|
||||||
|
name: "ari-w9500-display"
|
||||||
|
gif: "data/fonts/ari-w9500-display.gif"
|
||||||
|
fnt: "data/fonts/ari-w9500-display.fnt"
|
||||||
|
kerning: 0
|
||||||
|
menu_items:
|
||||||
|
name: "ari-w9500-condensed-display"
|
||||||
|
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||||
|
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||||
|
kerning: 0
|
||||||
|
|
||||||
|
hud:
|
||||||
|
bg_enabled: true
|
||||||
|
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
|
||||||
|
flash_ms: 250 # durada del flash quan un jugador rep un hit
|
||||||
|
shake_ms: 300 # durada del shake que acompanya el flash
|
||||||
|
|
||||||
|
# Pausa graduada de la GameScene: el món no para d'un colp, s'atura amb
|
||||||
|
# easing i torna a arrancar amb una altra corba. Els timings es poden
|
||||||
|
# afinar independentment, i les corbes permeten estils diferents entrant
|
||||||
|
# i sortint (p.ex. frenar suau i arrancar seca). Corbes disponibles:
|
||||||
|
# linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
|
||||||
|
pause:
|
||||||
|
enter_ms: 400
|
||||||
|
exit_ms: 250
|
||||||
|
enter_curve: outCubic
|
||||||
|
exit_curve: inCubic
|
||||||
@@ -276,10 +276,74 @@ Canvi a [CMakeLists.txt](../CMakeLists.txt): la còpia de `data/` al build era u
|
|||||||
|
|
||||||
- `Mummy::step` partit: `snapToPlayfieldEdge`, `collectDirectionPools`, `applyChoice` — per mantenir la complexitat cognitiva sota el llindar de clang-tidy (< 25).
|
- `Mummy::step` partit: `snapToPlayfieldEdge`, `collectDirectionPools`, `applyChoice` — per mantenir la complexitat cognitiva sota el llindar de clang-tidy (< 25).
|
||||||
|
|
||||||
## 8. Estat global
|
## 8. Fase 6 — Pintat de tiles (DONE)
|
||||||
|
|
||||||
- **Andamiatge, integració, render animat, falsa perspectiva, moviment, animació direccional de momia**: DONE. El `pyramid.yaml` actual (4 torches, 20 sarcs, 1 door, 2 mummies) es carrega, anima, col·loca amb l'anchor correcte, col·lisiona segons caixa lògica, el z-order interleava per peus, i la momia gira visualment a cada intersecció.
|
Introdueix la mecànica central: el jugador deixa rastre al terra en trepitjar-lo, i els sarcòfags compten tiles **pintats** (no tiles *trepitjats*). Font única de veritat del pintat: una nova capa `PaintLayer` escolta el moviment del Player, filtra pintabilitat i re-publica `tile_painted`, que és el que ara observen els sarcs.
|
||||||
- **Pendents majors**: ENT-HAZ-01 (sistema de vides), ENT-DOOR-01 (transició d'escena), ENT-IA-02 (bias IA), PER-01 (perímetre amb blocked_predicate quan els sarcs siguin veïns).
|
|
||||||
|
### Cadena d'events resultant
|
||||||
|
|
||||||
|
```
|
||||||
|
Player canvi de tile
|
||||||
|
→ bus: on_tile_entered { col, row, paint_tile_index }
|
||||||
|
→ PaintLayer: filtra Map::isPaintable + "primer pinta guanya"
|
||||||
|
→ buffer ARGB + bus: tile_painted { col, row }
|
||||||
|
→ EntityManager fan-out → Sarcophagus::notifyTilePainted
|
||||||
|
→ perímetre complet → OPENING → OPEN → sarcophagus_opened
|
||||||
|
→ tots oberts → Door::open
|
||||||
|
```
|
||||||
|
|
||||||
|
Dos events encadenats perquè el *pintat* no és la *presència*: es filtra per pintabilitat i per "ja pintat", i el re-publish garanteix que els sarcs no vegin falsos positius (p. ex. el jugador lliscant contra un mur, o un tile ja marcat per l'altre jugador).
|
||||||
|
|
||||||
|
### Canvis per fitxer
|
||||||
|
|
||||||
|
| Path | Canvi |
|
||||||
|
|---|---|
|
||||||
|
| [event_bus.hpp](../source/game/entities/event_bus.hpp) | Nou `struct PaintedTile { col, row, tile_index }` al `Payload` variant + overload `publish`. |
|
||||||
|
| [map.hpp](../source/game/map/map.hpp) / [.cpp](../source/game/map/map.cpp) | `Map::isPaintable(col, row)` → `true` si el tile cau dins del playfield i no s'encavalca cap sarc. |
|
||||||
|
| [map/paint_layer.hpp](../source/game/map/paint_layer.hpp) / [.cpp](../source/game/map/paint_layer.cpp) | Classe nova. Subscriu `on_tile_entered`, marca `painted_[row][col]`, blita el tile al buffer ARGB propi, publica `tile_painted`. `render()` anàleg a `Tilemap::render`. |
|
||||||
|
| [player.hpp](../source/game/entities/player.hpp) / [.cpp](../source/game/entities/player.cpp) | Camp `paint_tile_index_` al ctor (default -1). El Player publica `PaintedTile` amb el seu índex en lloc de `TilePos`. |
|
||||||
|
| [entity.hpp](../source/game/entities/entity.hpp) / [.cpp](../source/game/entities/entity.cpp) | Rename `notifyTileEntered` → `notifyTilePainted`. |
|
||||||
|
| [concrete/sarcophagus.hpp](../source/game/entities/concrete/sarcophagus.hpp) / [.cpp](../source/game/entities/concrete/sarcophagus.cpp) | Mateix rename. La lògica del `visited_` és idèntica — només canvia qui li truca el hook. |
|
||||||
|
| [entity_manager.hpp](../source/game/entities/entity_manager.hpp) / [.cpp](../source/game/entities/entity_manager.cpp) | La subscripció canvia d'`on_tile_entered` → `tile_painted`. Fan-out renombrat a `notifyPlayerTilePainted`. |
|
||||||
|
| [game_scene.hpp](../source/game/scenes/game_scene.hpp) / [.cpp](../source/game/scenes/game_scene.cpp) | Membre `std::unique_ptr<PaintLayer> paint_layer_` (declarat després d'`entity_manager_` per garantir ordre invers de destrucció). Helper local `readOptionalInt` llegeix `player1_paint_tile` del YAML del mapa i el passa al Player. `render()` crida `paint_layer_->render()` entre `tilemap_.render()` i els actors; s'amaga al mode editor. |
|
||||||
|
| [data/maps/pyramid.yaml](../data/maps/pyramid.yaml) | Camps nous `player1_paint_tile: 2` i `player2_paint_tile: 2` (índex provisional, canviable sense recompilar). |
|
||||||
|
| [tests/test_entities_smoke.cpp](../tests/test_entities_smoke.cpp) | T9 adaptat: publica `tile_painted` directament al bus per aïllar el fan-out d'EntityManager de la resta de la cadena. |
|
||||||
|
|
||||||
|
### Decisions de disseny
|
||||||
|
|
||||||
|
- **Centralitzat vs per-sarc**: l'estat "pintat" viu al `PaintLayer`, no dins cada sarc. Els sarcs només apunten els tiles del seu perímetre que han estat notificats pintats. Aquesta separació deixa preparat per a items-en-sarc (obrir quan `N` items trobats, no quan tots oberts) i per a altres productors de pintat futurs (enemics, trampes).
|
||||||
|
- **"Primer pinta guanya"**: `PaintLayer::tryPaint` retorna false si el tile ja està pintat. No se sobreescriu amb l'índex d'un altre jugador. Reduïm fonts de decisió inconsistent; el rastre visual és estable.
|
||||||
|
- **Paint tile per jugador**: l'índex l'aporta el YAML del mapa (`player1_paint_tile`, `player2_paint_tile`), no el YAML del jugador. Raó: depèn del tileset del mapa — cada nivell pot oferir rastres diferents; canviar de mapa canvia l'índex vàlid.
|
||||||
|
- **Render al mode editor**: desactivat. L'editor edita el tilemap base; la capa de pintat és d'estat de joc, no de dades del nivell. Quan `editor_.active()`, `SKIP_ACTORS=true` i el `paint_layer_->render()` se salta amb la resta d'actors.
|
||||||
|
- **Resolució**: 16×16 reutilitzant `Map::tile_size` i l'origen del tilemap. El `PaintLayer` emmagatzema `tile_size_` com a camp propi per facilitar una eventual migració a 8×8 (graella 2× més fina) sense tocar la interfície.
|
||||||
|
- **Persistència**: runtime only. No hi ha camp `painted:` al YAML. Entrar al `GameScene` reseteja el pintat.
|
||||||
|
- **No cal `is_blocked` al `sarc->place(...)`** per a aquest nivell: l'`outer_corridor` del `pyramid.yaml` és exactament 1 tile, cosa que garanteix que el perímetre de qualsevol sarc (fins i tot els de cantonades) cau íntegrament dins del playfield i és pintable. Quedaria rellevant si el disseny futur posa sarcs sense passadís al voltant — cobert per PER-01.
|
||||||
|
|
||||||
|
### Verificació
|
||||||
|
|
||||||
|
- `make debug` net, sense warnings ni de clang-tidy ni de cppcheck als fitxers nous.
|
||||||
|
- `./build/test_entities_smoke`: **ALL OK** (14/14). T9 adaptat (publica `tile_painted` en lloc d'`on_tile_entered`); la resta de tests del sistema d'entitats passen sense tocar.
|
||||||
|
- `timeout 5 xvfb-run ./build/aee_2026`: arrenca, log `PaintLayer: 21x15 tiles (336x240 px), tileset data/tilesets/tomb.gif` després de `EntityManager: 27 entitats carregades`, surt amb `Bye!` sense crash.
|
||||||
|
|
||||||
|
### Pendent derivat
|
||||||
|
|
||||||
|
| ID | Descripció | Estat |
|
||||||
|
|---|---|---|
|
||||||
|
| PAINT-01 | **Tile de pintat definitiu** quan l'art estigui madur. Ara `2` provisional al `pyramid.yaml`. Canviar l'índex no demana recompilar. | DEFERRED |
|
||||||
|
| PAINT-02 | **Resolució 8×8** si es vol més granularitat. El `PaintLayer` porta el seu propi `tile_size_`; cal propagar-lo al ctor per a no lligar-lo a `Tilemap::getTileSize()`. També cal decidir com es mapen els perímetres dels sarcs a sub-caselles. | DEFERRED |
|
||||||
|
| PAINT-03 | **Smoke unit del `PaintLayer`**: "primer pinta guanya", filtre d'`isPaintable`, publicació de `tile_painted`. Ara la cadena està coberta end-to-end via T9 (fan-out del bus) i via l'smoke d'arrencada, però el filtre i la deduplicació no tenen test directe. | DEFERRED |
|
||||||
|
| PAINT-04 | **Player 2 cablejat al `GameScene`**: la lectura del YAML i el ctor ja suporten un segon jugador amb `paint_tile_index_` propi; només cal instanciar-lo amb Controls distints (p. ex. WASD) i afegir-lo al vector. | DEFERRED |
|
||||||
|
|
||||||
|
### Memòria durable afectada
|
||||||
|
|
||||||
|
INF-02 queda parcialment cobert: el `PaintedTile` ja porta `tile_index` (per jugador), que és una forma d'identificar qui pinta sense haver de propagar un `player_id` explícit. Si en el futur cal saber *qui* (p. ex. per puntuació individual), caldrà afegir el camp; el canvi és local al struct.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Estat global
|
||||||
|
|
||||||
|
- **Andamiatge, integració, render animat, falsa perspectiva, moviment, animació direccional de momia, pintat de tiles**: DONE. El `pyramid.yaml` actual (4 torches, 20 sarcs, 1 door, 2 mummies) es carrega, anima, col·loca amb l'anchor correcte, col·lisiona segons caixa lògica, el z-order interleava per peus, la momia gira visualment a cada intersecció, i el jugador deixa rastre al terra.
|
||||||
|
- **Pendents majors**: ENT-HAZ-01 (sistema de vides), ENT-DOOR-01 (transició d'escena), ENT-IA-02 (bias IA), PER-01 (perímetre amb blocked_predicate quan els sarcs siguin veïns), PAINT-03 (test unit del PaintLayer), PAINT-04 (Player 2 cablejat).
|
||||||
- **Smoke tests**: 14/14 passant; regla durable de cobrir amb smoke tota lògica no trivial respectada a totes les fases.
|
- **Smoke tests**: 14/14 passant; regla durable de cobrir amb smoke tota lògica no trivial respectada a totes les fases.
|
||||||
|
|
||||||
El document és viu: cada tasca nova hauria d'afegir-s'hi (entrada amb ID, descripció, estat) al fer-la o ajornar-la.
|
El document és viu: cada tasca nova hauria d'afegir-s'hi (entrada amb ID, descripció, estat) al fer-la o ajornar-la.
|
||||||
@@ -5,9 +5,10 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
// Implementació completa de stb_vorbis en aquest TU. HEADER_ONLY es queda
|
// Només declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
|
||||||
// indefinit aquí perquè el .c aporti les definicions. La resta del projecte
|
// d'implementació. Les definicions les aporta source/external/stb_vorbis_impl.cpp
|
||||||
// només veu el forward-decl de jail_audio.hpp.
|
// (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi).
|
||||||
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
// clang-format off
|
// clang-format off
|
||||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
||||||
#include "external/stb_vorbis.c"
|
#include "external/stb_vorbis.c"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <iterator>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -78,6 +79,17 @@ auto GamepadManager::uniqueName(const std::string& base) const -> std::string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
|
void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
|
||||||
|
// Dedup per `instance_id`: SDL encua un SDL_EVENT_GAMEPAD_ADDED per
|
||||||
|
// cada mando ja connectat quan s'inicialitza el subsistema, a més
|
||||||
|
// dels que ja hem obert a `init()`. Sense aquesta guarda, el mateix
|
||||||
|
// mando s'afegiria dues vegades (silenciós + notificat amb sufix
|
||||||
|
// "#2") i qualsevol referència via `getPrimary`/`entries_` seria
|
||||||
|
// ambigua.
|
||||||
|
const bool ALREADY_KNOWN = std::ranges::any_of(
|
||||||
|
entries_,
|
||||||
|
[jid](const Entry& e) { return e.instance_id == jid; });
|
||||||
|
if (ALREADY_KNOWN) { return; }
|
||||||
|
|
||||||
installWebStandardMapping(jid);
|
installWebStandardMapping(jid);
|
||||||
if (!SDL_IsGamepad(jid)) { return; }
|
if (!SDL_IsGamepad(jid)) { return; }
|
||||||
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
|
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
|
||||||
@@ -87,8 +99,23 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad)));
|
std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad)));
|
||||||
std::cout << "GamepadManager: connectat " << name << '\n';
|
// `SDL_GetGamepadPath` pot tornar null a backends que no exposen ruta
|
||||||
entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name});
|
// física (Emscripten, alguns drivers). Buit com a fallback; la
|
||||||
|
// reconnexió es resoldrà per nom.
|
||||||
|
const char* raw_path = SDL_GetGamepadPath(pad);
|
||||||
|
std::string path = (raw_path != nullptr) ? raw_path : "";
|
||||||
|
std::cout << "GamepadManager: connectat " << name
|
||||||
|
<< (path.empty() ? "" : " [" + path + "]") << '\n';
|
||||||
|
entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name, .path = path});
|
||||||
|
|
||||||
|
// Hot-plug: si alguna assignació pendent encaixa amb aquest mando,
|
||||||
|
// la vinculem ara. Primer intentem per path exacte, després per nom.
|
||||||
|
for (auto& a : assignments_) {
|
||||||
|
if (a.pad != nullptr) { continue; }
|
||||||
|
if (a.name.empty() && a.path.empty()) { continue; }
|
||||||
|
rebindAssignment(a);
|
||||||
|
}
|
||||||
|
|
||||||
if (notify) {
|
if (notify) {
|
||||||
Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_connected"));
|
Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_connected"));
|
||||||
}
|
}
|
||||||
@@ -100,9 +127,18 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
|
|||||||
[jid](const Entry& e) { return e.instance_id == jid; });
|
[jid](const Entry& e) { return e.instance_id == jid; });
|
||||||
if (it == entries_.end()) { return; }
|
if (it == entries_.end()) { return; }
|
||||||
std::string name = std::move(it->pretty_name);
|
std::string name = std::move(it->pretty_name);
|
||||||
if (it->pad != nullptr) { SDL_CloseGamepad(it->pad); }
|
SDL_Gamepad* pad = it->pad;
|
||||||
|
if (pad != nullptr) { SDL_CloseGamepad(pad); }
|
||||||
std::cout << "GamepadManager: desconnectat " << name << '\n';
|
std::cout << "GamepadManager: desconnectat " << name << '\n';
|
||||||
entries_.erase(it);
|
entries_.erase(it);
|
||||||
|
|
||||||
|
// Qualsevol assignació que apuntava a aquest mando queda amb `pad`
|
||||||
|
// a null però conservem nom/ruta: si el mando es torna a connectar,
|
||||||
|
// el hot-plug el re-vincularà automàticament.
|
||||||
|
for (auto& a : assignments_) {
|
||||||
|
if (a.pad == pad) { a.pad = nullptr; }
|
||||||
|
}
|
||||||
|
|
||||||
Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_disconnected"));
|
Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_disconnected"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,11 +149,22 @@ void GamepadManager::init() {
|
|||||||
<< SDL_GetError() << '\n';
|
<< SDL_GetError() << '\n';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// `gamecontrollerdb.txt` és opcional. Si no existeix, SDL retorna -1 i
|
// `gamecontrollerdb.txt` és opcional. Viu a l'arrel del projecte
|
||||||
// continuem amb les definicions internes del sistema.
|
// (no a `data/`) perquè SDL_AddGamepadMappingsFromFile només sap
|
||||||
const int ADDED = SDL_AddGamepadMappingsFromFile("data/input/gamecontrollerdb.txt");
|
// llegir del filesystem real, no del pack. CMake el copia al costat
|
||||||
if (ADDED > 0) {
|
// del binari a cada build perquè SDL_GetBasePath el trobi tant en
|
||||||
std::cout << "GamepadManager: " << ADDED << " mappings de gamecontrollerdb.txt\n";
|
// dev com en release. Sempre informem del resultat per facilitar el
|
||||||
|
// diagnòstic de mandos no reconeguts.
|
||||||
|
std::string db_path = "gamecontrollerdb.txt";
|
||||||
|
if (const char* base = SDL_GetBasePath()) {
|
||||||
|
db_path = std::string(base) + "gamecontrollerdb.txt";
|
||||||
|
}
|
||||||
|
const int ADDED = SDL_AddGamepadMappingsFromFile(db_path.c_str());
|
||||||
|
if (ADDED < 0) {
|
||||||
|
std::cout << "GamepadManager: no s'ha pogut carregar " << db_path
|
||||||
|
<< ": " << SDL_GetError() << '\n';
|
||||||
|
} else {
|
||||||
|
std::cout << "GamepadManager: " << ADDED << " mappings carregats de " << db_path << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@@ -128,6 +175,7 @@ void GamepadManager::init() {
|
|||||||
}
|
}
|
||||||
SDL_free(ids);
|
SDL_free(ids);
|
||||||
}
|
}
|
||||||
|
std::cout << "GamepadManager: " << entries_.size() << " mando(s) detectat(s) a l'inici\n";
|
||||||
initialized_ = true;
|
initialized_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,3 +209,108 @@ auto GamepadManager::getCount() const -> std::size_t {
|
|||||||
auto GamepadManager::getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad* {
|
auto GamepadManager::getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad* {
|
||||||
return idx < entries_.size() ? entries_[idx].pad : nullptr;
|
return idx < entries_.size() ? entries_[idx].pad : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto GamepadManager::getAll() const -> std::vector<Info> {
|
||||||
|
std::vector<Info> result;
|
||||||
|
result.reserve(entries_.size());
|
||||||
|
std::ranges::transform(entries_, std::back_inserter(result), [](const Entry& e) {
|
||||||
|
return Info{.pad = e.pad, .name = e.pretty_name, .path = e.path};
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GamepadManager::getForPlayer(int player_id) const -> SDL_Gamepad* {
|
||||||
|
if (player_id < 0 || player_id >= MAX_PLAYERS) { return nullptr; }
|
||||||
|
return assignments_[static_cast<std::size_t>(player_id)].pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GamepadManager::rebindAssignment(Assignment& a) {
|
||||||
|
// Primera passada: coincidència exacta per path. És el cas estable
|
||||||
|
// (mateix mando, mateix port) i no canvia res de l'assignació.
|
||||||
|
if (!a.path.empty()) {
|
||||||
|
auto it = std::ranges::find_if(
|
||||||
|
entries_,
|
||||||
|
[&](const Entry& e) { return !e.path.empty() && e.path == a.path; });
|
||||||
|
if (it != entries_.end()) {
|
||||||
|
a.pad = it->pad;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Segona passada: coincidència per nom (mando canviat de port USB).
|
||||||
|
// Acceptem la primera entrada que no estigui ja vinculada a una altra
|
||||||
|
// assignació — així dos mandos amb el mateix nom no es pisen.
|
||||||
|
if (!a.name.empty()) {
|
||||||
|
for (const auto& e : entries_) {
|
||||||
|
if (e.pretty_name != a.name) { continue; }
|
||||||
|
const bool TAKEN = std::ranges::any_of(
|
||||||
|
assignments_,
|
||||||
|
[&](const Assignment& other) { return &other != &a && other.pad == e.pad; });
|
||||||
|
if (TAKEN) { continue; }
|
||||||
|
a.pad = e.pad;
|
||||||
|
a.path = e.path; // actualitzem ruta persistida
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.pad = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GamepadManager::assignToPlayer(int player_id, const std::string& name, const std::string& path) {
|
||||||
|
if (player_id < 0 || player_id >= MAX_PLAYERS) { return; }
|
||||||
|
auto& a = assignments_[static_cast<std::size_t>(player_id)];
|
||||||
|
a.name = name;
|
||||||
|
a.path = path;
|
||||||
|
a.pad = nullptr;
|
||||||
|
if (name.empty() && path.empty()) { return; }
|
||||||
|
rebindAssignment(a);
|
||||||
|
// Last-wins: si l'altre jugador tenia el mateix mando vinculat, el
|
||||||
|
// desvinculem (conservant el seu nom/path perquè el menú pugui
|
||||||
|
// mostrar l'estat "pendent" i oferir re-selecció).
|
||||||
|
if (a.pad != nullptr) {
|
||||||
|
for (auto& other : assignments_) {
|
||||||
|
if (&other != &a && other.pad == a.pad) {
|
||||||
|
std::cout << "GamepadManager: mando " << a.name
|
||||||
|
<< " reassignat (last-wins)\n";
|
||||||
|
other.pad = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Traç de l'assignació persistida (vinculada o pendent) perquè quede
|
||||||
|
// visible a la consola igual que les autoassignacions.
|
||||||
|
if (a.pad != nullptr) {
|
||||||
|
std::cout << "GamepadManager: assignat " << a.name
|
||||||
|
<< " → P" << (player_id + 1) << '\n';
|
||||||
|
} else {
|
||||||
|
std::cout << "GamepadManager: P" << (player_id + 1) << " pendent ("
|
||||||
|
<< (a.name.empty() ? a.path : a.name) << " no connectat)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GamepadManager::unassignFromPlayer(int player_id) {
|
||||||
|
if (player_id < 0 || player_id >= MAX_PLAYERS) { return; }
|
||||||
|
assignments_[static_cast<std::size_t>(player_id)] = Assignment{};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GamepadManager::autoAssignFreeSlots() -> std::vector<AutoAssigned> {
|
||||||
|
std::vector<AutoAssigned> assigned;
|
||||||
|
for (int pid = 0; pid < MAX_PLAYERS; ++pid) {
|
||||||
|
auto& slot = assignments_[static_cast<std::size_t>(pid)];
|
||||||
|
if (slot.pad != nullptr) { continue; }
|
||||||
|
// Primer mando que cap altre slot no haja reclamat. Així respectem
|
||||||
|
// qualsevol assignació persistida que ja haja vinculat un pad
|
||||||
|
// concret i no el reassignem a un jugador diferent.
|
||||||
|
for (const auto& e : entries_) {
|
||||||
|
const bool TAKEN = std::ranges::any_of(
|
||||||
|
assignments_,
|
||||||
|
[&](const Assignment& other) { return other.pad == e.pad; });
|
||||||
|
if (TAKEN) { continue; }
|
||||||
|
slot.pad = e.pad;
|
||||||
|
slot.name = e.pretty_name;
|
||||||
|
slot.path = e.path;
|
||||||
|
assigned.push_back(AutoAssigned{.player_id = pid, .name = e.pretty_name, .path = e.path});
|
||||||
|
std::cout << "GamepadManager: auto-assigned " << e.pretty_name
|
||||||
|
<< " → P" << (pid + 1) << '\n';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assigned;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -41,22 +42,83 @@ class GamepadManager {
|
|||||||
void handleEvent(const SDL_Event& ev);
|
void handleEvent(const SDL_Event& ev);
|
||||||
|
|
||||||
// Primary = primer gamepad al vector. `nullptr` si no n'hi ha cap.
|
// Primary = primer gamepad al vector. `nullptr` si no n'hi ha cap.
|
||||||
// Ús típic: Player 1 i navegació del ServiceMenu.
|
// Ús típic: navegació del ServiceMenu. Per al gameplay els jugadors
|
||||||
|
// usen `getForPlayer(id)`.
|
||||||
[[nodiscard]] auto getPrimary() const -> SDL_Gamepad*;
|
[[nodiscard]] auto getPrimary() const -> SDL_Gamepad*;
|
||||||
|
|
||||||
[[nodiscard]] auto getCount() const -> std::size_t;
|
[[nodiscard]] auto getCount() const -> std::size_t;
|
||||||
|
|
||||||
// Retorna el mand del jugador `idx`-èsim (0-indexed) o `nullptr` si
|
// Retorna el mand del jugador `idx`-èsim (0-indexed) en l'ordre de
|
||||||
// encara no hi ha prou mandos connectats. Reservat per a multi-jugador.
|
// connexió. Per al gameplay, `getForPlayer` fa servir l'assignació
|
||||||
|
// persistida, no l'ordre de connexió.
|
||||||
[[nodiscard]] auto getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad*;
|
[[nodiscard]] auto getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad*;
|
||||||
|
|
||||||
|
// Informació exposada al menú de servei (llista de mandos
|
||||||
|
// disponibles). `path` pot estar buit si SDL no l'exposa (p. ex. a
|
||||||
|
// alguns backends web).
|
||||||
|
struct Info {
|
||||||
|
SDL_Gamepad* pad{nullptr};
|
||||||
|
std::string name;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
[[nodiscard]] auto getAll() const -> std::vector<Info>;
|
||||||
|
|
||||||
|
// Retorna el mand assignat al jugador `player_id` (0 = P1, 1 = P2)
|
||||||
|
// o `nullptr` si no n'hi ha cap. L'assignació la fixa
|
||||||
|
// `assignToPlayer` (cridat per Options en carregar `config.yaml` o
|
||||||
|
// des del ServiceMenu quan l'usuari tria un mando).
|
||||||
|
[[nodiscard]] auto getForPlayer(int player_id) const -> SDL_Gamepad*;
|
||||||
|
|
||||||
|
// Assigna un mando per nom + ruta física. Si ja hi ha un mando
|
||||||
|
// connectat que coincideix per ruta (o per nom com a fallback), el
|
||||||
|
// vincula immediatament; si no, l'assignació queda pendent i
|
||||||
|
// s'aplicarà si el mando es connecta més tard (hot-plug).
|
||||||
|
// Política last-wins: si l'altre jugador tenia el mateix mando
|
||||||
|
// vinculat, se'l desvincula silenciosament.
|
||||||
|
void assignToPlayer(int player_id, const std::string& name, const std::string& path);
|
||||||
|
|
||||||
|
// Esborra l'assignació (per si l'usuari trie "Cap" al menú).
|
||||||
|
void unassignFromPlayer(int player_id);
|
||||||
|
|
||||||
|
// Per a cada slot buit, hi vincula el primer mando connectat que no
|
||||||
|
// estigui reclamat per un altre slot. Equivalent al patró de
|
||||||
|
// `coffee_crisis_arcade_edition`: les assignacions persistides tenen
|
||||||
|
// prioritat; els slots que queden buits després s'emplenen per
|
||||||
|
// ordre. Retorna la llista de parells (player_id, name, path) que
|
||||||
|
// s'han vinculat, per a que Options pugui persistir-los.
|
||||||
|
struct AutoAssigned {
|
||||||
|
int player_id;
|
||||||
|
std::string name;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
auto autoAssignFreeSlots() -> std::vector<AutoAssigned>;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Entry {
|
struct Entry {
|
||||||
SDL_Gamepad* pad{nullptr};
|
SDL_Gamepad* pad{nullptr};
|
||||||
SDL_JoystickID instance_id{0};
|
SDL_JoystickID instance_id{0};
|
||||||
std::string pretty_name;
|
std::string pretty_name;
|
||||||
|
std::string path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Assignació persistent per jugador. `name` i `path` venen d'Options
|
||||||
|
// (o del menú de servei) i poden referenciar un mando encara no
|
||||||
|
// connectat; en aquest cas `pad` és `nullptr` i el hot-plug el
|
||||||
|
// vincularà quan aparegui.
|
||||||
|
struct Assignment {
|
||||||
|
std::string name;
|
||||||
|
std::string path;
|
||||||
|
SDL_Gamepad* pad{nullptr};
|
||||||
|
};
|
||||||
|
static constexpr int MAX_PLAYERS = 2;
|
||||||
|
std::array<Assignment, MAX_PLAYERS> assignments_{};
|
||||||
|
|
||||||
|
// Busca a `entries_` el millor match per a una assignació (primer
|
||||||
|
// per path, després per nom). Retorna l'iterador o `entries_.end()`.
|
||||||
|
// També accepta assignar `path` nou si ha coincidit per nom
|
||||||
|
// (cas mando reenchufat a un altre port USB).
|
||||||
|
void rebindAssignment(Assignment& a);
|
||||||
|
|
||||||
GamepadManager() = default;
|
GamepadManager() = default;
|
||||||
~GamepadManager() = default;
|
~GamepadManager() = default;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
#include "core/rendering/notifier.hpp"
|
#include "core/rendering/notifier.hpp"
|
||||||
#include "core/rendering/render_info.hpp"
|
#include "core/rendering/render_info.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/screenshot.hpp"
|
||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/options_apply.hpp"
|
#include "game/options_apply.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
@@ -24,7 +26,7 @@ namespace GlobalInputs {
|
|||||||
// La lliguem a la durada de la notificació EXIT perquè el missatge i
|
// La lliguem a la durada de la notificació EXIT perquè el missatge i
|
||||||
// la confirmació expiren alhora — si no, l'usuari podria veure el
|
// la confirmació expiren alhora — si no, l'usuari podria veure el
|
||||||
// text però ja no ser dins del rang de confirmació (o al revés).
|
// text però ja no ser dins del rang de confirmació (o al revés).
|
||||||
constexpr float EXIT_WINDOW_S = static_cast<float>(Notification::EXIT.time_ms) / 1000.0F;
|
// El valor concret viu a data/ui.yaml (timings.exit_ms) via UiConfig.
|
||||||
constexpr int MIN_ZOOM = 1;
|
constexpr int MIN_ZOOM = 1;
|
||||||
constexpr int MAX_ZOOM = 6;
|
constexpr int MAX_ZOOM = 6;
|
||||||
|
|
||||||
@@ -67,6 +69,18 @@ namespace GlobalInputs {
|
|||||||
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
|
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Volca el canvas ARGB actual a PNG dins de `screenshots/`. Si ha
|
||||||
|
// fallat l'escriptura, el handler no mostra notificació (retorn buit).
|
||||||
|
void onScreenshot() {
|
||||||
|
const auto* buffer = Screen::get().getCanvasArgb();
|
||||||
|
const int W = Screen::get().getCanvasWidth();
|
||||||
|
const int H = Screen::get().getCanvasHeight();
|
||||||
|
const std::string FILENAME = Screenshot::save(buffer, W, H);
|
||||||
|
if (!FILENAME.empty()) {
|
||||||
|
Notification::notify(Locale::get().text("input.screenshot") + ' ' + FILENAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onToggleShader() {
|
void onToggleShader() {
|
||||||
const bool NEW_ENABLED = !Options::video().shader.enabled;
|
const bool NEW_ENABLED = !Options::video().shader.enabled;
|
||||||
Options::setShaderEnabled(NEW_ENABLED);
|
Options::setShaderEnabled(NEW_ENABLED);
|
||||||
@@ -140,17 +154,13 @@ namespace GlobalInputs {
|
|||||||
Options::setVideoRenderInfoPosition(static_cast<Defaults::RenderInfoPosition>(NEXT));
|
Options::setVideoRenderInfoPosition(static_cast<Defaults::RenderInfoPosition>(NEXT));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onToggleRenderTime() {
|
|
||||||
Options::setVideoRenderInfoShowTime(!Options::video().render_info_show_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onExit() {
|
void onExit() {
|
||||||
if (exit_window_s > 0.0F) {
|
if (exit_window_s > 0.0F) {
|
||||||
quit_requested = true;
|
quit_requested = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
exit_window_s = EXIT_WINDOW_S;
|
exit_window_s = static_cast<float>(UiConfig::get().exitMs()) / 1000.0F;
|
||||||
Notification::notify(Locale::get().text("input.press_again_exit"), Notification::EXIT);
|
Notification::notify(Locale::get().text("input.press_again_exit"), Notification::exitPreset());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -171,6 +181,8 @@ namespace GlobalInputs {
|
|||||||
onToggleShader();
|
onToggleShader();
|
||||||
} else if (SC == KeyConfig::get().scancode("toggle_vsync")) {
|
} else if (SC == KeyConfig::get().scancode("toggle_vsync")) {
|
||||||
onToggleVSync();
|
onToggleVSync();
|
||||||
|
} else if (SC == KeyConfig::get().scancode("screenshot")) {
|
||||||
|
onScreenshot();
|
||||||
} else if (SC == KeyConfig::get().scancode("next_shader")) {
|
} else if (SC == KeyConfig::get().scancode("next_shader")) {
|
||||||
onNextShader();
|
onNextShader();
|
||||||
} else if (SC == KeyConfig::get().scancode("next_preset")) {
|
} else if (SC == KeyConfig::get().scancode("next_preset")) {
|
||||||
@@ -179,8 +191,6 @@ namespace GlobalInputs {
|
|||||||
onNextScalingMode();
|
onNextScalingMode();
|
||||||
} else if (SC == KeyConfig::get().scancode("toggle_render_info")) {
|
} else if (SC == KeyConfig::get().scancode("toggle_render_info")) {
|
||||||
onCycleRenderInfo();
|
onCycleRenderInfo();
|
||||||
} else if (SC == KeyConfig::get().scancode("toggle_render_time")) {
|
|
||||||
onToggleRenderTime();
|
|
||||||
} else if (SC == KeyConfig::get().scancode("exit")) {
|
} else if (SC == KeyConfig::get().scancode("exit")) {
|
||||||
onExit();
|
onExit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,18 @@ auto KeyConfig::get() -> KeyConfig& {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void KeyConfig::load(const std::string& file_path) {
|
void KeyConfig::load(const std::string& file_path) {
|
||||||
const auto FULL = Resource::Helper::resolve(file_path);
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
std::ifstream file(FULL);
|
if (bytes.empty()) {
|
||||||
if (!file.is_open()) {
|
std::cerr << "KeyConfig: no s'ha pogut obrir " << file_path << '\n';
|
||||||
std::cerr << "KeyConfig: no s'ha pogut obrir " << FULL << '\n';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto yaml = fkyaml::node::deserialize(file);
|
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("keys") || !yaml["keys"].is_sequence()) {
|
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
|
||||||
std::cerr << "KeyConfig: falta la secció 'keys' a " << FULL << '\n';
|
std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bindings_.clear();
|
bindings_.clear();
|
||||||
@@ -38,9 +39,9 @@ void KeyConfig::load(const std::string& file_path) {
|
|||||||
}
|
}
|
||||||
bindings_[ID] = SC;
|
bindings_[ID] = SC;
|
||||||
}
|
}
|
||||||
std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << FULL << '\n';
|
std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << file_path << '\n';
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "KeyConfig: error parsejant " << FULL << ": " << e.what() << '\n';
|
std::cerr << "KeyConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,20 +34,21 @@ auto Locale::get() -> Locale& {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Locale::load(const std::string& file_path) {
|
void Locale::load(const std::string& file_path) {
|
||||||
const auto FULL = Resource::Helper::resolve(file_path);
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
std::ifstream file(FULL);
|
if (bytes.empty()) {
|
||||||
if (!file.is_open()) {
|
std::cerr << "Locale: no s'ha pogut obrir " << file_path << '\n';
|
||||||
std::cerr << "Locale: no s'ha pogut obrir " << FULL << '\n';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto yaml = fkyaml::node::deserialize(file);
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
strings_.clear();
|
strings_.clear();
|
||||||
flatten(yaml, "", strings_);
|
flatten(yaml, "", strings_);
|
||||||
std::cout << "Locale: " << strings_.size() << " traduccions des de " << FULL << '\n';
|
std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n';
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "Locale: error parsejant " << FULL << ": " << e.what() << '\n';
|
std::cerr << "Locale: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace GIF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inicializa el diccionario LZW con los valores iniciales
|
// Inicializa el diccionario LZW con los valores iniciales
|
||||||
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
|
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) {
|
||||||
int size = 1 << code_length;
|
int size = 1 << code_length;
|
||||||
dictionary.resize(1 << (code_length + 1));
|
dictionary.resize(1 << (code_length + 1));
|
||||||
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
||||||
@@ -82,7 +82,7 @@ namespace GIF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Agrega una nueva entrada al diccionario
|
// Agrega una nueva entrada al diccionario
|
||||||
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming)
|
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) {
|
||||||
uint8_t first_byte;
|
uint8_t first_byte;
|
||||||
if (code == dictionary_ind) {
|
if (code == dictionary_ind) {
|
||||||
first_byte = findFirstByte(dictionary, prev);
|
first_byte = findFirstByte(dictionary, prev);
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/rendering/notifier.hpp"
|
#include "core/rendering/notifier.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
|
|
||||||
// Temes per a les notificacions de pantalla. Un "preset" captura els dos
|
// Temes per a les notificacions de pantalla. Un "preset" captura els dos
|
||||||
// colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
|
// colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
|
||||||
// en mil·lisegons. Per a llançar una notificació amb un preset concret,
|
// en mil·lisegons. Els valors concrets viuen a data/ui.yaml (carregat per
|
||||||
// cridar Notification::notify(text, preset); si s'omet, s'usa DEFAULT.
|
// UiConfig); per llançar una notificació amb un preset concret, cridar
|
||||||
|
// Notification::notify(text, preset). Sense preset s'usa el DEFAULT.
|
||||||
namespace Notification {
|
namespace Notification {
|
||||||
|
|
||||||
struct Preset {
|
struct Preset {
|
||||||
@@ -19,27 +20,36 @@ namespace Notification {
|
|||||||
int time_ms;
|
int time_ms;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tema per defecte: text clar sobre fons fosc. Els valors concrets
|
// Preset per defecte: text clar sobre fons fosc. Es construeix en cada
|
||||||
// viuen a `Defaults::UI` — tocar l'aparença global es fa allà.
|
// crida a partir d'UiConfig perquè els valors no són constexpr: viuen a
|
||||||
constexpr Preset DEFAULT{
|
// data/ui.yaml i es carreguen a l'arrencada.
|
||||||
.text_color = Defaults::UI::NOTIFICATION_TEXT,
|
[[nodiscard]] inline auto defaultPreset() -> Preset {
|
||||||
.bg_color = Defaults::UI::NOTIFICATION_BG,
|
return {
|
||||||
.time_ms = Defaults::UI::NOTIFICATION_TIME_MS,
|
.text_color = UiConfig::get().notificationText(),
|
||||||
|
.bg_color = UiConfig::get().notificationBg(),
|
||||||
|
.time_ms = UiConfig::get().notificationMs(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Preset per al missatge de confirmació d'eixida (ESC doble). Color
|
// Preset per al missatge de confirmació d'eixida (ESC doble). Color
|
||||||
// d'accent per a distingir-lo d'una notificació informativa; la durada
|
// d'accent per distingir-lo d'una notificació informativa; la durada
|
||||||
// coincideix amb la finestra durant la qual la segona pulsació d'ESC
|
// coincideix amb la finestra durant la qual la segona pulsació d'ESC
|
||||||
// confirma el tancament — veure GlobalInputs::onExit.
|
// confirma el tancament — veure GlobalInputs::onExit.
|
||||||
constexpr Preset EXIT{
|
[[nodiscard]] inline auto exitPreset() -> Preset {
|
||||||
.text_color = Defaults::UI::EXIT_TEXT,
|
return {
|
||||||
.bg_color = Defaults::UI::EXIT_BG,
|
.text_color = UiConfig::get().exitText(),
|
||||||
.time_ms = Defaults::UI::EXIT_TIME_MS,
|
.bg_color = UiConfig::get().exitBg(),
|
||||||
|
.time_ms = UiConfig::get().exitMs(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Helper de conveniència: aplica un preset (per defecte DEFAULT) cridant
|
// Helper de conveniència: aplica un preset (per defecte el DEFAULT)
|
||||||
// la API crua de Notifier. La direcció game → core és la correcta.
|
// cridant la API crua de Notifier. La direcció game → core és correcta.
|
||||||
inline void notify(const std::string& text, const Preset& preset = DEFAULT) {
|
inline void notify(const std::string& text) {
|
||||||
|
const auto P = defaultPreset();
|
||||||
|
Notifier::get().notify(text, P.text_color, P.bg_color, P.time_ms);
|
||||||
|
}
|
||||||
|
inline void notify(const std::string& text, const Preset& preset) {
|
||||||
Notifier::get().notify(text, preset.text_color, preset.bg_color, preset.time_ms);
|
Notifier::get().notify(text, preset.text_color, preset.bg_color, preset.time_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
|
||||||
#include "core/rendering/canvas.hpp"
|
#include "core/rendering/canvas.hpp"
|
||||||
|
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -265,6 +266,34 @@ void Screen::blendRect(int x, int y, int w, int h, std::uint32_t argb, std::uint
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Screen::windowToCanvas(float window_x, float window_y) const -> SDL_FPoint {
|
||||||
|
// Path fallback: SDL ja porta un helper que desfà el
|
||||||
|
// logical_presentation automàticament. Evita que haguem de replicar
|
||||||
|
// les regles de letterbox/integer/etc.
|
||||||
|
if (renderer_ != nullptr) {
|
||||||
|
float cx = 0.0F;
|
||||||
|
float cy = 0.0F;
|
||||||
|
SDL_RenderCoordinatesFromWindow(renderer_, window_x, window_y, &cx, &cy);
|
||||||
|
return {cx, cy};
|
||||||
|
}
|
||||||
|
// Path GPU: el canvas es pinta al viewport que calcula
|
||||||
|
// `computeViewport` a partir del mode d'escalat i la mida del
|
||||||
|
// swapchain. Fem el mateix càlcul aquí per trobar la inversa.
|
||||||
|
int win_w = 0;
|
||||||
|
int win_h = 0;
|
||||||
|
SDL_GetWindowSize(window_, &win_w, &win_h);
|
||||||
|
if (win_w <= 0 || win_h <= 0) { return {-1.0F, -1.0F}; }
|
||||||
|
const auto VP = Rendering::computeViewport(config_.scaling_mode,
|
||||||
|
static_cast<std::uint32_t>(win_w),
|
||||||
|
static_cast<std::uint32_t>(win_h),
|
||||||
|
canvas_width_,
|
||||||
|
canvas_height_);
|
||||||
|
if (VP.w <= 0.0F || VP.h <= 0.0F) { return {-1.0F, -1.0F}; }
|
||||||
|
const float NX = (window_x - VP.x) / VP.w;
|
||||||
|
const float NY = (window_y - VP.y) / VP.h;
|
||||||
|
return {NX * static_cast<float>(canvas_width_), NY * static_cast<float>(canvas_height_)};
|
||||||
|
}
|
||||||
|
|
||||||
void Screen::setOverlayRenderer(std::function<void()> callback) {
|
void Screen::setOverlayRenderer(std::function<void()> callback) {
|
||||||
overlay_renderer_ = std::move(callback);
|
overlay_renderer_ = std::move(callback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,16 @@ class Screen {
|
|||||||
[[nodiscard]] auto getCanvasWidth() const -> int { return canvas_width_; }
|
[[nodiscard]] auto getCanvasWidth() const -> int { return canvas_width_; }
|
||||||
[[nodiscard]] auto getCanvasHeight() const -> int { return canvas_height_; }
|
[[nodiscard]] auto getCanvasHeight() const -> int { return canvas_height_; }
|
||||||
|
|
||||||
|
// Transforma una coordenada en espai finestra (tal com venen als
|
||||||
|
// esdeveniments SDL de ratolí) a espai canvas (0..canvas_w, 0..canvas_h).
|
||||||
|
// Reverteix el zoom de la finestra i el mode d'escalat actiu: al path
|
||||||
|
// fallback SDL_Renderer es delega a `SDL_RenderCoordinatesFromWindow`;
|
||||||
|
// al path GPU es recalcula a partir del viewport actual. Si el punt
|
||||||
|
// cau fora de la zona del canvas (letterbox/integer), els valors
|
||||||
|
// retornats queden fora del rang `[0, canvas_w/h)` — el caller ha de
|
||||||
|
// fer el bounds-check i ignorar el clic en eixe cas.
|
||||||
|
[[nodiscard]] auto windowToCanvas(float window_x, float window_y) const -> SDL_FPoint;
|
||||||
|
|
||||||
// Mètriques per a overlays de debug (RenderInfo).
|
// Mètriques per a overlays de debug (RenderInfo).
|
||||||
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
|
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
|
||||||
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
|
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#include "core/rendering/screenshot.hpp"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "external/stb_image_write.h"
|
||||||
|
|
||||||
|
namespace Screenshot {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Crea la carpeta `screenshots/` si no existeix i torna la ruta
|
||||||
|
// completa al fitxer. El paràmetre `filename` rep el nom nu (sense
|
||||||
|
// directori) per poder mostrar-lo a la notificació.
|
||||||
|
auto generateFilePath(std::string& filename) -> std::string {
|
||||||
|
std::filesystem::create_directories("screenshots");
|
||||||
|
|
||||||
|
const auto NOW = std::chrono::system_clock::now();
|
||||||
|
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW);
|
||||||
|
const std::tm* tm = std::localtime(&TIME);
|
||||||
|
|
||||||
|
char timestamp[20];
|
||||||
|
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm);
|
||||||
|
|
||||||
|
filename = std::string("scr_") + timestamp + ".png";
|
||||||
|
return "screenshots/" + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write
|
||||||
|
// espera RGBA en ordre byte (little-endian: R al byte baix, A al byte alt).
|
||||||
|
auto writePng(std::uint32_t* buffer, int width, int height, const std::string& filepath) -> bool {
|
||||||
|
const int TOTAL = width * height;
|
||||||
|
for (int i = 0; i < TOTAL; ++i) {
|
||||||
|
const std::uint32_t P = buffer[i];
|
||||||
|
const std::uint32_t A = (P >> 24) & 0xFF;
|
||||||
|
const std::uint32_t R = (P >> 16) & 0xFF;
|
||||||
|
const std::uint32_t G = (P >> 8) & 0xFF;
|
||||||
|
const std::uint32_t B = P & 0xFF;
|
||||||
|
buffer[i] = R | (G << 8) | (B << 16) | (A << 24);
|
||||||
|
}
|
||||||
|
return stbi_write_png(filepath.c_str(), width, height, 4, buffer, width * 4) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto save(const std::uint32_t* buffer, int width, int height) -> std::string {
|
||||||
|
// Còpia local per no tocar el canvas original durant la conversió.
|
||||||
|
std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height));
|
||||||
|
|
||||||
|
std::string filename;
|
||||||
|
const std::string FILEPATH = generateFilePath(filename);
|
||||||
|
|
||||||
|
return writePng(copy.data(), width, height, FILEPATH) ? filename : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Screenshot
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Volca un buffer ARGB8888 a un fitxer PNG dins de la carpeta `screenshots/`
|
||||||
|
// (relativa al CWD). El nom es genera amb un timestamp i la funció retorna
|
||||||
|
// el nom del fitxer creat, o una cadena buida si l'escriptura ha fallat.
|
||||||
|
namespace Screenshot {
|
||||||
|
|
||||||
|
auto save(const std::uint32_t* buffer, int width, int height) -> std::string;
|
||||||
|
|
||||||
|
} // namespace Screenshot
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
@@ -24,46 +25,6 @@ namespace {
|
|||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parseAnimations(const fkyaml::node& yaml, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<AnimatedSprite::AnimationData> {
|
|
||||||
std::vector<AnimatedSprite::AnimationData> animations;
|
|
||||||
if (!yaml.contains("animations") || !yaml["animations"].is_sequence()) {
|
|
||||||
return animations;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& anim_node : yaml["animations"]) {
|
|
||||||
AnimatedSprite::AnimationData animation;
|
|
||||||
|
|
||||||
if (anim_node.contains("name")) {
|
|
||||||
animation.name = anim_node["name"].get_value<std::string>();
|
|
||||||
}
|
|
||||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
|
||||||
animation.frames = convertYamlFramesToRects(
|
|
||||||
anim_node["frames"],
|
|
||||||
frame_width,
|
|
||||||
frame_height,
|
|
||||||
frames_per_row,
|
|
||||||
max_tiles);
|
|
||||||
}
|
|
||||||
if (anim_node.contains("speed")) {
|
|
||||||
const auto& speed_node = anim_node["speed"];
|
|
||||||
if (speed_node.is_sequence()) {
|
|
||||||
for (const auto& s : speed_node) {
|
|
||||||
animation.speeds.push_back(s.get_value<float>());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
auto spd = speed_node.get_value<float>();
|
|
||||||
if (spd > 0.0F) { animation.speeds.assign(animation.frames.size(), spd); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (anim_node.contains("loopFrom")) {
|
|
||||||
animation.loop_from = anim_node["loopFrom"].get_value<int>();
|
|
||||||
}
|
|
||||||
|
|
||||||
animations.push_back(animation);
|
|
||||||
}
|
|
||||||
return animations;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SheetParams {
|
struct SheetParams {
|
||||||
float frame_width{0.0F};
|
float frame_width{0.0F};
|
||||||
float frame_height{0.0F};
|
float frame_height{0.0F};
|
||||||
@@ -71,16 +32,22 @@ namespace {
|
|||||||
int max_tiles{1};
|
int max_tiles{1};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto parseGlobalConfig(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams {
|
// Llegeix el bloc `sprite: { path, size: [w, h] }` — mateix format que
|
||||||
|
// les plantilles d'entitat. La surface es resol via Resource::Cache amb
|
||||||
|
// el basename del fitxer.
|
||||||
|
auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams {
|
||||||
SheetParams params;
|
SheetParams params;
|
||||||
if (yaml.contains("tileSetFile")) {
|
if (!yaml.contains("sprite")) { return params; }
|
||||||
surface = Resource::Cache::get().getSurface(yaml["tileSetFile"].get_value<std::string>());
|
const auto& s = yaml["sprite"];
|
||||||
|
if (s.contains("path")) {
|
||||||
|
surface = Resource::Cache::get().getSurface(s["path"].get_value<std::string>());
|
||||||
}
|
}
|
||||||
if (yaml.contains("frameWidth")) {
|
if (s.contains("size")) {
|
||||||
params.frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
const auto& sz = s["size"];
|
||||||
|
if (sz.is_sequence() && sz.size() == 2) {
|
||||||
|
params.frame_width = static_cast<float>(sz[0].get_value<int>());
|
||||||
|
params.frame_height = static_cast<float>(sz[1].get_value<int>());
|
||||||
}
|
}
|
||||||
if (yaml.contains("frameHeight")) {
|
|
||||||
params.frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
|
||||||
}
|
}
|
||||||
if (surface && params.frame_width > 0.0F && params.frame_height > 0.0F) {
|
if (surface && params.frame_width > 0.0F && params.frame_height > 0.0F) {
|
||||||
params.frames_per_row = surface->getWidth() / static_cast<int>(params.frame_width);
|
params.frames_per_row = surface->getWidth() / static_cast<int>(params.frame_width);
|
||||||
@@ -90,21 +57,82 @@ namespace {
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `frame_ms` pot ser un escalar (temps uniforme) o una llista (per-frame).
|
||||||
|
// Convertim sempre a segons perquè `animate()` treballa amb delta_time
|
||||||
|
// en segons. Si falta o val 0, `speeds` queda buit i l'animació es
|
||||||
|
// considera estàtica (no avança).
|
||||||
|
void parseFrameTimes(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
||||||
|
if (!anim_node.contains("frame_ms")) { return; }
|
||||||
|
const auto& fm = anim_node["frame_ms"];
|
||||||
|
if (fm.is_sequence()) {
|
||||||
|
for (const auto& v : fm) {
|
||||||
|
animation.speeds.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const float SEC = static_cast<float>(fm.get_value<int>()) / 1000.0F;
|
||||||
|
if (SEC > 0.0F) { animation.speeds.assign(animation.frames.size(), SEC); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordre de prioritat per a la política de loop:
|
||||||
|
// 1) `loop_from: N` → rebota al frame N en passar del final (implica loop).
|
||||||
|
// 2) `loop: false` → animació d'un sol tret (es queda al darrer frame).
|
||||||
|
// 3) Res → loop complet des del frame 0 (default).
|
||||||
|
void parseLoopPolicy(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
||||||
|
if (anim_node.contains("loop_from")) {
|
||||||
|
animation.loop_from = anim_node["loop_from"].get_value<int>();
|
||||||
|
} else if (anim_node.contains("loop") && !anim_node["loop"].get_value<bool>()) {
|
||||||
|
animation.loop_from = -1;
|
||||||
|
} else {
|
||||||
|
animation.loop_from = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseAnimations(const fkyaml::node& yaml, const SheetParams& params) -> std::unordered_map<std::string, AnimatedSprite::AnimationData> {
|
||||||
|
std::unordered_map<std::string, AnimatedSprite::AnimationData> animations;
|
||||||
|
if (!yaml.contains("animations") || !yaml["animations"].is_mapping()) {
|
||||||
|
return animations;
|
||||||
|
}
|
||||||
|
const auto& anims_node = yaml["animations"];
|
||||||
|
for (auto it = anims_node.begin(); it != anims_node.end(); ++it) {
|
||||||
|
const auto NAME = it.key().get_value<std::string>();
|
||||||
|
const auto& anim_node = *it;
|
||||||
|
|
||||||
|
AnimatedSprite::AnimationData animation;
|
||||||
|
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||||
|
animation.frames = convertYamlFramesToRects(
|
||||||
|
anim_node["frames"],
|
||||||
|
params.frame_width,
|
||||||
|
params.frame_height,
|
||||||
|
params.frames_per_row,
|
||||||
|
params.max_tiles);
|
||||||
|
}
|
||||||
|
parseFrameTimes(anim_node, animation);
|
||||||
|
parseLoopPolicy(anim_node, animation);
|
||||||
|
|
||||||
|
animations.emplace(NAME, std::move(animation));
|
||||||
|
}
|
||||||
|
return animations;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||||
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
|
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
|
||||||
try {
|
try {
|
||||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||||
auto params = parseGlobalConfig(yaml, surface_);
|
auto params = parseSpriteBlock(yaml, surface_);
|
||||||
animations_ = parseAnimations(yaml, params.frame_width, params.frame_height, params.frames_per_row, params.max_tiles);
|
animations_ = parseAnimations(yaml, params);
|
||||||
buildNameIndex();
|
|
||||||
|
|
||||||
setWidth(params.frame_width);
|
setWidth(params.frame_width);
|
||||||
setHeight(params.frame_height);
|
setHeight(params.frame_height);
|
||||||
|
|
||||||
if (!animations_.empty() && !animations_[0].frames.empty()) {
|
// Clip inicial: qualsevol frame del primer element del mapa serveix
|
||||||
setClip(animations_[0].frames[0]);
|
// com a placeholder. El consumidor sempre crida `setCurrentAnimation`
|
||||||
|
// abans del primer `update()`, així que el valor real el posa ell.
|
||||||
|
if (!animations_.empty()) {
|
||||||
|
const auto& first = animations_.begin()->second;
|
||||||
|
if (!first.frames.empty()) { setClip(first.frames[0]); }
|
||||||
}
|
}
|
||||||
} catch (const fkyaml::exception& e) {
|
} catch (const fkyaml::exception& e) {
|
||||||
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||||
@@ -115,17 +143,12 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnimatedSprite::buildNameIndex() {
|
|
||||||
animation_index_.clear();
|
|
||||||
for (int i = 0; i < static_cast<int>(animations_.size()); ++i) {
|
|
||||||
animation_index_[animations_[i].name] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AnimatedSprite::animate(float delta_time) {
|
void AnimatedSprite::animate(float delta_time) {
|
||||||
if (animations_.empty()) { return; }
|
if (current_animation_.empty()) { return; }
|
||||||
auto& anim = animations_[current_animation_];
|
auto it = animations_.find(current_animation_);
|
||||||
if (anim.speeds.empty()) { return; }
|
if (it == animations_.end()) { return; }
|
||||||
|
auto& anim = it->second;
|
||||||
|
if (anim.speeds.empty() || anim.frames.empty()) { return; }
|
||||||
|
|
||||||
anim.accumulated_time += delta_time;
|
anim.accumulated_time += delta_time;
|
||||||
|
|
||||||
@@ -159,26 +182,25 @@ void AnimatedSprite::animate(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto AnimatedSprite::animationIsCompleted() -> bool {
|
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||||
return animations_[current_animation_].completed;
|
if (current_animation_.empty()) { return false; }
|
||||||
|
auto it = animations_.find(current_animation_);
|
||||||
|
if (it == animations_.end()) { return false; }
|
||||||
|
return it->second.completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
||||||
auto it = animation_index_.find(name);
|
if (current_animation_ == name) { return; }
|
||||||
if (it == animation_index_.end()) {
|
auto it = animations_.find(name);
|
||||||
|
if (it == animations_.end()) {
|
||||||
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
|
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCurrentAnimation(it->second);
|
current_animation_ = name;
|
||||||
}
|
auto& anim = it->second;
|
||||||
|
anim.current_frame = 0;
|
||||||
void AnimatedSprite::setCurrentAnimation(int index) {
|
anim.accumulated_time = 0.0F;
|
||||||
if (current_animation_ != index) {
|
anim.completed = false;
|
||||||
current_animation_ = index;
|
if (!anim.frames.empty()) { setClip(anim.frames[0]); }
|
||||||
animations_[current_animation_].current_frame = 0;
|
|
||||||
animations_[current_animation_].accumulated_time = 0.0F;
|
|
||||||
animations_[current_animation_].completed = false;
|
|
||||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnimatedSprite::update(float delta_time) {
|
void AnimatedSprite::update(float delta_time) {
|
||||||
@@ -187,15 +209,11 @@ void AnimatedSprite::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AnimatedSprite::resetAnimation() {
|
void AnimatedSprite::resetAnimation() {
|
||||||
animations_[current_animation_].current_frame = 0;
|
if (current_animation_.empty()) { return; }
|
||||||
animations_[current_animation_].accumulated_time = 0.0F;
|
auto it = animations_.find(current_animation_);
|
||||||
animations_[current_animation_].completed = false;
|
if (it == animations_.end()) { return; }
|
||||||
}
|
auto& anim = it->second;
|
||||||
|
anim.current_frame = 0;
|
||||||
void AnimatedSprite::setCurrentAnimationFrame(int num) {
|
anim.accumulated_time = 0.0F;
|
||||||
if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
anim.completed = false;
|
||||||
num = 0;
|
|
||||||
}
|
|
||||||
animations_[current_animation_].current_frame = num;
|
|
||||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
|
||||||
}
|
}
|
||||||
@@ -11,11 +11,19 @@
|
|||||||
|
|
||||||
class AnimatedSprite : public MovingSprite {
|
class AnimatedSprite : public MovingSprite {
|
||||||
public:
|
public:
|
||||||
|
// Dades d'una animació concreta dins d'un spritesheet. Les claus del
|
||||||
|
// mapa (`animations_`) són els noms; aquí només es guarden els fotogrames
|
||||||
|
// i l'estat de reproducció.
|
||||||
struct AnimationData {
|
struct AnimationData {
|
||||||
std::string name;
|
|
||||||
std::vector<SDL_FRect> frames;
|
std::vector<SDL_FRect> frames;
|
||||||
|
// Durada de cada frame en segons (convertit des de `frame_ms` al YAML).
|
||||||
|
// Si té un sol valor, val per a tots els frames; altrament ha de tenir
|
||||||
|
// la mateixa mida que `frames`.
|
||||||
std::vector<float> speeds;
|
std::vector<float> speeds;
|
||||||
int loop_from{-1};
|
// -1 = no loop (es queda al darrer frame i marca `completed`).
|
||||||
|
// 0 = loop complet des del primer frame.
|
||||||
|
// N>0 = rebota al frame N en passar del final.
|
||||||
|
int loop_from{0};
|
||||||
bool completed{false};
|
bool completed{false};
|
||||||
int current_frame{0};
|
int current_frame{0};
|
||||||
float accumulated_time{0.0F};
|
float accumulated_time{0.0F};
|
||||||
@@ -28,16 +36,12 @@ class AnimatedSprite : public MovingSprite {
|
|||||||
|
|
||||||
auto animationIsCompleted() -> bool;
|
auto animationIsCompleted() -> bool;
|
||||||
|
|
||||||
void setCurrentAnimation(const std::string& name = "default");
|
void setCurrentAnimation(const std::string& name);
|
||||||
void setCurrentAnimation(int index = 0);
|
|
||||||
void resetAnimation();
|
void resetAnimation();
|
||||||
void setCurrentAnimationFrame(int num);
|
|
||||||
void animate(float delta_time);
|
void animate(float delta_time);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildNameIndex();
|
std::unordered_map<std::string, AnimationData> animations_;
|
||||||
|
// Nom de l'animació activa. Buit fins que algú crida `setCurrentAnimation`.
|
||||||
std::vector<AnimationData> animations_;
|
std::string current_animation_;
|
||||||
std::unordered_map<std::string, int> animation_index_;
|
|
||||||
int current_animation_{0};
|
|
||||||
};
|
};
|
||||||
@@ -150,6 +150,7 @@ void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerni
|
|||||||
const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
||||||
const bool COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
const bool COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
||||||
const bool STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
const bool STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
||||||
|
const bool STROKED_4 = ((flags & STROKE4_FLAG) == STROKE4_FLAG);
|
||||||
|
|
||||||
if (CENTERED) { x -= (Text::length(text, kerning) / 2); }
|
if (CENTERED) { x -= (Text::length(text, kerning) / 2); }
|
||||||
|
|
||||||
@@ -157,6 +158,16 @@ void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerni
|
|||||||
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length);
|
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (STROKED_4) {
|
||||||
|
const int MAX_DIST = static_cast<int>(shadow_distance);
|
||||||
|
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
||||||
|
writeColored(x + dist, y, text, shadow_color, kerning, length);
|
||||||
|
writeColored(x - dist, y, text, shadow_color, kerning, length);
|
||||||
|
writeColored(x, y + dist, text, shadow_color, kerning, length);
|
||||||
|
writeColored(x, y - dist, text, shadow_color, kerning, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (STROKED) {
|
if (STROKED) {
|
||||||
const int MAX_DIST = static_cast<int>(shadow_distance);
|
const int MAX_DIST = static_cast<int>(shadow_distance);
|
||||||
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
||||||
|
|||||||
@@ -31,10 +31,17 @@ class Text {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Flags per a writeDX (composables amb |).
|
// Flags per a writeDX (composables amb |).
|
||||||
|
// `STROKE_FLAG` dibuixa un contorn complet de 8 veïns (N/S/E/O + 4
|
||||||
|
// diagonals) al voltant del glif; `STROKE4_FLAG` només els 4 cardinals
|
||||||
|
// (N/S/E/O), amb una aparença més prima per a glifs petits on la versió
|
||||||
|
// diagonal engreixa massa. Els dos flags són mútuament excloents: si
|
||||||
|
// s'activen a la vegada, ambdós bucles s'executen i l'stroke diagonal
|
||||||
|
// queda per damunt del cardinal (no té gaire sentit combinar-los).
|
||||||
static constexpr Uint8 COLOR_FLAG = 1;
|
static constexpr Uint8 COLOR_FLAG = 1;
|
||||||
static constexpr Uint8 SHADOW_FLAG = 2;
|
static constexpr Uint8 SHADOW_FLAG = 2;
|
||||||
static constexpr Uint8 CENTER_FLAG = 4;
|
static constexpr Uint8 CENTER_FLAG = 4;
|
||||||
static constexpr Uint8 STROKE_FLAG = 8;
|
static constexpr Uint8 STROKE_FLAG = 8;
|
||||||
|
static constexpr Uint8 STROKE4_FLAG = 16;
|
||||||
|
|
||||||
// Índex del píxel "tinta" al PNG de la font. Els glifs són monocroms
|
// Índex del píxel "tinta" al PNG de la font. Els glifs són monocroms
|
||||||
// sobre transparent i tots els píxels opacs usen aquest índex; la Surface
|
// sobre transparent i tots els píxels opacs usen aquest índex; la Surface
|
||||||
|
|||||||
@@ -3,29 +3,20 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "core/resources/resource_pack.hpp"
|
||||||
|
|
||||||
namespace Resource::Helper {
|
namespace Resource::Helper {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
std::string g_data_root;
|
std::string g_data_root;
|
||||||
|
Pack g_pack;
|
||||||
|
bool g_pack_loaded = false;
|
||||||
|
bool g_fallback_enabled = true;
|
||||||
|
|
||||||
} // namespace
|
auto readFromDisk(const std::string& filepath) -> std::vector<std::uint8_t> {
|
||||||
|
|
||||||
void setDataRoot(const std::string& absolute_path) {
|
|
||||||
g_data_root = absolute_path;
|
|
||||||
if (!g_data_root.empty() && g_data_root.back() != '/') {
|
|
||||||
g_data_root += '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto resolve(const std::string& filepath) -> std::string {
|
|
||||||
if (filepath.empty() || filepath.front() == '/') { return filepath; }
|
|
||||||
if (g_data_root.empty()) { return filepath; }
|
|
||||||
return g_data_root + filepath;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t> {
|
|
||||||
const auto FULL = resolve(filepath);
|
const auto FULL = resolve(filepath);
|
||||||
std::ifstream file(FULL, std::ios::binary | std::ios::ate);
|
std::ifstream file(FULL, std::ios::binary | std::ios::ate);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -42,4 +33,60 @@ namespace Resource::Helper {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
|
||||||
|
g_fallback_enabled = enable_fallback;
|
||||||
|
g_pack_loaded = g_pack.loadPack(pack_file);
|
||||||
|
|
||||||
|
if (g_pack_loaded) {
|
||||||
|
std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount()
|
||||||
|
<< " entries) from " << pack_file << '\n';
|
||||||
|
} else if (enable_fallback) {
|
||||||
|
std::cout << "Resource::Helper: no pack at " << pack_file
|
||||||
|
<< " — using filesystem fallback\n";
|
||||||
|
} else {
|
||||||
|
std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file
|
||||||
|
<< " and fallback disabled\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdownResourceSystem() {
|
||||||
|
g_pack.clear();
|
||||||
|
g_pack_loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto hasPack() -> bool {
|
||||||
|
return g_pack_loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDataRoot(const std::string& absolute_path) {
|
||||||
|
g_data_root = absolute_path;
|
||||||
|
if (!g_data_root.empty() && g_data_root.back() != '/') {
|
||||||
|
g_data_root += '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto resolve(const std::string& filepath) -> std::string {
|
||||||
|
if (filepath.empty() || filepath.front() == '/') { return filepath; }
|
||||||
|
if (g_data_root.empty()) { return filepath; }
|
||||||
|
return g_data_root + filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t> {
|
||||||
|
if (g_pack_loaded) {
|
||||||
|
// El pack està indexat relatiu a `data/` (la carpeta que
|
||||||
|
// s'empaqueta), però els callers passen "data/foo.yaml" amb
|
||||||
|
// prefix. Fem el strip ací perquè no hagin de tenir-ho en compte.
|
||||||
|
constexpr std::string_view PREFIX = "data/";
|
||||||
|
std::string key = filepath;
|
||||||
|
if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); }
|
||||||
|
if (g_pack.hasResource(key)) { return g_pack.getResource(key); }
|
||||||
|
}
|
||||||
|
if (g_fallback_enabled) { return readFromDisk(filepath); }
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Resource::Helper
|
} // namespace Resource::Helper
|
||||||
@@ -6,23 +6,33 @@
|
|||||||
|
|
||||||
namespace Resource::Helper {
|
namespace Resource::Helper {
|
||||||
|
|
||||||
// Estableix el prefix absolut des d'on es resolen totes les rutes
|
// Inicialitza el sistema de recursos. Carrega el pack des de `pack_file`
|
||||||
// relatives passades a `loadFile`/`resolve`. Pensat per a cridar-se una
|
// i decideix si `loadFile` pot caure a disc quan un recurs no és al pack.
|
||||||
// sola vegada al primer pas del Director amb `SDL_GetBasePath()`: així el
|
// En Release el pack és obligatori: passar `enable_fallback=false` fa
|
||||||
// binari troba `data/` sense dependre del CWD des d'on s'ha llançat (és
|
// que `initializeResourceSystem` retorne false si no s'ha pogut carregar.
|
||||||
// obligatori per a .app de macOS i útil a Linux per no haver d'estar
|
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool;
|
||||||
// dins de `build/`).
|
|
||||||
|
// Allibera el pack carregat a memòria.
|
||||||
|
void shutdownResourceSystem();
|
||||||
|
|
||||||
|
// True si el sistema es va inicialitzar amb un pack vàlid.
|
||||||
|
[[nodiscard]] auto hasPack() -> bool;
|
||||||
|
|
||||||
|
// Estableix el prefix des d'on es resolen les rutes quan el fallback
|
||||||
|
// llig de disc. En Debug apunta al source tree (`${CMAKE_SOURCE_DIR}/`)
|
||||||
|
// perquè editar YAML/GIF dins `data/` es vegi reflectit sense re-packejar.
|
||||||
|
// En Release el fallback està desactivat i aquest camí no es fa servir.
|
||||||
void setDataRoot(const std::string& absolute_path);
|
void setDataRoot(const std::string& absolute_path);
|
||||||
|
|
||||||
// Resol una ruta relativa contra el data root. Si la ruta ja és absoluta
|
// Resol una ruta relativa contra el data root. Si ja és absoluta
|
||||||
// (comença per '/') la retorna sense modificar — així els fitxers de
|
// (comença per '/') la retorna sense modificar. S'usa per a rutes que
|
||||||
// config (system_folder_ + "config.yaml") segueixen funcionant sense
|
// NO passen pel pack (p.ex. fitxers de config dins system_folder_).
|
||||||
// canvis al call site.
|
|
||||||
[[nodiscard]] auto resolve(const std::string& filepath) -> std::string;
|
[[nodiscard]] auto resolve(const std::string& filepath) -> std::string;
|
||||||
|
|
||||||
// Carga un fitxer del filesystem (a aquesta fase encara no hi ha pack).
|
// Carrega un recurs pel seu camí relatiu (p.ex. "fonts/hud.gif",
|
||||||
// Les rutes relatives es resolen via `resolve()`; les absolutes s'obrin
|
// "input/keys.yaml"). Prova primer el pack; si no hi és i el fallback
|
||||||
// directament.
|
// està actiu, cau a disc (data root + ruta). Retorna vector buit si
|
||||||
|
// no el troba.
|
||||||
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t>;
|
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t>;
|
||||||
|
|
||||||
} // namespace Resource::Helper
|
} // namespace Resource::Helper
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
#include "core/resources/resource_pack.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace Resource {
|
||||||
|
|
||||||
|
const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_2026_PACK_KEY";
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr const char* MAGIC = "AE26";
|
||||||
|
constexpr std::uint32_t VERSION = 1;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t {
|
||||||
|
// djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai
|
||||||
|
// reutilitzem eines de diagnòstic entre projectes.
|
||||||
|
std::uint32_t checksum = 0x12345678;
|
||||||
|
for (unsigned char b : data) {
|
||||||
|
// cppcheck-suppress useStlAlgorithm
|
||||||
|
checksum = ((checksum << 5) + checksum) + b;
|
||||||
|
}
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) {
|
||||||
|
if (key.empty()) { return; }
|
||||||
|
for (std::size_t i = 0; i < data.size(); ++i) {
|
||||||
|
data[i] ^= static_cast<std::uint8_t>(key[i % key.length()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pack::loadPack(const std::string& pack_file) -> bool {
|
||||||
|
std::ifstream file(pack_file, std::ios::binary);
|
||||||
|
if (!file) { return false; }
|
||||||
|
|
||||||
|
std::array<char, 4> header{};
|
||||||
|
file.read(header.data(), 4);
|
||||||
|
if (std::string(header.data(), 4) != MAGIC) {
|
||||||
|
std::cerr << "Resource::Pack: invalid pack file format (bad magic): " << pack_file << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t version = 0;
|
||||||
|
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||||
|
if (version != VERSION) {
|
||||||
|
std::cerr << "Resource::Pack: unsupported pack version: " << version << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t resource_count = 0;
|
||||||
|
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
|
||||||
|
|
||||||
|
resources_.clear();
|
||||||
|
resources_.reserve(resource_count);
|
||||||
|
|
||||||
|
for (std::uint32_t i = 0; i < resource_count; ++i) {
|
||||||
|
std::uint32_t filename_length = 0;
|
||||||
|
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
|
||||||
|
|
||||||
|
std::string filename(filename_length, '\0');
|
||||||
|
file.read(filename.data(), filename_length);
|
||||||
|
|
||||||
|
PackEntry entry;
|
||||||
|
entry.filename = filename;
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
|
||||||
|
|
||||||
|
resources_[filename] = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t data_size = 0;
|
||||||
|
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
|
||||||
|
|
||||||
|
data_.resize(data_size);
|
||||||
|
file.read(reinterpret_cast<char*>(data_.data()), static_cast<std::streamsize>(data_size));
|
||||||
|
|
||||||
|
xorInPlace(data_, DEFAULT_ENCRYPT_KEY);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pack::savePack(const std::string& pack_file) const -> bool {
|
||||||
|
std::ofstream file(pack_file, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "Resource::Pack: could not create pack file: " << pack_file << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(MAGIC, 4);
|
||||||
|
|
||||||
|
std::uint32_t version = VERSION;
|
||||||
|
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
|
||||||
|
|
||||||
|
auto resource_count = static_cast<std::uint32_t>(resources_.size());
|
||||||
|
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
|
||||||
|
|
||||||
|
for (const auto& [filename, entry] : resources_) {
|
||||||
|
auto filename_length = static_cast<std::uint32_t>(filename.length());
|
||||||
|
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
|
||||||
|
file.write(filename.c_str(), filename_length);
|
||||||
|
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
|
||||||
|
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
|
||||||
|
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> encrypted = data_;
|
||||||
|
xorInPlace(encrypted, DEFAULT_ENCRYPT_KEY);
|
||||||
|
|
||||||
|
std::uint64_t data_size = encrypted.size();
|
||||||
|
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
|
||||||
|
file.write(reinterpret_cast<const char*>(encrypted.data()), static_cast<std::streamsize>(data_size));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool {
|
||||||
|
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "Resource::Pack: could not open file: " << filepath << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamsize file_size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> file_data(file_size);
|
||||||
|
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
|
||||||
|
std::cerr << "Resource::Pack: could not read file: " << filepath << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PackEntry entry;
|
||||||
|
entry.filename = filename;
|
||||||
|
entry.offset = data_.size();
|
||||||
|
entry.size = file_data.size();
|
||||||
|
entry.checksum = calculateChecksum(file_data);
|
||||||
|
|
||||||
|
data_.insert(data_.end(), file_data.begin(), file_data.end());
|
||||||
|
resources_[filename] = entry;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pack::addDirectory(const std::string& directory) -> bool {
|
||||||
|
if (!std::filesystem::exists(directory)) {
|
||||||
|
std::cerr << "Resource::Pack: directory does not exist: " << directory << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cppcheck-suppress useStlAlgorithm
|
||||||
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
|
||||||
|
if (!entry.is_regular_file()) { continue; }
|
||||||
|
|
||||||
|
std::string filepath = entry.path().string();
|
||||||
|
std::string filename = std::filesystem::relative(entry.path(), directory).string();
|
||||||
|
std::ranges::replace(filename, '\\', '/');
|
||||||
|
|
||||||
|
if (!addFile(filename, filepath)) { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pack::getResource(const std::string& filename) const -> std::vector<std::uint8_t> {
|
||||||
|
auto it = resources_.find(filename);
|
||||||
|
if (it == resources_.end()) { return {}; }
|
||||||
|
|
||||||
|
const PackEntry& entry = it->second;
|
||||||
|
if (entry.offset + entry.size > data_.size()) {
|
||||||
|
std::cerr << "Resource::Pack: invalid resource data: " << filename << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> result(
|
||||||
|
data_.begin() + static_cast<std::ptrdiff_t>(entry.offset),
|
||||||
|
data_.begin() + static_cast<std::ptrdiff_t>(entry.offset + entry.size));
|
||||||
|
|
||||||
|
std::uint32_t checksum = calculateChecksum(result);
|
||||||
|
if (checksum != entry.checksum) {
|
||||||
|
std::cerr << "Resource::Pack: checksum mismatch for: " << filename << '\n';
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pack::hasResource(const std::string& filename) const -> bool {
|
||||||
|
return resources_.contains(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pack::clear() {
|
||||||
|
resources_.clear();
|
||||||
|
data_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pack::getResourceCount() const -> std::size_t {
|
||||||
|
return resources_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pack::getResourceList() const -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> result;
|
||||||
|
result.reserve(resources_.size());
|
||||||
|
for (const auto& [filename, entry] : resources_) {
|
||||||
|
result.push_back(filename);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Resource
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Resource {
|
||||||
|
|
||||||
|
// Entrada d'un recurs dins el pack.
|
||||||
|
struct PackEntry {
|
||||||
|
std::string filename;
|
||||||
|
std::uint64_t offset{0};
|
||||||
|
std::uint64_t size{0};
|
||||||
|
std::uint32_t checksum{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pack binari de recursos carregat a memòria. Format:
|
||||||
|
// Header: "AE26" (4 bytes) + version uint32 + resource_count uint32
|
||||||
|
// Index: per cada recurs -> filename_len uint32 + filename
|
||||||
|
// + offset uint64 + size uint64 + checksum uint32
|
||||||
|
// Payload: data_size uint64 + bytes xifrats amb XOR (DEFAULT_ENCRYPT_KEY)
|
||||||
|
// La classe té dos rols: (a) loader en temps d'execució — carregar un
|
||||||
|
// .pack del disc i obtindre els bytes d'un recurs per nom; (b) builder
|
||||||
|
// usat per l'eina `pack_resources` per a empaquetar `data/`.
|
||||||
|
class Pack {
|
||||||
|
public:
|
||||||
|
Pack() = default;
|
||||||
|
~Pack() = default;
|
||||||
|
|
||||||
|
Pack(const Pack&) = delete;
|
||||||
|
auto operator=(const Pack&) -> Pack& = delete;
|
||||||
|
Pack(Pack&&) = default;
|
||||||
|
auto operator=(Pack&&) -> Pack& = default;
|
||||||
|
|
||||||
|
// I/O del fitxer. `loadPack` retorna false silenciosament si el
|
||||||
|
// fitxer no existeix (el caller decideix si cal fallback).
|
||||||
|
auto loadPack(const std::string& pack_file) -> bool;
|
||||||
|
auto savePack(const std::string& pack_file) const -> bool;
|
||||||
|
|
||||||
|
// Builders usats per l'eina `pack_resources`.
|
||||||
|
auto addFile(const std::string& filename, const std::string& filepath) -> bool;
|
||||||
|
auto addDirectory(const std::string& directory) -> bool;
|
||||||
|
|
||||||
|
[[nodiscard]] auto getResource(const std::string& filename) const -> std::vector<std::uint8_t>;
|
||||||
|
[[nodiscard]] auto hasResource(const std::string& filename) const -> bool;
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
[[nodiscard]] auto getResourceCount() const -> std::size_t;
|
||||||
|
[[nodiscard]] auto getResourceList() const -> std::vector<std::string>;
|
||||||
|
|
||||||
|
static const std::string DEFAULT_ENCRYPT_KEY;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, PackEntry> resources_;
|
||||||
|
std::vector<std::uint8_t> data_;
|
||||||
|
|
||||||
|
static auto calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t;
|
||||||
|
static void xorInPlace(std::vector<std::uint8_t>& data, const std::string& key);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Resource
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "core/scene/scene.hpp"
|
#include "core/scene/scene.hpp"
|
||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/debug_config.hpp"
|
#include "game/debug_config.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/scenes/game_scene.hpp"
|
#include "game/scenes/game_scene.hpp"
|
||||||
@@ -32,7 +33,6 @@
|
|||||||
#include "game/scenes/logo_scene.hpp"
|
#include "game/scenes/logo_scene.hpp"
|
||||||
#include "game/scenes/title_scene.hpp"
|
#include "game/scenes/title_scene.hpp"
|
||||||
#include "game/ui/service_menu.hpp"
|
#include "game/ui/service_menu.hpp"
|
||||||
#include "game/ui/ui_palette.hpp"
|
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
Director::Director() {
|
Director::Director() {
|
||||||
@@ -40,18 +40,41 @@ Director::Director() {
|
|||||||
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
|
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rutes dels recursos relatives a l'executable, no al CWD. SDL_GetBasePath
|
// Sistema de recursos: el pack viu al costat del binari (build/), el
|
||||||
// retorna la carpeta del binari al desplegament portable i Contents/
|
// regenera CMake a cada build. En Debug habilitem el fallback a disc
|
||||||
// Resources/ dins d'un .app a macOS. Ha de fer-se abans de qualsevol
|
// apuntant al source tree perquè editar YAML/GIF dins `data/` es vegi
|
||||||
// loader que passi per Resource::Helper::resolve (Locale, KeyConfig,
|
// sense re-packejar; en Release el pack és obligatori.
|
||||||
// Resource::Cache, PaletteLoader, etc.).
|
std::string pack_file = "resources.pack";
|
||||||
if (const char* base = SDL_GetBasePath()) {
|
if (const char* base = SDL_GetBasePath()) {
|
||||||
|
pack_file = std::string(base) + "resources.pack";
|
||||||
|
#ifndef _DEBUG
|
||||||
Resource::Helper::setDataRoot(base);
|
Resource::Helper::setDataRoot(base);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#ifdef _DEBUG
|
||||||
|
Resource::Helper::setDataRoot(AEE_SOURCE_DIR);
|
||||||
|
constexpr bool ENABLE_FALLBACK = true;
|
||||||
|
#else
|
||||||
|
constexpr bool ENABLE_FALLBACK = false;
|
||||||
|
#endif
|
||||||
|
if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) {
|
||||||
|
std::fprintf(stderr, "Director: sistema de recursos no disponible; avortem\n");
|
||||||
|
std::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega
|
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega
|
||||||
// les opcions; si no existeix config.yaml el crea amb els defaults.
|
// les opcions; si no existeix config.yaml el crea amb els defaults.
|
||||||
createSystemFolder();
|
createSystemFolder();
|
||||||
|
|
||||||
|
// Gamepad: descobreix mandos presents silenciosament. Cal fer-ho ABANS
|
||||||
|
// de `Options::loadFromFile`, ja que aquesta crida internament a
|
||||||
|
// `applyControlsToGamepadManager` (rebind d'assignacions persistides +
|
||||||
|
// auto-assign de slots buits) i necessita que `entries_` ja estiga
|
||||||
|
// poblat per a poder casar mandos↔jugadors. A partir d'ara els events
|
||||||
|
// SDL_EVENT_GAMEPAD_ADDED/REMOVED arribaran via `processGamepadEvents()`
|
||||||
|
// i dispararan notificacions.
|
||||||
|
GamepadManager::get().init();
|
||||||
|
|
||||||
Options::setConfigFile(system_folder_ + "config.yaml");
|
Options::setConfigFile(system_folder_ + "config.yaml");
|
||||||
if (!Options::loadFromFile()) {
|
if (!Options::loadFromFile()) {
|
||||||
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
||||||
@@ -84,15 +107,19 @@ Director::Director() {
|
|||||||
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
|
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
|
||||||
KeyConfig::get().load("data/input/keys.yaml");
|
KeyConfig::get().load("data/input/keys.yaml");
|
||||||
|
|
||||||
|
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
|
||||||
|
// data/ui.yaml cau al valor compilat a Defaults::UI.
|
||||||
|
UiConfig::get().load("data/ui.yaml");
|
||||||
|
|
||||||
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
|
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
|
||||||
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
|
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
|
||||||
// noms de font viuen a Defaults::UI::Font (un únic lloc per canviar-los).
|
// noms de font viuen al fitxer data/ui.yaml via UiConfig.
|
||||||
Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal");
|
Resource::Cache::get().loadPalette("ui", "data/palettes/uzebox.pal");
|
||||||
const std::string HUD_FONT_GIF_KEY = std::string(Defaults::UI::Font::HUD_NAME) + ".gif";
|
const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif";
|
||||||
Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, Defaults::UI::Font::HUD_GIF);
|
Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, UiConfig::get().hudFont().gif);
|
||||||
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||||
const std::string PLACEHOLDER_FONT_GIF_KEY = std::string(Defaults::UI::Font::PLACEHOLDER_NAME) + ".gif";
|
const std::string PLACEHOLDER_FONT_GIF_KEY = UiConfig::get().placeholderFont().name + ".gif";
|
||||||
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, Defaults::UI::Font::PLACEHOLDER_GIF);
|
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, UiConfig::get().placeholderFont().gif);
|
||||||
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||||
|
|
||||||
// Música de la piràmide 1 (primer i únic track de moment). Carreguem ací
|
// Música de la piràmide 1 (primer i únic track de moment). Carreguem ací
|
||||||
@@ -104,50 +131,37 @@ Director::Director() {
|
|||||||
// JA_Music_t que continga, així que no queden streams orfes.
|
// JA_Music_t que continga, així que no queden streams orfes.
|
||||||
Audio::init(Options::buildAudioConfig());
|
Audio::init(Options::buildAudioConfig());
|
||||||
|
|
||||||
// Gamepad: descobreix mandos presents silenciosament. A partir d'ara
|
|
||||||
// els events SDL_EVENT_GAMEPAD_ADDED/REMOVED arribaran via
|
|
||||||
// `processGamepadEvents()` i dispararan notificacions.
|
|
||||||
GamepadManager::get().init();
|
|
||||||
|
|
||||||
// Font compartida pels overlays: construïm una sola instància de Text
|
// Font compartida pels overlays: construïm una sola instància de Text
|
||||||
// i la injectem als consumidors (Notifier, RenderInfo) per a que cap
|
// i la injectem als consumidors (Notifier, RenderInfo) per a que cap
|
||||||
// d'ells hagi de conèixer els noms dels recursos concrets.
|
// d'ells hagi de conèixer els noms dels recursos concrets.
|
||||||
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||||||
auto hud_text = std::make_shared<Text>(hud_font_surface, Defaults::UI::Font::HUD_FNT);
|
auto hud_text = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||||||
hud_text->setDefaultKerning(Defaults::UI::Font::HUD_KERNING);
|
hud_text->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||||||
const auto& ui_palette = Resource::Cache::get().getPalette("ui");
|
const auto& ui_palette = Resource::Cache::get().getPalette("ui");
|
||||||
|
|
||||||
RenderInfo::get().init(Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW), hud_text);
|
RenderInfo::get().init(Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()), hud_text);
|
||||||
Notifier::get().init(hud_text, ui_palette);
|
Notifier::get().init(hud_text, ui_palette);
|
||||||
|
|
||||||
// Font dels placeholders (LOGO / TITLE / INTRO). És una font distinta a
|
// Font dels placeholders (LOGO / TITLE / INTRO). És una font distinta a
|
||||||
// la de HUD perquè els placeholders desapareixeran amb les seues escenes
|
// la de HUD perquè els placeholders desapareixeran amb les seues escenes
|
||||||
// finals; mentrestant tenen estètica pròpia.
|
// finals; mentrestant tenen estètica pròpia.
|
||||||
auto placeholder_font_surface = Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY);
|
auto placeholder_font_surface = Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY);
|
||||||
auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, Defaults::UI::Font::PLACEHOLDER_FNT);
|
auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, UiConfig::get().placeholderFont().fnt);
|
||||||
placeholder_text->setDefaultKerning(Defaults::UI::Font::PLACEHOLDER_KERNING);
|
placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
|
||||||
|
|
||||||
// Font del menú de servei (F9). El títol va amb la variant `display`
|
// Font del menú de servei (F9). El títol va amb la variant `display`
|
||||||
// i els items amb `condensed-display` (més compacte perquè caben més
|
// i els items amb `condensed-display` (més compacte perquè caben més
|
||||||
// files). Els dos noms i kernings viuen a `Defaults::UI::Font`.
|
// files). Els dos noms i kernings viuen a data/ui.yaml via UiConfig.
|
||||||
auto load_menu_font = [&](const char* name, const char* gif, const char* fnt, int kerning) {
|
auto load_menu_font = [&](const UiConfig::Font& font) {
|
||||||
const std::string KEY = std::string(name) + ".gif";
|
const std::string KEY = font.name + ".gif";
|
||||||
Resource::Cache::get().loadSurface(KEY, gif);
|
Resource::Cache::get().loadSurface(KEY, font.gif);
|
||||||
Resource::Cache::get().getSurface(KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
Resource::Cache::get().getSurface(KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||||
auto text = std::make_shared<Text>(Resource::Cache::get().getSurface(KEY), fnt);
|
auto text = std::make_shared<Text>(Resource::Cache::get().getSurface(KEY), font.fnt);
|
||||||
text->setDefaultKerning(kerning);
|
text->setDefaultKerning(font.kerning);
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
auto menu_title_text = load_menu_font(
|
auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont());
|
||||||
Defaults::UI::Font::MENU_TITLE_NAME,
|
auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont());
|
||||||
Defaults::UI::Font::MENU_TITLE_GIF,
|
|
||||||
Defaults::UI::Font::MENU_TITLE_FNT,
|
|
||||||
Defaults::UI::Font::MENU_TITLE_KERNING);
|
|
||||||
auto menu_items_text = load_menu_font(
|
|
||||||
Defaults::UI::Font::MENU_ITEMS_NAME,
|
|
||||||
Defaults::UI::Font::MENU_ITEMS_GIF,
|
|
||||||
Defaults::UI::Font::MENU_ITEMS_FNT,
|
|
||||||
Defaults::UI::Font::MENU_ITEMS_KERNING);
|
|
||||||
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
|
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
|
||||||
|
|
||||||
// Overlay hook: Screen::present() cridarà aquesta lambda al principi,
|
// Overlay hook: Screen::present() cridarà aquesta lambda al principi,
|
||||||
@@ -167,12 +181,19 @@ Director::Director() {
|
|||||||
const auto DEBUG_YAML = system_folder_ + "debug.yaml";
|
const auto DEBUG_YAML = system_folder_ + "debug.yaml";
|
||||||
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
||||||
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
||||||
|
const auto STARTING_PLAYERS = DebugConfig::resolveStartingPlayers(DEBUG_YAML);
|
||||||
#else
|
#else
|
||||||
const auto START = DebugConfig::StartScene::LOGO;
|
const auto START = DebugConfig::StartScene::LOGO;
|
||||||
|
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
||||||
#endif
|
#endif
|
||||||
std::cout << "Director: start_scene=" << DebugConfig::toString(START) << '\n';
|
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
||||||
|
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
|
||||||
|
|
||||||
auto make_game = []() -> std::unique_ptr<Scene> { return std::make_unique<GameScene>(); };
|
auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr<Scene> {
|
||||||
|
auto cfg = GameScene::defaultConfig();
|
||||||
|
cfg.starting_players = STARTING_PLAYERS;
|
||||||
|
return std::make_unique<GameScene>(std::move(cfg));
|
||||||
|
};
|
||||||
auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr<Scene> {
|
auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr<Scene> {
|
||||||
return std::make_unique<IntroScene>(placeholder_text, make_game);
|
return std::make_unique<IntroScene>(placeholder_text, make_game);
|
||||||
};
|
};
|
||||||
@@ -207,6 +228,7 @@ Director::~Director() {
|
|||||||
// desfà les llibreries abans que corren els destructors estàtics.
|
// desfà les llibreries abans que corren els destructors estàtics.
|
||||||
current_scene_.reset();
|
current_scene_.reset();
|
||||||
Resource::Cache::get().clearAudio();
|
Resource::Cache::get().clearAudio();
|
||||||
|
Resource::Helper::shutdownResourceSystem();
|
||||||
GamepadManager::get().shutdown();
|
GamepadManager::get().shutdown();
|
||||||
Audio::destroy();
|
Audio::destroy();
|
||||||
Screen::get().shutdown();
|
Screen::get().shutdown();
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#include "core/ui/ui_config.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/easing.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Lector de camp amb fallback: si la clau existeix al node, escriu a
|
||||||
|
// `dst` el valor convertit; si no, deixa `dst` com estava (que al
|
||||||
|
// constructor ja hem inicialitzat amb Defaults::UI).
|
||||||
|
template <typename T>
|
||||||
|
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||||
|
if (node.contains(key)) { dst = node[key].get_value<T>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variant per a Uint8 — fkyaml no serialitza a Uint8 directament, cal
|
||||||
|
// passar per int i encaixar al rang 0..255 (valors fora de rang es
|
||||||
|
// trunquen i s'avisen).
|
||||||
|
void readColorIndex(const fkyaml::node& node, const char* key, Uint8& dst) {
|
||||||
|
if (!node.contains(key)) { return; }
|
||||||
|
const int VALUE = node[key].get_value<int>();
|
||||||
|
if (VALUE < 0 || VALUE > 255) {
|
||||||
|
std::cerr << "UiConfig: índex '" << key << "'=" << VALUE << " fora de rang 0..255 — ignorat\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dst = static_cast<Uint8>(VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registre de corbes disponibles. Afegir-ne una nova és només una
|
||||||
|
// entrada ací: la llegeix `UiConfig::curveByName` i queda exposada al
|
||||||
|
// YAML pel seu nom. No hi ha fallback mut: si el nom no hi és, l'api
|
||||||
|
// retorna linear i avisa per stderr perquè l'error no passi en silenci.
|
||||||
|
auto curveRegistry() -> const std::unordered_map<std::string_view, UiConfig::CurveFn>& {
|
||||||
|
static const std::unordered_map<std::string_view, UiConfig::CurveFn> REGISTRY = {
|
||||||
|
{"linear", &Easing::linear},
|
||||||
|
{"outQuad", &Easing::outQuad},
|
||||||
|
{"inQuad", &Easing::inQuad},
|
||||||
|
{"inOutQuad", &Easing::inOutQuad},
|
||||||
|
{"outCubic", &Easing::outCubic},
|
||||||
|
{"inCubic", &Easing::inCubic},
|
||||||
|
};
|
||||||
|
return REGISTRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void readCurve(const fkyaml::node& node, const char* key, UiConfig::CurveFn& dst) {
|
||||||
|
if (!node.contains(key)) { return; }
|
||||||
|
const auto NAME = node[key].get_value<std::string>();
|
||||||
|
dst = UiConfig::curveByName(NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
void readFont(const fkyaml::node& node, const char* key, UiConfig::Font& dst) {
|
||||||
|
if (!node.contains(key)) { return; }
|
||||||
|
const auto& f = node[key];
|
||||||
|
readField(f, "name", dst.name);
|
||||||
|
readField(f, "gif", dst.gif);
|
||||||
|
readField(f, "fnt", dst.fnt);
|
||||||
|
readField(f, "kerning", dst.kerning);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
UiConfig::UiConfig()
|
||||||
|
: render_info_text_(Defaults::UI::RENDER_INFO_TEXT),
|
||||||
|
render_info_shadow_(Defaults::UI::RENDER_INFO_SHADOW),
|
||||||
|
canvas_bg_(Defaults::UI::CANVAS_BG),
|
||||||
|
notification_text_(Defaults::UI::NOTIFICATION_TEXT),
|
||||||
|
notification_bg_(Defaults::UI::NOTIFICATION_BG),
|
||||||
|
exit_text_(Defaults::UI::EXIT_TEXT),
|
||||||
|
exit_bg_(Defaults::UI::EXIT_BG),
|
||||||
|
menu_bg_(Defaults::UI::MENU_BG),
|
||||||
|
menu_border_(Defaults::UI::MENU_BORDER),
|
||||||
|
menu_title_(Defaults::UI::MENU_TITLE),
|
||||||
|
menu_label_(Defaults::UI::MENU_LABEL),
|
||||||
|
menu_value_(Defaults::UI::MENU_VALUE),
|
||||||
|
menu_cursor_(Defaults::UI::MENU_CURSOR),
|
||||||
|
menu_disabled_(Defaults::UI::MENU_DISABLED),
|
||||||
|
hud_bg_(Defaults::UI::HUD_BG),
|
||||||
|
hud_label_(Defaults::UI::HUD_LABEL),
|
||||||
|
hud_value_(Defaults::UI::HUD_VALUE),
|
||||||
|
hud_flash_(Defaults::UI::HUD_FLASH),
|
||||||
|
notification_ms_(Defaults::UI::NOTIFICATION_TIME_MS),
|
||||||
|
exit_ms_(Defaults::UI::EXIT_TIME_MS),
|
||||||
|
hud_entry_ms_(Defaults::UI::HUD_ENTRY_MS),
|
||||||
|
hud_flash_ms_(Defaults::UI::HUD_FLASH_MS),
|
||||||
|
hud_shake_ms_(Defaults::UI::HUD_SHAKE_MS),
|
||||||
|
pause_enter_ms_(Defaults::UI::PAUSE_ENTER_MS),
|
||||||
|
pause_exit_ms_(Defaults::UI::PAUSE_EXIT_MS),
|
||||||
|
pause_enter_curve_(curveByName(Defaults::UI::PAUSE_ENTER_CURVE)),
|
||||||
|
pause_exit_curve_(curveByName(Defaults::UI::PAUSE_EXIT_CURVE)),
|
||||||
|
hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING},
|
||||||
|
placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING},
|
||||||
|
menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING},
|
||||||
|
menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING} {
|
||||||
|
}
|
||||||
|
|
||||||
|
auto UiConfig::get() -> UiConfig& {
|
||||||
|
static UiConfig instance_;
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto UiConfig::curveByName(const std::string& name) -> CurveFn {
|
||||||
|
const auto& reg = curveRegistry();
|
||||||
|
const auto IT = reg.find(name);
|
||||||
|
if (IT == reg.end()) {
|
||||||
|
std::cerr << "UiConfig: corba '" << name << "' desconeguda — s'usa linear\n";
|
||||||
|
return &Easing::linear;
|
||||||
|
}
|
||||||
|
return IT->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiConfig::load(const std::string& file_path) {
|
||||||
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
std::cerr << "UiConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\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("colors")) {
|
||||||
|
const auto& c = yaml["colors"];
|
||||||
|
readColorIndex(c, "render_info_text", render_info_text_);
|
||||||
|
readColorIndex(c, "render_info_shadow", render_info_shadow_);
|
||||||
|
readColorIndex(c, "canvas_bg", canvas_bg_);
|
||||||
|
readColorIndex(c, "notification_text", notification_text_);
|
||||||
|
readColorIndex(c, "notification_bg", notification_bg_);
|
||||||
|
readColorIndex(c, "exit_text", exit_text_);
|
||||||
|
readColorIndex(c, "exit_bg", exit_bg_);
|
||||||
|
readColorIndex(c, "menu_bg", menu_bg_);
|
||||||
|
readColorIndex(c, "menu_border", menu_border_);
|
||||||
|
readColorIndex(c, "menu_title", menu_title_);
|
||||||
|
readColorIndex(c, "menu_label", menu_label_);
|
||||||
|
readColorIndex(c, "menu_value", menu_value_);
|
||||||
|
readColorIndex(c, "menu_cursor", menu_cursor_);
|
||||||
|
readColorIndex(c, "menu_disabled", menu_disabled_);
|
||||||
|
readColorIndex(c, "hud_bg", hud_bg_);
|
||||||
|
readColorIndex(c, "hud_label", hud_label_);
|
||||||
|
readColorIndex(c, "hud_value", hud_value_);
|
||||||
|
readColorIndex(c, "hud_flash", hud_flash_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("timings")) {
|
||||||
|
const auto& t = yaml["timings"];
|
||||||
|
readField(t, "notification_ms", notification_ms_);
|
||||||
|
readField(t, "exit_ms", exit_ms_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("fonts")) {
|
||||||
|
const auto& f = yaml["fonts"];
|
||||||
|
readFont(f, "hud", hud_font_);
|
||||||
|
readFont(f, "placeholder", placeholder_font_);
|
||||||
|
readFont(f, "menu_title", menu_title_font_);
|
||||||
|
readFont(f, "menu_items", menu_items_font_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("hud")) {
|
||||||
|
const auto& h = yaml["hud"];
|
||||||
|
readField(h, "bg_enabled", hud_bg_enabled_);
|
||||||
|
readField(h, "entry_ms", hud_entry_ms_);
|
||||||
|
readField(h, "flash_ms", hud_flash_ms_);
|
||||||
|
readField(h, "shake_ms", hud_shake_ms_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("pause")) {
|
||||||
|
const auto& p = yaml["pause"];
|
||||||
|
readField(p, "enter_ms", pause_enter_ms_);
|
||||||
|
readField(p, "exit_ms", pause_exit_ms_);
|
||||||
|
readCurve(p, "enter_curve", pause_enter_curve_);
|
||||||
|
readCurve(p, "exit_curve", pause_exit_curve_);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "UiConfig: carregat des de " << file_path << '\n';
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "UiConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Configuració del tema UI (índexs de paleta, timings, fonts i flags)
|
||||||
|
// carregada des de data/ui.yaml. Cada camp s'inicialitza amb el valor
|
||||||
|
// de Defaults::UI com a fallback: si el fitxer falta o li manca una
|
||||||
|
// clau, el joc s'executa amb el tema compilat.
|
||||||
|
//
|
||||||
|
// Patró idèntic a Locale i KeyConfig: Meyers singleton + load(path).
|
||||||
|
// No hi ha hot-reload — els canvis al YAML requereixen reiniciar.
|
||||||
|
class UiConfig {
|
||||||
|
public:
|
||||||
|
struct Font {
|
||||||
|
std::string name;
|
||||||
|
std::string gif;
|
||||||
|
std::string fnt;
|
||||||
|
int kerning;
|
||||||
|
};
|
||||||
|
|
||||||
|
static auto get() -> UiConfig&;
|
||||||
|
|
||||||
|
UiConfig(const UiConfig&) = delete;
|
||||||
|
UiConfig(UiConfig&&) = delete;
|
||||||
|
auto operator=(const UiConfig&) -> UiConfig& = delete;
|
||||||
|
auto operator=(UiConfig&&) -> UiConfig& = delete;
|
||||||
|
|
||||||
|
void load(const std::string& file_path);
|
||||||
|
|
||||||
|
// Colors — índexs sobre la paleta "ui" (Resource::Cache).
|
||||||
|
[[nodiscard]] auto renderInfoText() const -> Uint8 { return render_info_text_; }
|
||||||
|
[[nodiscard]] auto renderInfoShadow() const -> Uint8 { return render_info_shadow_; }
|
||||||
|
[[nodiscard]] auto canvasBg() const -> Uint8 { return canvas_bg_; }
|
||||||
|
[[nodiscard]] auto notificationText() const -> Uint8 { return notification_text_; }
|
||||||
|
[[nodiscard]] auto notificationBg() const -> Uint8 { return notification_bg_; }
|
||||||
|
[[nodiscard]] auto exitText() const -> Uint8 { return exit_text_; }
|
||||||
|
[[nodiscard]] auto exitBg() const -> Uint8 { return exit_bg_; }
|
||||||
|
[[nodiscard]] auto menuBg() const -> Uint8 { return menu_bg_; }
|
||||||
|
[[nodiscard]] auto menuBorder() const -> Uint8 { return menu_border_; }
|
||||||
|
[[nodiscard]] auto menuTitle() const -> Uint8 { return menu_title_; }
|
||||||
|
[[nodiscard]] auto menuLabel() const -> Uint8 { return menu_label_; }
|
||||||
|
[[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; }
|
||||||
|
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
|
||||||
|
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
|
||||||
|
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
|
||||||
|
[[nodiscard]] auto hudLabel() const -> Uint8 { return hud_label_; }
|
||||||
|
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; }
|
||||||
|
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
|
||||||
|
|
||||||
|
// Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1].
|
||||||
|
// Permet triar corbes des de data/ui.yaml per nom (cadena) i guardar
|
||||||
|
// el punter a la corresponent funció de `utils/easing.hpp`.
|
||||||
|
using CurveFn = auto (*)(float) -> float;
|
||||||
|
|
||||||
|
// Timings (ms).
|
||||||
|
[[nodiscard]] auto notificationMs() const -> int { return notification_ms_; }
|
||||||
|
[[nodiscard]] auto exitMs() const -> int { return exit_ms_; }
|
||||||
|
[[nodiscard]] auto hudEntryMs() const -> int { return hud_entry_ms_; }
|
||||||
|
[[nodiscard]] auto hudFlashMs() const -> int { return hud_flash_ms_; }
|
||||||
|
[[nodiscard]] auto hudShakeMs() const -> int { return hud_shake_ms_; }
|
||||||
|
[[nodiscard]] auto pauseEnterMs() const -> int { return pause_enter_ms_; }
|
||||||
|
[[nodiscard]] auto pauseExitMs() const -> int { return pause_exit_ms_; }
|
||||||
|
|
||||||
|
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
|
||||||
|
// Easing::linear com a fallback.
|
||||||
|
[[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; }
|
||||||
|
[[nodiscard]] auto pauseExitCurve() const -> CurveFn { return pause_exit_curve_; }
|
||||||
|
|
||||||
|
// Resol un nom de corba a la seua funció. Pública per permetre a
|
||||||
|
// futurs camps d'UI triar corbes pel mateix mecanisme (ex.: HUD
|
||||||
|
// entry, notifications slide...). Si el nom no està registrat,
|
||||||
|
// retorna Easing::linear i deixa un warning al log.
|
||||||
|
[[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn;
|
||||||
|
|
||||||
|
// Fonts de UI.
|
||||||
|
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
|
||||||
|
[[nodiscard]] auto placeholderFont() const -> const Font& { return placeholder_font_; }
|
||||||
|
[[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; }
|
||||||
|
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
|
||||||
|
|
||||||
|
// Flags visuals.
|
||||||
|
[[nodiscard]] auto hudBgEnabled() const -> bool { return hud_bg_enabled_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
UiConfig();
|
||||||
|
~UiConfig() = default;
|
||||||
|
|
||||||
|
Uint8 render_info_text_;
|
||||||
|
Uint8 render_info_shadow_;
|
||||||
|
Uint8 canvas_bg_;
|
||||||
|
Uint8 notification_text_;
|
||||||
|
Uint8 notification_bg_;
|
||||||
|
Uint8 exit_text_;
|
||||||
|
Uint8 exit_bg_;
|
||||||
|
Uint8 menu_bg_;
|
||||||
|
Uint8 menu_border_;
|
||||||
|
Uint8 menu_title_;
|
||||||
|
Uint8 menu_label_;
|
||||||
|
Uint8 menu_value_;
|
||||||
|
Uint8 menu_cursor_;
|
||||||
|
Uint8 menu_disabled_;
|
||||||
|
Uint8 hud_bg_;
|
||||||
|
Uint8 hud_label_;
|
||||||
|
Uint8 hud_value_;
|
||||||
|
Uint8 hud_flash_;
|
||||||
|
|
||||||
|
int notification_ms_;
|
||||||
|
int exit_ms_;
|
||||||
|
int hud_entry_ms_;
|
||||||
|
int hud_flash_ms_;
|
||||||
|
int hud_shake_ms_;
|
||||||
|
int pause_enter_ms_;
|
||||||
|
int pause_exit_ms_;
|
||||||
|
CurveFn pause_enter_curve_;
|
||||||
|
CurveFn pause_exit_curve_;
|
||||||
|
|
||||||
|
Font hud_font_;
|
||||||
|
Font placeholder_font_;
|
||||||
|
Font menu_title_font_;
|
||||||
|
Font menu_items_font_;
|
||||||
|
|
||||||
|
bool hud_bg_enabled_{false};
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Unitat de compilació aïllada per a la implementació de stb_image_write.
|
||||||
|
// Viu dins de source/external/ perquè el `.clang-tidy` d'aquesta carpeta
|
||||||
|
// desactiva tots els checks (com fa per stb_vorbis.c) i el pre-commit hook
|
||||||
|
// ja filtra aquesta ruta de clang-format / clang-tidy. Així els fals
|
||||||
|
// positius de clang-analyzer-* dins de codi de tercers no afecten el
|
||||||
|
// nostre codi, que continua tenint tots els checks actius.
|
||||||
|
//
|
||||||
|
// La resta del codi inclou només el header (sense la macro d'implementació),
|
||||||
|
// que queda només amb declaracions — clang-analyzer no pot trobar cap bug
|
||||||
|
// dins d'una declaració, així que l'inclusió és innòcua.
|
||||||
|
|
||||||
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
|
#include "external/stb_image_write.h"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Unitat de compilació aïllada per a la implementació de stb_vorbis.
|
||||||
|
// Viu dins de source/external/ perquè el `.clang-tidy` d'aquesta carpeta
|
||||||
|
// desactiva tots els checks (com fa per stb_image_write_impl.cpp) i el
|
||||||
|
// pre-commit hook ja filtra aquesta ruta de clang-format / clang-tidy.
|
||||||
|
// Així els fals positius de clang-analyzer-* dins de codi C de tercers
|
||||||
|
// no afecten el nostre codi, que continua tenint tots els checks actius.
|
||||||
|
//
|
||||||
|
// jail_audio.cpp defineix STB_VORBIS_HEADER_ONLY abans d'incloure el .c,
|
||||||
|
// així només en veu les declaracions; les definicions les aporta aquest
|
||||||
|
// TU i l'enllaçador les resol.
|
||||||
|
|
||||||
|
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
||||||
|
#include "external/stb_vorbis.c"
|
||||||
@@ -33,6 +33,17 @@ namespace DebugConfig {
|
|||||||
return name == "LOGO" || name == "TITLE" || name == "INTRO" || name == "GAME";
|
return name == "LOGO" || name == "TITLE" || name == "INTRO" || name == "GAME";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto playersFromName(const std::string& name) -> StartingPlayers {
|
||||||
|
if (name == "P1") { return StartingPlayers::P1; }
|
||||||
|
if (name == "P2") { return StartingPlayers::P2; }
|
||||||
|
if (name == "BOTH") { return StartingPlayers::BOTH; }
|
||||||
|
return StartingPlayers::BOTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isKnownPlayers(const std::string& name) -> bool {
|
||||||
|
return name == "P1" || name == "P2" || name == "BOTH";
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto parseStartScene(const std::string& yaml_content) -> StartScene {
|
auto parseStartScene(const std::string& yaml_content) -> StartScene {
|
||||||
@@ -67,6 +78,31 @@ namespace DebugConfig {
|
|||||||
return parseStartScene(CONTENT);
|
return parseStartScene(CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto parseStartingPlayers(const std::string& yaml_content) -> StartingPlayers {
|
||||||
|
if (yaml_content.empty()) { return StartingPlayers::BOTH; }
|
||||||
|
try {
|
||||||
|
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||||
|
if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; }
|
||||||
|
const auto RAW = yaml["starting_players"].get_value<std::string>();
|
||||||
|
if (!isKnownPlayers(RAW)) {
|
||||||
|
std::cerr << "debug.yaml: starting_players \"" << RAW << "\" invàlid, arrancant amb BOTH\n";
|
||||||
|
return StartingPlayers::BOTH;
|
||||||
|
}
|
||||||
|
return playersFromName(RAW);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "debug.yaml: error parsejant starting_players: " << e.what() << " — arrancant amb BOTH\n";
|
||||||
|
return StartingPlayers::BOTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto resolveStartingPlayers(const std::string& file_path) -> StartingPlayers {
|
||||||
|
std::error_code error;
|
||||||
|
if (!std::filesystem::exists(file_path, error)) { return StartingPlayers::BOTH; }
|
||||||
|
const auto CONTENT = readFile(file_path);
|
||||||
|
if (CONTENT.empty()) { return StartingPlayers::BOTH; }
|
||||||
|
return parseStartingPlayers(CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
auto toString(StartScene scene) -> const char* {
|
auto toString(StartScene scene) -> const char* {
|
||||||
switch (scene) {
|
switch (scene) {
|
||||||
case StartScene::LOGO:
|
case StartScene::LOGO:
|
||||||
@@ -81,6 +117,18 @@ namespace DebugConfig {
|
|||||||
return "GAME";
|
return "GAME";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto toString(StartingPlayers players) -> const char* {
|
||||||
|
switch (players) {
|
||||||
|
case StartingPlayers::P1:
|
||||||
|
return "P1";
|
||||||
|
case StartingPlayers::P2:
|
||||||
|
return "P2";
|
||||||
|
case StartingPlayers::BOTH:
|
||||||
|
return "BOTH";
|
||||||
|
}
|
||||||
|
return "BOTH";
|
||||||
|
}
|
||||||
|
|
||||||
void createDefaultIfMissing(const std::string& file_path) {
|
void createDefaultIfMissing(const std::string& file_path) {
|
||||||
std::error_code error;
|
std::error_code error;
|
||||||
if (std::filesystem::exists(file_path, error)) { return; }
|
if (std::filesystem::exists(file_path, error)) { return; }
|
||||||
@@ -90,9 +138,11 @@ namespace DebugConfig {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
||||||
file << "# Valors vàlids: LOGO, TITLE, INTRO, GAME.\n";
|
file << "# start_scene: LOGO | TITLE | INTRO | GAME\n";
|
||||||
|
file << "# starting_players: P1 | P2 | BOTH\n";
|
||||||
file << "start_scene: GAME\n";
|
file << "start_scene: GAME\n";
|
||||||
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME\n";
|
file << "starting_players: BOTH\n";
|
||||||
|
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME starting_players=BOTH\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace DebugConfig
|
} // namespace DebugConfig
|
||||||
@@ -14,6 +14,13 @@ namespace DebugConfig {
|
|||||||
INTRO,
|
INTRO,
|
||||||
GAME };
|
GAME };
|
||||||
|
|
||||||
|
// Quins jugadors arrenquen a la GameScene. BOTH és el cas de joc real;
|
||||||
|
// P1 i P2 són només punts d'entrada de dev per provar cada personatge
|
||||||
|
// aïllat. Llegit de `debug.yaml` (clau `starting_players`).
|
||||||
|
enum class StartingPlayers : std::uint8_t { P1,
|
||||||
|
P2,
|
||||||
|
BOTH };
|
||||||
|
|
||||||
// Parseja un contingut YAML en cru. Si no conté `start_scene`, el valor és
|
// Parseja un contingut YAML en cru. Si no conté `start_scene`, el valor és
|
||||||
// desconegut o el YAML és invàlid, retorna GAME. Pura: no fa I/O.
|
// desconegut o el YAML és invàlid, retorna GAME. Pura: no fa I/O.
|
||||||
[[nodiscard]] auto parseStartScene(const std::string& yaml_content) -> StartScene;
|
[[nodiscard]] auto parseStartScene(const std::string& yaml_content) -> StartScene;
|
||||||
@@ -24,8 +31,15 @@ namespace DebugConfig {
|
|||||||
// GAME.
|
// GAME.
|
||||||
[[nodiscard]] auto resolveStartScene(const std::string& file_path) -> StartScene;
|
[[nodiscard]] auto resolveStartScene(const std::string& file_path) -> StartScene;
|
||||||
|
|
||||||
|
// Paral·lel a parseStartScene / resolveStartScene per al camp
|
||||||
|
// `starting_players`. El valor per defecte (tant si el fitxer no
|
||||||
|
// existeix com si li manca la clau) és BOTH, que és el cas de joc real.
|
||||||
|
[[nodiscard]] auto parseStartingPlayers(const std::string& yaml_content) -> StartingPlayers;
|
||||||
|
[[nodiscard]] auto resolveStartingPlayers(const std::string& file_path) -> StartingPlayers;
|
||||||
|
|
||||||
// Nom canònic de l'enum (útil per a logs): "LOGO" / "TITLE" / "INTRO" / "GAME".
|
// Nom canònic de l'enum (útil per a logs): "LOGO" / "TITLE" / "INTRO" / "GAME".
|
||||||
[[nodiscard]] auto toString(StartScene scene) -> const char*;
|
[[nodiscard]] auto toString(StartScene scene) -> const char*;
|
||||||
|
[[nodiscard]] auto toString(StartingPlayers players) -> const char*;
|
||||||
|
|
||||||
// Si el fitxer no existeix, el crea amb `start_scene: GAME` (el valor
|
// Si el fitxer no existeix, el crea amb `start_scene: GAME` (el valor
|
||||||
// per defecte pensat per al dia a dia de dev). Si ja existeix, no el
|
// per defecte pensat per al dia a dia de dev). Si ja existeix, no el
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
#include "game/editor/editor.hpp"
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "core/input/key_config.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
|
#include "core/rendering/notification_presets.hpp"
|
||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/surface.hpp"
|
||||||
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "core/system/event_buffer.hpp"
|
||||||
|
#include "game/map/tilemap.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Geometria del TilePicker en espai canvas. `cols × rows_visible` tiles
|
||||||
|
// encapsulats dins d'un rectangle amb `PICKER_MARGIN` per vora. Com que
|
||||||
|
// el tileset real (p.ex. 32×16 tiles de 16 px = 512×256) no cabria a
|
||||||
|
// 1× dins del canvas 336×240, linealitzem els índexs i els repartim
|
||||||
|
// en una graella que SÍ cap, amb scroll vertical per veure la resta.
|
||||||
|
struct PickerLayout {
|
||||||
|
int x{0};
|
||||||
|
int y{0};
|
||||||
|
int w{0};
|
||||||
|
int h{0};
|
||||||
|
int cols{0};
|
||||||
|
int rows_visible{0};
|
||||||
|
int tile_size{0};
|
||||||
|
int total_tiles{0};
|
||||||
|
int max_scroll{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto layoutPicker(const Tilemap& tm) -> PickerLayout {
|
||||||
|
PickerLayout lay{};
|
||||||
|
lay.tile_size = tm.getTileSize();
|
||||||
|
const int M = Defaults::Editor::PICKER_MARGIN;
|
||||||
|
const int INNER_W = GameCanvas::WIDTH - (2 * M);
|
||||||
|
const int INNER_H = GameCanvas::HEIGHT - (2 * M);
|
||||||
|
if (lay.tile_size <= 0) { return lay; }
|
||||||
|
lay.cols = INNER_W / lay.tile_size;
|
||||||
|
lay.rows_visible = INNER_H / lay.tile_size;
|
||||||
|
lay.x = M;
|
||||||
|
lay.y = M;
|
||||||
|
lay.w = lay.cols * lay.tile_size;
|
||||||
|
lay.h = lay.rows_visible * lay.tile_size;
|
||||||
|
const auto TILESET = Resource::Cache::get().getSurface(tm.getTilesetPath());
|
||||||
|
if (TILESET != nullptr) {
|
||||||
|
const int TCOLS = TILESET->getWidth() / lay.tile_size;
|
||||||
|
const int TROWS = TILESET->getHeight() / lay.tile_size;
|
||||||
|
lay.total_tiles = TCOLS * TROWS;
|
||||||
|
}
|
||||||
|
const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_tiles + lay.cols - 1) / lay.cols) : 0;
|
||||||
|
lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible);
|
||||||
|
return lay;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Editor::enter() {
|
||||||
|
if (active_) { return; }
|
||||||
|
active_ = true;
|
||||||
|
Notification::notify(Locale::get().text("input.editor_on"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// cppcheck-suppress constParameterReference
|
||||||
|
// Tilemap& mutable per simetria amb `toggle()` i per futures
|
||||||
|
// invalidacions de caches d'edició, encara que `save()` sigui const.
|
||||||
|
void Editor::exit(Tilemap& tilemap) {
|
||||||
|
if (!active_) { return; }
|
||||||
|
active_ = false;
|
||||||
|
picker_open_ = false;
|
||||||
|
// Resolem la ruta via Resource::Helper perquè en Debug el root de
|
||||||
|
// dades és AEE_SOURCE_DIR (ruta absoluta al repo), no el CWD. Sense
|
||||||
|
// això, el writer obriria `data/maps/pyramid.yaml` relatiu al CWD
|
||||||
|
// i fallaria sempre excepte si el binari es llança des del repo root.
|
||||||
|
const auto ABS_PATH = Resource::Helper::resolve(Defaults::Editor::SAVE_PATH);
|
||||||
|
const bool SAVED = tilemap.save(ABS_PATH);
|
||||||
|
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::toggle(Tilemap& tilemap) {
|
||||||
|
if (active_) {
|
||||||
|
exit(tilemap);
|
||||||
|
} else {
|
||||||
|
enter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::update(int /*delta_ms*/, Tilemap& tilemap) {
|
||||||
|
if (!active_) { return; }
|
||||||
|
|
||||||
|
// Posició del ratolí en coordenades del canvas (independent del
|
||||||
|
// zoom de finestra i del mode d'escalat).
|
||||||
|
float mouse_x = 0.0F;
|
||||||
|
float mouse_y = 0.0F;
|
||||||
|
SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||||
|
const auto CP = Screen::get().windowToCanvas(mouse_x, mouse_y);
|
||||||
|
const int CX = static_cast<int>(CP.x);
|
||||||
|
const int CY = static_cast<int>(CP.y);
|
||||||
|
|
||||||
|
// Consumim tecles pròpies del picker abans que cap altre handler
|
||||||
|
// (GlobalInputs) les vegi. ESC només es consumeix si el picker està
|
||||||
|
// obert — fora d'ell segueix arribant a GlobalInputs per la seva
|
||||||
|
// acció de confirmar-eixida.
|
||||||
|
const SDL_Scancode PICKER_SC = KeyConfig::get().scancode("editor_picker");
|
||||||
|
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
||||||
|
bool consumed = false;
|
||||||
|
if (it->type == SDL_EVENT_KEY_DOWN) {
|
||||||
|
if (PICKER_SC != SDL_SCANCODE_UNKNOWN && it->key.scancode == PICKER_SC) {
|
||||||
|
picker_open_ = !picker_open_;
|
||||||
|
if (picker_open_) { picker_scroll_ = 0; }
|
||||||
|
consumed = true;
|
||||||
|
} else if (picker_open_ && it->key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||||
|
picker_open_ = false;
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (consumed) {
|
||||||
|
it = EventBuffer::events.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (picker_open_) {
|
||||||
|
updatePicker(tilemap, CX, CY);
|
||||||
|
} else {
|
||||||
|
updateMap(tilemap, CX, CY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) {
|
||||||
|
// Cursor → cel·la del tilemap. Fora del mapa deixem -1/-1 perquè
|
||||||
|
// `render()` sàpiga no dibuixar preview i `setTileAt` ignori el clic.
|
||||||
|
const int TS = tilemap.getTileSize();
|
||||||
|
const int MX = canvas_x - tilemap.getOriginX();
|
||||||
|
const int MY = canvas_y - tilemap.getOriginY();
|
||||||
|
cursor_col_ = -1;
|
||||||
|
cursor_row_ = -1;
|
||||||
|
if (TS > 0 && MX >= 0 && MY >= 0) {
|
||||||
|
const int C = MX / TS;
|
||||||
|
const int R = MY / TS;
|
||||||
|
if (C < tilemap.getCols() && R < tilemap.getRows()) {
|
||||||
|
cursor_col_ = C;
|
||||||
|
cursor_row_ = R;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& ev : EventBuffer::events) {
|
||||||
|
if (ev.type != SDL_EVENT_MOUSE_BUTTON_DOWN) { continue; }
|
||||||
|
if (ev.button.button == SDL_BUTTON_LEFT) {
|
||||||
|
if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) {
|
||||||
|
tilemap.setTileAt(cursor_col_, cursor_row_, brush_);
|
||||||
|
}
|
||||||
|
} else if (ev.button.button == SDL_BUTTON_RIGHT) {
|
||||||
|
brush_ = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) {
|
||||||
|
const auto LAY = layoutPicker(tilemap);
|
||||||
|
picker_hover_ = -1;
|
||||||
|
if (canvas_x >= LAY.x && canvas_x < LAY.x + LAY.w && canvas_y >= LAY.y && canvas_y < LAY.y + LAY.h && LAY.tile_size > 0) {
|
||||||
|
const int VC = (canvas_x - LAY.x) / LAY.tile_size;
|
||||||
|
const int VR = (canvas_y - LAY.y) / LAY.tile_size;
|
||||||
|
const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC;
|
||||||
|
if (IDX < LAY.total_tiles) { picker_hover_ = IDX; }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& ev : EventBuffer::events) {
|
||||||
|
if (ev.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||||
|
picker_scroll_ -= static_cast<int>(ev.wheel.y);
|
||||||
|
picker_scroll_ = std::clamp(picker_scroll_, 0, LAY.max_scroll);
|
||||||
|
} else if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN && ev.button.button == SDL_BUTTON_LEFT) {
|
||||||
|
if (picker_hover_ >= 0) {
|
||||||
|
brush_ = picker_hover_;
|
||||||
|
picker_open_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::render(const Tilemap& tilemap) const {
|
||||||
|
if (!active_) { return; }
|
||||||
|
auto& screen = Screen::get();
|
||||||
|
constexpr int W = GameCanvas::WIDTH;
|
||||||
|
constexpr int H = GameCanvas::HEIGHT;
|
||||||
|
|
||||||
|
if (picker_open_) {
|
||||||
|
renderPicker(tilemap);
|
||||||
|
} else if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) {
|
||||||
|
// Preview del brush sota el cursor (només quan el picker és tancat).
|
||||||
|
const int PX = tilemap.getOriginX() + (cursor_col_ * tilemap.getTileSize());
|
||||||
|
const int PY = tilemap.getOriginY() + (cursor_row_ * tilemap.getTileSize());
|
||||||
|
tilemap.renderTilePreview(PX, PY, brush_);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto BORDER = Defaults::Editor::BORDER_ARGB;
|
||||||
|
screen.fillRect(0, 0, W, 1, BORDER);
|
||||||
|
screen.fillRect(0, H - 1, W, 1, BORDER);
|
||||||
|
screen.fillRect(0, 0, 1, H, BORDER);
|
||||||
|
screen.fillRect(W - 1, 0, 1, H, BORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::renderPicker(const Tilemap& tilemap) const {
|
||||||
|
const auto LAY = layoutPicker(tilemap);
|
||||||
|
auto& screen = Screen::get();
|
||||||
|
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, Defaults::Editor::PICKER_BG_ARGB);
|
||||||
|
|
||||||
|
for (int vr = 0; vr < LAY.rows_visible; ++vr) {
|
||||||
|
const int LOGICAL_ROW = picker_scroll_ + vr;
|
||||||
|
for (int vc = 0; vc < LAY.cols; ++vc) {
|
||||||
|
const int IDX = (LOGICAL_ROW * LAY.cols) + vc;
|
||||||
|
if (IDX >= LAY.total_tiles) { break; }
|
||||||
|
const int PX = LAY.x + (vc * LAY.tile_size);
|
||||||
|
const int PY = LAY.y + (vr * LAY.tile_size);
|
||||||
|
tilemap.renderTilePreview(PX, PY, IDX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (picker_hover_ >= 0 && LAY.cols > 0) {
|
||||||
|
const int VR = (picker_hover_ / LAY.cols) - picker_scroll_;
|
||||||
|
const int VC = picker_hover_ % LAY.cols;
|
||||||
|
if (VR >= 0 && VR < LAY.rows_visible) {
|
||||||
|
const int PX = LAY.x + (VC * LAY.tile_size);
|
||||||
|
const int PY = LAY.y + (VR * LAY.tile_size);
|
||||||
|
const int TS = LAY.tile_size;
|
||||||
|
constexpr auto C = Defaults::Editor::PICKER_HOVER_ARGB;
|
||||||
|
screen.fillRect(PX, PY, TS, 1, C);
|
||||||
|
screen.fillRect(PX, PY + TS - 1, TS, 1, C);
|
||||||
|
screen.fillRect(PX, PY, 1, TS, C);
|
||||||
|
screen.fillRect(PX + TS - 1, PY, 1, TS, C);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _DEBUG
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
|
||||||
|
class Tilemap;
|
||||||
|
|
||||||
|
// Mode editor modal del GameScene. Només disponible en Debug — en Release
|
||||||
|
// aquesta classe no existeix (el .cpp es compila a un object buit i cap
|
||||||
|
// codi del joc final en fa referència).
|
||||||
|
//
|
||||||
|
// L'editor NO gestiona la tecla de toggle pròpia (`editor_toggle`): això
|
||||||
|
// ho fa GameScene per poder decidir la coexistència amb la pausa F11
|
||||||
|
// (mútuament excloents). L'editor s'encarrega de:
|
||||||
|
// - Estat propi (actiu/no actiu, brush, picker obert).
|
||||||
|
// - Input del ratolí i tecles internes (`T`, `ESC`).
|
||||||
|
// - Render del border blau + brush preview + picker.
|
||||||
|
// - Persistència del tilemap al YAML del repo en sortir.
|
||||||
|
//
|
||||||
|
// GameScene, quan `active()` és true, salta el tick de jugadors/entitats
|
||||||
|
// i no els pinta; el tilemap sí es pinta perquè és el que s'està editant.
|
||||||
|
class Editor {
|
||||||
|
public:
|
||||||
|
// Entra al mode editor: notifica i marca `active=true`. La congelació
|
||||||
|
// d'entitats / jugador la gestiona GameScene consultant `active()`.
|
||||||
|
void enter();
|
||||||
|
|
||||||
|
// Surt del mode editor: persisteix el tilemap al YAML (writer surgical)
|
||||||
|
// i notifica. Si la persistència falla, emet la notificació d'error.
|
||||||
|
void exit(Tilemap& tilemap);
|
||||||
|
|
||||||
|
// Helper: enter() si no actiu, exit() si actiu. Útil des de GameScene
|
||||||
|
// quan detecta la tecla de toggle.
|
||||||
|
void toggle(Tilemap& tilemap);
|
||||||
|
|
||||||
|
[[nodiscard]] auto active() const -> bool { return active_; }
|
||||||
|
|
||||||
|
// Processa input propi (brush, picker). No-op si no actiu.
|
||||||
|
void update(int delta_ms, Tilemap& tilemap);
|
||||||
|
|
||||||
|
// Pinta el border blau i, més endavant, brush preview + picker.
|
||||||
|
// No-op si no actiu.
|
||||||
|
void render(const Tilemap& tilemap) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Les branques d'update / render segons el mode (picker obert o
|
||||||
|
// pintura al mapa) queden separades per claredat; no formen part
|
||||||
|
// de l'API pública perquè només té sentit cridar-les des d'`update`.
|
||||||
|
void updateMap(Tilemap& tilemap, int canvas_x, int canvas_y);
|
||||||
|
void updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y);
|
||||||
|
void renderPicker(const Tilemap& tilemap) const;
|
||||||
|
|
||||||
|
bool active_{false};
|
||||||
|
|
||||||
|
// Estat de l'edició (brush + cursor + picker). Es poblen al pas
|
||||||
|
// següent; els declarem ara perquè `update()` pugui mutar-los i les
|
||||||
|
// signatures queden estables entre hitos.
|
||||||
|
int brush_{-1}; // Índex de tile al tileset; -1 = sense brush.
|
||||||
|
int cursor_col_{-1}; // Cel·la del mapa sota el cursor (-1 = fora).
|
||||||
|
int cursor_row_{-1};
|
||||||
|
bool picker_open_{false};
|
||||||
|
int picker_scroll_{0}; // Fila superior visible del picker (linealitzat).
|
||||||
|
int picker_hover_{-1}; // Índex del tile sota el cursor dins del picker.
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _DEBUG
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -63,16 +65,35 @@ void Door::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Door::onPlayerContact(Player* /*player*/) {
|
void Door::onPlayerContact(Player* player) {
|
||||||
if (state_ != State::OPEN || level_complete_emitted_) { return; }
|
// Només la porta OPEN despawna jugadors. CLOSED/OPENING ignoren el
|
||||||
if (bus_ != nullptr) { bus_->publish("level_complete"); }
|
// contacte (i a més, en CLOSED, el `Map::collides` ja bloqueja el
|
||||||
level_complete_emitted_ = true;
|
// moviment, així que el contacte no es dóna en condicions normals).
|
||||||
|
if (state_ != State::OPEN || player == nullptr || player->isExited()) { return; }
|
||||||
|
// Marquem el jugador com a EXITED i publiquem l'event d'agregació.
|
||||||
|
// El check `isExited` de la guarda anterior garanteix que només
|
||||||
|
// emetem l'event en la primera transició — `EntityManager` compta
|
||||||
|
// emissions per decidir quan publicar `level_complete`.
|
||||||
|
player->exit();
|
||||||
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_exited",
|
||||||
|
.payload = std::monostate{},
|
||||||
|
.player_id = player->getId(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Door::open() {
|
void Door::open() {
|
||||||
if (state_ != State::CLOSED) { return; }
|
if (state_ != State::CLOSED) { return; }
|
||||||
state_ = State::OPENING;
|
state_ = State::OPENING;
|
||||||
setAnimation(anim_opening_);
|
setAnimation(anim_opening_);
|
||||||
|
// Marquem el passadís com a transitable: a partir d'aquest tic
|
||||||
|
// `Map::collides` deixa de bloquejar el rect del passadís. La
|
||||||
|
// transició d'animació OPENING → OPEN encara dura el seu temps,
|
||||||
|
// però la geometria ja és permeable — coherent amb el feel de
|
||||||
|
// "la porta s'obri i el jugador pot avançar al moment".
|
||||||
|
Map::door_open = true;
|
||||||
// Edge case: si `opening` té ≤1 frame i és loop=true, mai es
|
// Edge case: si `opening` té ≤1 frame i és loop=true, mai es
|
||||||
// completarà. Per força, forcem loop=false del frame únic aquí. Els
|
// completarà. Per força, forcem loop=false del frame únic aquí. Els
|
||||||
// placeholders defineixen `loop: false` explícitament al YAML, així
|
// placeholders defineixen `loop: false` explícitament al YAML, així
|
||||||
|
|||||||
@@ -7,11 +7,14 @@
|
|||||||
#include "game/entities/interactable.hpp"
|
#include "game/entities/interactable.hpp"
|
||||||
|
|
||||||
// Porta de sortida del nivell. Tres estats:
|
// Porta de sortida del nivell. Tres estats:
|
||||||
// CLOSED → inicial; bloqueja el pas.
|
// CLOSED → inicial; el passadís de Map bloqueja el pas via `Map::collides`.
|
||||||
// OPENING → animació d'obertura en curs. Quan el component acaba →
|
// OPENING → animació d'obertura en curs. Quan el component acaba →
|
||||||
// transita a OPEN.
|
// transita a OPEN. El passadís ja és transitable (s'allibera
|
||||||
// OPEN → accessible. Al contacte amb el jugador publica
|
// a `open()` amb `Map::door_open = true`).
|
||||||
// `level_complete` al bus (si n'hi ha).
|
// OPEN → accessible. Al contacte amb un jugador, crida `Player::exit()`
|
||||||
|
// — el jugador queda despawnat i emet `player_exited` al bus.
|
||||||
|
// L'agregació "tots els jugadors han eixit → `level_complete`"
|
||||||
|
// viu a EntityManager.
|
||||||
//
|
//
|
||||||
// El que decideix quan s'obri és extern (`EntityManager` comptant sarcs
|
// El que decideix quan s'obri és extern (`EntityManager` comptant sarcs
|
||||||
// oberts) via `open()`. L'animació `opening` es fa servir com a
|
// oberts) via `open()`. L'animació `opening` es fa servir com a
|
||||||
@@ -38,7 +41,6 @@ class Door : public Interactable {
|
|||||||
void setAnimation(const AnimationComponent& anim);
|
void setAnimation(const AnimationComponent& anim);
|
||||||
|
|
||||||
State state_{State::CLOSED};
|
State state_{State::CLOSED};
|
||||||
bool level_complete_emitted_{false};
|
|
||||||
|
|
||||||
// Les 3 animacions precalculades al ctor des del YAML. L'activa
|
// Les 3 animacions precalculades al ctor des del YAML. L'activa
|
||||||
// s'assigna a `anim_` (heretat d'Entity) segons l'estat.
|
// s'assigna a `anim_` (heretat d'Entity) segons l'estat.
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#include "game/entities/concrete/dust.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Llig una animació opcional del bloc `animations` del YAML, amb
|
||||||
|
// valors per defecte segurs (1 frame, fps=1, sense loop). Si la
|
||||||
|
// clau no existeix, retornem una animació "ja completada" — així,
|
||||||
|
// un Dust spawnejat amb una variant inexistent al YAML mor al
|
||||||
|
// pròxim frame sense petar.
|
||||||
|
auto buildAnim(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||||
|
std::vector<int> frames = {0};
|
||||||
|
float fps = 1.0F;
|
||||||
|
bool loop = false;
|
||||||
|
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
||||||
|
const auto& anim = yaml["animations"][key];
|
||||||
|
if (anim.contains("frames")) {
|
||||||
|
frames.clear();
|
||||||
|
for (const auto& f : anim["frames"]) {
|
||||||
|
// cppcheck-suppress useStlAlgorithm
|
||||||
|
frames.push_back(f.get_value<int>());
|
||||||
|
}
|
||||||
|
if (frames.empty()) { frames.push_back(0); }
|
||||||
|
}
|
||||||
|
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
||||||
|
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
||||||
|
}
|
||||||
|
return {frames, fps, loop};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Dust::Dust(const fkyaml::node& yaml)
|
||||||
|
: anim_jump_(buildAnim(yaml, "jump")),
|
||||||
|
anim_land_(buildAnim(yaml, "land")) {
|
||||||
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("dust");
|
||||||
|
collides_ = false;
|
||||||
|
interactable_ = false;
|
||||||
|
// Per defecte JUMP; el spawner pot canviar-la abans del primer tick.
|
||||||
|
anim_ = anim_jump_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Dust::setVariant(Variant v) {
|
||||||
|
anim_ = (v == Variant::JUMP) ? anim_jump_ : anim_land_;
|
||||||
|
if (anim_.has_value()) { anim_->reset(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void Dust::update(float delta_time) {
|
||||||
|
Entity::update(delta_time);
|
||||||
|
if (anim_.has_value() && anim_->completed()) { markDead(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
REGISTER_ENTITY("dust", Dust)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/animation_component.hpp"
|
||||||
|
#include "game/entities/entity.hpp"
|
||||||
|
|
||||||
|
// Polvet transitori: efecte purament visual que viu una animació i
|
||||||
|
// s'autoelimina (`markDead`) quan acaba. Spawneja el `GameScene` com a
|
||||||
|
// reacció a events `player_jumped` i `player_landed` del bus. Les dues
|
||||||
|
// variants (despegue / aterratge) viuen al mateix YAML amb dues
|
||||||
|
// animacions distintes (`jump` i `land`); l'instància tria una via
|
||||||
|
// `setVariant` just després de construir-se.
|
||||||
|
class Dust : public Entity {
|
||||||
|
public:
|
||||||
|
enum class Variant : std::uint8_t {
|
||||||
|
JUMP,
|
||||||
|
LAND,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit Dust(const fkyaml::node& yaml);
|
||||||
|
|
||||||
|
// Tria quina animació es reproduirà. Cal cridar-lo abans del primer
|
||||||
|
// `update`; per defecte la variant és JUMP.
|
||||||
|
void setVariant(Variant v);
|
||||||
|
|
||||||
|
// Tickeja l'animació activa. Quan arriba a `completed()`, marquem
|
||||||
|
// l'entitat morta perquè l'`EntityManager` la purgue al final del
|
||||||
|
// frame.
|
||||||
|
void update(float delta_time) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
AnimationComponent anim_jump_;
|
||||||
|
AnimationComponent anim_land_;
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
|
||||||
FloorTrap::FloorTrap(const fkyaml::node& yaml) {
|
FloorTrap::FloorTrap(const fkyaml::node& yaml) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("floor_trap");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("floor_trap");
|
||||||
@@ -21,9 +22,15 @@ FloorTrap::FloorTrap(const fkyaml::node& yaml) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloorTrap::onPlayerContact(Player* /*player*/) {
|
void FloorTrap::onPlayerContact(Player* player) {
|
||||||
if (!isActive()) { return; }
|
if (!isActive()) { return; }
|
||||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_hit",
|
||||||
|
.payload = getDamage(),
|
||||||
|
.player_id = (player != nullptr) ? player->getId() : -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
REGISTER_ENTITY("floor_trap", FloorTrap)
|
REGISTER_ENTITY("floor_trap", FloorTrap)
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -45,6 +47,29 @@ namespace {
|
|||||||
return {frames, fps, loop};
|
return {frames, fps, loop};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Busca si el segment [prev, prev+delta] travessa una lane del patró
|
||||||
|
// `base + k·pitch` (k ∈ [0..max_idx]). Retorna el delta que cal aplicar
|
||||||
|
// per arribar exactament a la primera línia travessada. Iterem en el
|
||||||
|
// sentit de la marxa per encertar la MÉS propera quan el frame creua
|
||||||
|
// més d'una lane (cas marginal a velocitats típiques, però pot passar
|
||||||
|
// amb dt grans). Comparació estricta a un costat: no dispara quan la
|
||||||
|
// caixa ja es troba alineada a la lane (hi està després del snap).
|
||||||
|
auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> {
|
||||||
|
const float NEXT = prev + delta;
|
||||||
|
if (delta > 0.0F) {
|
||||||
|
for (int k = 0; k <= max_idx; ++k) {
|
||||||
|
const float TARGET = base + (static_cast<float>(k) * pitch);
|
||||||
|
if (prev < TARGET && NEXT >= TARGET) { return TARGET - prev; }
|
||||||
|
}
|
||||||
|
} else if (delta < 0.0F) {
|
||||||
|
for (int k = max_idx; k >= 0; --k) {
|
||||||
|
const float TARGET = base + (static_cast<float>(k) * pitch);
|
||||||
|
if (prev > TARGET && NEXT <= TARGET) { return TARGET - prev; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Mummy::Mummy(const fkyaml::node& yaml)
|
Mummy::Mummy(const fkyaml::node& yaml)
|
||||||
@@ -148,7 +173,23 @@ void Mummy::step(float delta_time) {
|
|||||||
// no-op fins que algú la injecte amb `setDirection`.
|
// no-op fins que algú la injecte amb `setDirection`.
|
||||||
if (DX == 0.0F && DY == 0.0F) { return; }
|
if (DX == 0.0F && DY == 0.0F) { return; }
|
||||||
|
|
||||||
// Prova d'avançar. Si la caixa candidata és vàlida (dins del playfield
|
// Branca creuament: si aquest frame travessa la línia central d'un
|
||||||
|
// corredor perpendicular, amarra la caixa al centre del creuament i
|
||||||
|
// hi decideix. La mòmia consumeix només la porció de moviment fins al
|
||||||
|
// centre; el remanent queda per al pròxim frame, ja amb la (potser
|
||||||
|
// nova) direcció aplicada. Així la caixa 16×16 queda sempre alineada
|
||||||
|
// a la graella de corredors i els girs no xoquen per drift sub-píxel.
|
||||||
|
if (const auto SNAP = detectLaneCrossing(DX, DY); SNAP.has_value()) {
|
||||||
|
if (DX != 0.0F) {
|
||||||
|
pixel_rect_.x += *SNAP;
|
||||||
|
} else {
|
||||||
|
pixel_rect_.y += *SNAP;
|
||||||
|
}
|
||||||
|
decideAtIntersection(speed_ * delta_time);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avenç normal: si la caixa candidata és vàlida (dins del playfield
|
||||||
// i sense trepitjar cap sarc), s'aplica el moviment i s'acaba el frame.
|
// i sense trepitjar cap sarc), s'aplica el moviment i s'acaba el frame.
|
||||||
const auto CANDIDATE = candidateBox(DX, DY);
|
const auto CANDIDATE = candidateBox(DX, DY);
|
||||||
if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) {
|
if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) {
|
||||||
@@ -168,6 +209,42 @@ void Mummy::step(float delta_time) {
|
|||||||
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
|
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Mummy::detectLaneCrossing(float dx, float dy) const -> std::optional<float> {
|
||||||
|
const auto BOX = getCollisionRect();
|
||||||
|
if (dx != 0.0F) {
|
||||||
|
return findLaneCross(BOX.x, dx, static_cast<float>(Map::playfield_x), static_cast<float>(Map::pitch_x), Map::grid_cols);
|
||||||
|
}
|
||||||
|
return findLaneCross(BOX.y, dy, static_cast<float>(Map::playfield_y), static_cast<float>(Map::pitch_y), Map::grid_rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mummy::currentDirection() const -> Direction {
|
||||||
|
if (dir_x_ > 0.0F) { return Direction::RIGHT; }
|
||||||
|
if (dir_x_ < 0.0F) { return Direction::LEFT; }
|
||||||
|
if (dir_y_ > 0.0F) { return Direction::DOWN; }
|
||||||
|
if (dir_y_ < 0.0F) { return Direction::UP; }
|
||||||
|
return Direction::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Mummy::decideAtIntersection(float step_px) {
|
||||||
|
// Pool base: perpendiculars lliures (collectDirectionPools ja exclou la
|
||||||
|
// direcció actual i la mitja volta va al seu propi calaix, que ací
|
||||||
|
// ignorem — mai fem U-turn a un creuament).
|
||||||
|
auto pools = collectDirectionPools(step_px);
|
||||||
|
std::vector<Direction> pool = std::move(pools.perp);
|
||||||
|
|
||||||
|
// Afegim la recta si segueix essent lliure. A un creuament "net" ho és
|
||||||
|
// sempre (la mòmia hi ha arribat per aquest corredor), però als creua-
|
||||||
|
// ments de la vora del playfield la recta pot estar al límit i no
|
||||||
|
// permetre un pas més — en aquest cas la descartem.
|
||||||
|
const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px);
|
||||||
|
if (Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX)) {
|
||||||
|
pool.push_back(currentDirection());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool.empty()) { return; }
|
||||||
|
applyChoice(chooseDirection(pool));
|
||||||
|
}
|
||||||
|
|
||||||
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||||
struct Candidate {
|
struct Candidate {
|
||||||
float dx;
|
float dx;
|
||||||
@@ -183,7 +260,9 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
|||||||
DirectionPools pools;
|
DirectionPools pools;
|
||||||
for (const auto& c : ALL) {
|
for (const auto& c : ALL) {
|
||||||
const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F);
|
const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F);
|
||||||
if (IS_CURRENT) { continue; } // ja sabem que està bloquejada
|
// La direcció actual no forma part del pool: al bloqueig sabem que
|
||||||
|
// està bloquejada; al creuament, qui decideix l'afegeix a banda.
|
||||||
|
if (IS_CURRENT) { continue; }
|
||||||
const auto TEST = candidateBox(c.dx, c.dy);
|
const auto TEST = candidateBox(c.dx, c.dy);
|
||||||
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
|
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
|
||||||
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
|
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
|
||||||
@@ -216,8 +295,14 @@ void Mummy::applyChoice(Direction dir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mummy::onPlayerContact(Player* /*player*/) {
|
void Mummy::onPlayerContact(Player* player) {
|
||||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_hit",
|
||||||
|
.payload = getDamage(),
|
||||||
|
.player_id = (player != nullptr) ? player->getId() : -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
REGISTER_ENTITY("mummy", Mummy)
|
REGISTER_ENTITY("mummy", Mummy)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
@@ -12,11 +13,6 @@
|
|||||||
// (`chooseDirection`) que la base implementa com a aleatori uniforme
|
// (`chooseDirection`) que la base implementa com a aleatori uniforme
|
||||||
// sobre les direccions admissibles. Futurs derivats poden sesgar cap al
|
// sobre les direccions admissibles. Futurs derivats poden sesgar cap al
|
||||||
// jugador sense tocar la lògica de moviment.
|
// jugador sense tocar la lògica de moviment.
|
||||||
//
|
|
||||||
// Andamiatge: el concepte d'"intersecció" i la validació de direcció
|
|
||||||
// requereixen conèixer el mapa. De moment l'exposem com un conjunt de
|
|
||||||
// direccions admissibles que l'EntityManager injecta; la implementació
|
|
||||||
// real arribarà quan cablegem l'entitat al game loop.
|
|
||||||
class Mummy : public DynamicHazard {
|
class Mummy : public DynamicHazard {
|
||||||
public:
|
public:
|
||||||
enum class Direction : std::uint8_t { NONE,
|
enum class Direction : std::uint8_t { NONE,
|
||||||
@@ -32,11 +28,15 @@ class Mummy : public DynamicHazard {
|
|||||||
// aleatoria; si `allowed` està buit, retorna `NONE`.
|
// aleatoria; si `allowed` està buit, retorna `NONE`.
|
||||||
virtual auto chooseDirection(const std::vector<Direction>& allowed) -> Direction;
|
virtual auto chooseDirection(const std::vector<Direction>& allowed) -> Direction;
|
||||||
|
|
||||||
// Política de moviment "corridor_wanderer": camina recte fins que el
|
// Política "corridor_wanderer":
|
||||||
// pròxim tram la bloquejaria (mur del playfield o sarc). Quan troba un
|
// (a) A cada creuament de corredor (edge-detect de travessar la línia
|
||||||
// obstacle, recull les direccions perpendiculars vàlides (evitant
|
// central d'un corredor perpendicular) amarra la caixa al centre
|
||||||
// U-turn llevat que no hi hagi alternativa) i crida `chooseDirection`
|
// exacte del creuament i decideix: seguir recta o girar a una
|
||||||
// per decidir-ne una. No consumeix moviment el frame de gir.
|
// perpendicular lliure. Mai mitja volta.
|
||||||
|
// (b) Si el pròxim tram la bloqueja (mur del playfield o sarc), gira
|
||||||
|
// cap a una perpendicular vàlida i, només si no en queda cap, fa
|
||||||
|
// mitja volta.
|
||||||
|
// En ambdós casos el frame de gir no consumeix avenç extra.
|
||||||
void step(float delta_time) override;
|
void step(float delta_time) override;
|
||||||
|
|
||||||
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
||||||
@@ -72,6 +72,24 @@ class Mummy : public DynamicHazard {
|
|||||||
// Aplica la direcció triada (wrapper del switch en una sola crida).
|
// Aplica la direcció triada (wrapper del switch en una sola crida).
|
||||||
void applyChoice(Direction dir);
|
void applyChoice(Direction dir);
|
||||||
|
|
||||||
|
// Si aquest frame travessa la línia central d'un corredor perpendicular,
|
||||||
|
// retorna el delta (sobre l'eix actiu) per amarrar la caixa a aquesta
|
||||||
|
// línia. `nullopt` si no en creua cap. Els corredors perpendiculars
|
||||||
|
// estan a `base + k·pitch` (k ∈ [0..max_idx]); la comparació és
|
||||||
|
// estricta a un costat per no re-disparar quan la caixa ja hi està.
|
||||||
|
[[nodiscard]] auto detectLaneCrossing(float dx, float dy) const -> std::optional<float>;
|
||||||
|
|
||||||
|
// Decisió a un creuament: pool = perpendiculars lliures + direcció
|
||||||
|
// actual si continua essent lliure. Mai mitja volta. Si el pool sortís
|
||||||
|
// buit (cas degenerat, a la pràctica no s'hi arriba), manté la
|
||||||
|
// direcció actual; el frame següent, si està bloquejada, la branca de
|
||||||
|
// bloqueig ja la reorientarà.
|
||||||
|
void decideAtIntersection(float step_px);
|
||||||
|
|
||||||
|
// Direcció actual com a enum, reconstruïda a partir de `dir_x_/dir_y_`.
|
||||||
|
// S'usa al pool de creuament per afegir la recta com a candidata.
|
||||||
|
[[nodiscard]] auto currentDirection() const -> Direction;
|
||||||
|
|
||||||
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
|
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
|
||||||
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
||||||
AnimationComponent anim_walk_down_;
|
AnimationComponent anim_walk_down_;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -60,8 +61,14 @@ void RollingRock::step(float delta_time) {
|
|||||||
setDirection(0.0F, 0.0F);
|
setDirection(0.0F, 0.0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RollingRock::onPlayerContact(Player* /*player*/) {
|
void RollingRock::onPlayerContact(Player* player) {
|
||||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_hit",
|
||||||
|
.payload = getDamage(),
|
||||||
|
.player_id = (player != nullptr) ? player->getId() : -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
REGISTER_ENTITY("rolling_rock", RollingRock)
|
REGISTER_ENTITY("rolling_rock", RollingRock)
|
||||||
@@ -112,7 +112,7 @@ void Sarcophagus::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sarcophagus::notifyTileEntered(int col, int row) {
|
void Sarcophagus::notifyTilePainted(int col, int row) {
|
||||||
if (state_ != State::CLOSED) { return; }
|
if (state_ != State::CLOSED) { return; }
|
||||||
const TileKey KEY{.col = col, .row = row};
|
const TileKey KEY{.col = col, .row = row};
|
||||||
if (perimeter_tiles_.find(KEY) == perimeter_tiles_.end()) { return; }
|
if (perimeter_tiles_.find(KEY) == perimeter_tiles_.end()) { return; }
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class Sarcophagus : public Interactable {
|
|||||||
void place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked);
|
void place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked);
|
||||||
|
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
void notifyTileEntered(int col, int row) override;
|
void notifyTilePainted(int col, int row) override;
|
||||||
|
|
||||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||||
[[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); }
|
[[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
|
||||||
WallTrap::WallTrap(const fkyaml::node& yaml) {
|
WallTrap::WallTrap(const fkyaml::node& yaml) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("wall_trap");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("wall_trap");
|
||||||
@@ -21,9 +22,15 @@ WallTrap::WallTrap(const fkyaml::node& yaml) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallTrap::onPlayerContact(Player* /*player*/) {
|
void WallTrap::onPlayerContact(Player* player) {
|
||||||
if (!isActive()) { return; }
|
if (!isActive()) { return; }
|
||||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_hit",
|
||||||
|
.payload = getDamage(),
|
||||||
|
.player_id = (player != nullptr) ? player->getId() : -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
REGISTER_ENTITY("wall_trap", WallTrap)
|
REGISTER_ENTITY("wall_trap", WallTrap)
|
||||||
@@ -23,7 +23,7 @@ void Entity::onPlayerContact(Player* /*player*/) {
|
|||||||
// No-op per defecte.
|
// No-op per defecte.
|
||||||
}
|
}
|
||||||
|
|
||||||
void Entity::notifyTileEntered(int /*col*/, int /*row*/) {
|
void Entity::notifyTilePainted(int /*col*/, int /*row*/) {
|
||||||
// No-op per defecte; només `Sarcophagus` l'usa.
|
// No-op per defecte; només `Sarcophagus` l'usa.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,9 +77,10 @@ class Entity {
|
|||||||
virtual void onPlayerContact(Player* player);
|
virtual void onPlayerContact(Player* player);
|
||||||
|
|
||||||
// Hook que el `EntityManager` invoca per a cada entitat cada vegada
|
// Hook que el `EntityManager` invoca per a cada entitat cada vegada
|
||||||
// que el jugador entra en un tile nou. Només `Sarcophagus` ho consumeix
|
// que un tile es pinta (via PaintLayer, com a conseqüència d'un
|
||||||
// (per marcar perímetre visitat); per defecte no-op.
|
// jugador trepitjant un sòl pintable). Només `Sarcophagus` ho consumeix
|
||||||
virtual void notifyTileEntered(int col, int row);
|
// (per marcar perímetre); per defecte no-op.
|
||||||
|
virtual void notifyTilePainted(int col, int row);
|
||||||
|
|
||||||
[[nodiscard]] auto getName() const -> const std::string& { return name_; }
|
[[nodiscard]] auto getName() const -> const std::string& { return name_; }
|
||||||
[[nodiscard]] auto getTilePos() const -> TilePos { return tile_; }
|
[[nodiscard]] auto getTilePos() const -> TilePos { return tile_; }
|
||||||
@@ -87,6 +88,14 @@ class Entity {
|
|||||||
[[nodiscard]] auto collides() const -> bool { return collides_; }
|
[[nodiscard]] auto collides() const -> bool { return collides_; }
|
||||||
[[nodiscard]] auto interactable() const -> bool { return interactable_; }
|
[[nodiscard]] auto interactable() const -> bool { return interactable_; }
|
||||||
|
|
||||||
|
// Marca per a entitats transitòries: efectes (polvet, espurnes,
|
||||||
|
// explosions...) que viuen una sola animació i s'autoeliminen quan
|
||||||
|
// acaba. L'`EntityManager` purga totes les entitats amb `dead_=true`
|
||||||
|
// al final de cada `update`. Les entitats persistents (sarcs, momies,
|
||||||
|
// portes, jugadors) mai no la posen i hi viuen sempre.
|
||||||
|
[[nodiscard]] auto isDead() const -> bool { return dead_; }
|
||||||
|
void markDead() { dead_ = true; }
|
||||||
|
|
||||||
// Associar el bus una sola vegada (propietat externa, p.ex.
|
// Associar el bus una sola vegada (propietat externa, p.ex.
|
||||||
// `EntityManager`). Entitats que no publiquen events (Torch, trampes
|
// `EntityManager`). Entitats que no publiquen events (Torch, trampes
|
||||||
// estàtiques de moment) el poden ignorar.
|
// estàtiques de moment) el poden ignorar.
|
||||||
@@ -112,6 +121,17 @@ class Entity {
|
|||||||
// Entitats sense animació retornen `nullopt` → frame 0 per defecte.
|
// Entitats sense animació retornen `nullopt` → frame 0 per defecte.
|
||||||
[[nodiscard]] auto getAnimation() const -> const std::optional<AnimationComponent>& { return anim_; }
|
[[nodiscard]] auto getAnimation() const -> const std::optional<AnimationComponent>& { return anim_; }
|
||||||
|
|
||||||
|
// Alçada del cos de l'entitat en píxels. La consulten els filtres
|
||||||
|
// de col·lisió per altura: si un jugador en l'aire passa per damunt
|
||||||
|
// d'aquest valor (jump_z_ > height), no col·lisiona amb l'entitat —
|
||||||
|
// és la mecànica del salt per esquivar momies, roques, etc. Valor
|
||||||
|
// 0 = sense altura (no es pot esquivar saltant; per a entitats amb
|
||||||
|
// collides=false el camp és irrellevant). Per a obstacles que
|
||||||
|
// representen "murs" (sarcs, portes), el YAML els pot donar un
|
||||||
|
// valor molt alt (p.ex. 999) per fer-los infranquejables.
|
||||||
|
[[nodiscard]] auto getHeight() const -> int { return height_; }
|
||||||
|
void setHeight(int h) { height_ = h; }
|
||||||
|
|
||||||
// Caixa de col·lisió: per defecte és el `pixel_rect_` sencer (entitats
|
// Caixa de col·lisió: per defecte és el `pixel_rect_` sencer (entitats
|
||||||
// AABB-aligned). Si s'ha configurat un `CollisionBox`, retorna el
|
// AABB-aligned). Si s'ha configurat un `CollisionBox`, retorna el
|
||||||
// sub-rect dins del sprite — vida útil: validar col·lisions amb el
|
// sub-rect dins del sprite — vida útil: validar col·lisions amb el
|
||||||
@@ -149,6 +169,8 @@ class Entity {
|
|||||||
std::shared_ptr<Surface> surface_{nullptr};
|
std::shared_ptr<Surface> surface_{nullptr};
|
||||||
bool collides_{false};
|
bool collides_{false};
|
||||||
bool interactable_{false};
|
bool interactable_{false};
|
||||||
|
int height_{0};
|
||||||
|
bool dead_{false};
|
||||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
};
|
};
|
||||||
@@ -43,17 +43,20 @@ class EntityFactory {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// La macro genera un objecte anònim dins un namespace anònim: el seu
|
// La macro genera un objecte anònim dins un namespace anònim: el seu
|
||||||
// constructor s'executa abans de `main` i registra la factory. El nom del
|
// constructor s'executa abans de `main` i registra la factory. El nom
|
||||||
// símbol és únic per `CLASS` perquè el preprocessor el concatena; així
|
// del símbol es compon amb `TAG`, perquè així es poden registrar àlies
|
||||||
// dues entitats diferents no xoquen.
|
// per una mateixa classe (p.ex. dues claus diferents al YAML lligades a
|
||||||
|
// la mateixa classe C++) sense que els símbols xoquen. La forma curta
|
||||||
|
// `REGISTER_ENTITY(NAME, CLASS)` reusa `CLASS` com a tag; per àlies cal
|
||||||
|
// usar `REGISTER_ENTITY_AS` i passar un identificador únic.
|
||||||
//
|
//
|
||||||
// IMPORTANT: perquè el registrador es cridi, el `.cpp` que el conté ha
|
// IMPORTANT: perquè el registrador es cridi, el `.cpp` que el conté ha
|
||||||
// d'estar enllaçat directament a l'executable (no dins una STATIC lib
|
// d'estar enllaçat directament a l'executable (no dins una STATIC lib
|
||||||
// sense `--whole-archive`). Tots els `concrete/*.cpp` entren a APP_SOURCES.
|
// sense `--whole-archive`). Tots els `concrete/*.cpp` entren a APP_SOURCES.
|
||||||
#define REGISTER_ENTITY(NAME, CLASS) \
|
#define REGISTER_ENTITY_AS(NAME, CLASS, TAG) \
|
||||||
namespace { \
|
namespace { \
|
||||||
struct CLASS##_Registrar { \
|
struct TAG##_Registrar { \
|
||||||
CLASS##_Registrar() { \
|
TAG##_Registrar() { \
|
||||||
EntityFactory::instance().registerCreator( \
|
EntityFactory::instance().registerCreator( \
|
||||||
NAME, \
|
NAME, \
|
||||||
[](const fkyaml::node& yaml) -> std::unique_ptr<Entity> { \
|
[](const fkyaml::node& yaml) -> std::unique_ptr<Entity> { \
|
||||||
@@ -61,5 +64,7 @@ class EntityFactory {
|
|||||||
}); \
|
}); \
|
||||||
} \
|
} \
|
||||||
}; \
|
}; \
|
||||||
const CLASS##_Registrar g_##CLASS##_registrar; /* NOLINT(cert-err58-cpp) */ \
|
const TAG##_Registrar g_##TAG##_registrar; /* NOLINT(cert-err58-cpp) */ \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define REGISTER_ENTITY(NAME, CLASS) REGISTER_ENTITY_AS(NAME, CLASS, CLASS)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "game/entities/entity_loader.hpp"
|
#include "game/entities/entity_loader.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
@@ -31,23 +30,26 @@ namespace EntityLoader {
|
|||||||
auto it = cache.find(name);
|
auto it = cache.find(name);
|
||||||
if (it != cache.end()) { return it->second; }
|
if (it != cache.end()) { return it->second; }
|
||||||
|
|
||||||
const std::string FULL = Resource::Helper::resolve(templatePath(name));
|
const auto PATH = templatePath(name);
|
||||||
std::ifstream file(FULL);
|
auto bytes = Resource::Helper::loadFile(PATH);
|
||||||
if (!file.is_open()) {
|
if (bytes.empty()) {
|
||||||
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + FULL);
|
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + PATH);
|
||||||
}
|
}
|
||||||
fkyaml::node node = fkyaml::node::deserialize(file);
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
fkyaml::node node = fkyaml::node::deserialize(begin, end);
|
||||||
cache.emplace(name, node);
|
cache.emplace(name, node);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> {
|
auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> {
|
||||||
const auto FULL = Resource::Helper::resolve(level_yaml_path);
|
auto bytes = Resource::Helper::loadFile(level_yaml_path);
|
||||||
std::ifstream file(FULL);
|
if (bytes.empty()) {
|
||||||
if (!file.is_open()) {
|
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + level_yaml_path);
|
||||||
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + FULL);
|
|
||||||
}
|
}
|
||||||
fkyaml::node root = fkyaml::node::deserialize(file);
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
fkyaml::node root = fkyaml::node::deserialize(begin, end);
|
||||||
std::vector<fkyaml::node> spawns;
|
std::vector<fkyaml::node> spawns;
|
||||||
if (!root.contains("entities")) { return spawns; }
|
if (!root.contains("entities")) { return spawns; }
|
||||||
const auto& list = root["entities"];
|
const auto& list = root["entities"];
|
||||||
|
|||||||
@@ -24,18 +24,45 @@ EntityManager::EntityManager() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscripció al moviment del jugador: quan publica `on_tile_entered`
|
// Subscripció al pintat: el PaintLayer publica `tile_painted` amb
|
||||||
// amb payload `TilePos`, fem fan-out a totes les entitats perquè els
|
// payload `TilePos` un cop ha filtrat pintabilitat i aplicat la regla
|
||||||
// sarcòfags puguin actualitzar el seu `visited_`.
|
// "primer que pinta guanya". Fem fan-out a totes les entitats perquè
|
||||||
tile_sub_id_ = bus_.subscribe("on_tile_entered", [this](const Event& ev) {
|
// els sarcòfags puguin actualitzar el seu `visited_`.
|
||||||
|
tile_sub_id_ = bus_.subscribe("tile_painted", [this](const Event& ev) {
|
||||||
const auto* tile = std::get_if<TilePos>(&ev.payload);
|
const auto* tile = std::get_if<TilePos>(&ev.payload);
|
||||||
if (tile != nullptr) { notifyPlayerEnteredTile(tile->col, tile->row); }
|
if (tile != nullptr) { notifyPlayerTilePainted(tile->col, tile->row); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscripció a `player_exited` (publicat per Door en travessar la
|
||||||
|
// porta oberta amb un jugador): comptem una vegada per jugador i,
|
||||||
|
// quan tots els jugadors actius han eixit, emetem `level_complete`
|
||||||
|
// — l'event d'agregat que GameScene escolta per disparar la
|
||||||
|
// transició FSM a ROOM_OUTRO. La idempotència de Door (check
|
||||||
|
// `isExited`) garanteix que cada jugador suma com a màxim un cop.
|
||||||
|
exit_sub_id_ = bus_.subscribe("player_exited", [this](const Event& /*ev*/) {
|
||||||
|
++exited_players_;
|
||||||
|
if (level_complete_emitted_) { return; }
|
||||||
|
if (expected_players_ > 0 && exited_players_ >= expected_players_) {
|
||||||
|
level_complete_emitted_ = true;
|
||||||
|
bus_.publish(Event{
|
||||||
|
.name = "level_complete",
|
||||||
|
.payload = std::monostate{},
|
||||||
|
.player_id = -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityManager::~EntityManager() {
|
EntityManager::~EntityManager() {
|
||||||
bus_.unsubscribe(sarc_sub_id_);
|
bus_.unsubscribe(sarc_sub_id_);
|
||||||
bus_.unsubscribe(tile_sub_id_);
|
bus_.unsubscribe(tile_sub_id_);
|
||||||
|
bus_.unsubscribe(exit_sub_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityManager::setExpectedPlayerCount(int n) {
|
||||||
|
expected_players_ = n;
|
||||||
|
exited_players_ = 0;
|
||||||
|
level_complete_emitted_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||||
@@ -63,11 +90,20 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::update(float delta_time) {
|
void EntityManager::update(float delta_time) {
|
||||||
|
// ATENCIÓ: si en el futur una entitat publica al bus events que
|
||||||
|
// resulten en `entity_manager_.add()` (push_back a `entities_`), açò
|
||||||
|
// invalidaria l'iterador de la ranged-for. Avui no passa: els spawns
|
||||||
|
// runtime venen de fora del cicle d'`update` (p.ex. el polvet del
|
||||||
|
// saltador es spawneja durant `Player::tick`, abans d'aquest mètode).
|
||||||
for (auto& e : entities_) { e->update(delta_time); }
|
for (auto& e : entities_) { e->update(delta_time); }
|
||||||
|
// Purga d'entitats marcades mortes aquest frame (efectes acabats,
|
||||||
|
// p.ex. polvet d'aterratge). Les persistents (sarcs, momies, etc.)
|
||||||
|
// mai no es marquen.
|
||||||
|
std::erase_if(entities_, [](const auto& e) { return e->isDead(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::notifyPlayerEnteredTile(int col, int row) {
|
void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
||||||
for (auto& e : entities_) { e->notifyTileEntered(col, row); }
|
for (auto& e : entities_) { e->notifyTilePainted(col, row); }
|
||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ class Player;
|
|||||||
// 2. `update(dt)` propaga el tick a totes.
|
// 2. `update(dt)` propaga el tick a totes.
|
||||||
// 3. Comptador de sarcòfags oberts — quan iguala el total registrat,
|
// 3. Comptador de sarcòfags oberts — quan iguala el total registrat,
|
||||||
// crida `door_->open()` automàticament.
|
// crida `door_->open()` automàticament.
|
||||||
// 4. `notifyPlayerEnteredTile(col, row)` — fan-out del hook per a tots
|
// 4. `notifyPlayerTilePainted(col, row)` — fan-out del hook per a tots
|
||||||
// els sarcòfags (i qualsevol entitat futura que s'hi subscrigui via
|
// els sarcòfags (i qualsevol entitat futura que s'hi subscrigui via
|
||||||
// `notifyTileEntered`).
|
// `notifyTilePainted`).
|
||||||
//
|
//
|
||||||
// NO és un singleton: es crea una instància per nivell. De moment no
|
// NO és un singleton: es crea una instància per nivell. De moment no
|
||||||
// s'usa encara al GameScene (andamiatge pur).
|
// s'usa encara al GameScene (andamiatge pur).
|
||||||
@@ -74,10 +74,11 @@ class EntityManager {
|
|||||||
// reflecteixi l'estat del frame actual.
|
// reflecteixi l'estat del frame actual.
|
||||||
void checkPlayerCollisions(const std::vector<Player*>& players);
|
void checkPlayerCollisions(const std::vector<Player*>& players);
|
||||||
|
|
||||||
// Fan-out del hook de moviment de jugador: per a cada sarcòfag,
|
// Fan-out del hook de pintat: per a cada sarcòfag, cridar
|
||||||
// cridar notifyTileEntered. Les entitats no-sarcòfag ho ignoren per
|
// notifyTilePainted. Les entitats no-sarcòfag ho ignoren per defecte
|
||||||
// defecte (veure Entity::notifyTileEntered).
|
// (veure Entity::notifyTilePainted). Invocat com a reacció a
|
||||||
void notifyPlayerEnteredTile(int col, int row);
|
// `tile_painted` al bus (re-publicat pel PaintLayer un cop filtrat).
|
||||||
|
void notifyPlayerTilePainted(int col, int row);
|
||||||
|
|
||||||
// Carrega els spawns d'un YAML de nivell i els afegeix al manager:
|
// Carrega els spawns d'un YAML de nivell i els afegeix al manager:
|
||||||
// 1. Llegeix el bloc `entities:` via EntityLoader.
|
// 1. Llegeix el bloc `entities:` via EntityLoader.
|
||||||
@@ -89,10 +90,27 @@ class EntityManager {
|
|||||||
// `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`).
|
// `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`).
|
||||||
void loadFromLevel(const std::string& level_yaml_path);
|
void loadFromLevel(const std::string& level_yaml_path);
|
||||||
|
|
||||||
|
// Reset complet del nivell: destrueix totes les entitats, reseteja
|
||||||
|
// els comptadors interns (sarcòfags oberts, total, exited, flag de
|
||||||
|
// level_complete) i torna a cridar `loadFromLevel` per regenerar les
|
||||||
|
// entitats des del YAML. El bus i les seues subscripcions
|
||||||
|
// sobreviuen — això inclou les subscripcions externes (paint_layer_,
|
||||||
|
// hud_, players, GameScene). Cridat des de GameScene a la transició
|
||||||
|
// ROOM_OUTRO → ROOM_INTRO. `setExpectedPlayerCount` cal cridar-lo de
|
||||||
|
// nou després si vols agregació de level_complete al nou cicle.
|
||||||
|
void reload(const std::string& level_yaml_path);
|
||||||
|
|
||||||
|
// Fixa quants jugadors actius es consideren al recompte d'EXITED.
|
||||||
|
// Cridat per GameScene un cop instanciats els jugadors. Si manca,
|
||||||
|
// `expected_players_ = 0` i mai s'emet `level_complete` automàticament.
|
||||||
|
// Cridar-lo de nou al respawn (futur reload) reseteja el comptador.
|
||||||
|
void setExpectedPlayerCount(int n);
|
||||||
|
|
||||||
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
||||||
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
||||||
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
||||||
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
||||||
|
[[nodiscard]] auto exitedPlayerCount() const -> std::size_t { return exited_players_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EventBus bus_;
|
EventBus bus_;
|
||||||
@@ -100,6 +118,14 @@ class EntityManager {
|
|||||||
Door* door_{nullptr};
|
Door* door_{nullptr};
|
||||||
std::size_t total_sarcs_{0};
|
std::size_t total_sarcs_{0};
|
||||||
std::size_t opened_sarcs_{0};
|
std::size_t opened_sarcs_{0};
|
||||||
|
// Recompte d'`player_exited`: incrementat a la subscripció al bus,
|
||||||
|
// resetejat per `setExpectedPlayerCount`. Quan `exited_players_` arriba
|
||||||
|
// a `expected_players_` (i tots dos > 0), publica `level_complete`
|
||||||
|
// amb `player_id = -1` (event d'agregat, no atribuible a un jugador).
|
||||||
|
int expected_players_{0};
|
||||||
|
int exited_players_{0};
|
||||||
|
bool level_complete_emitted_{false};
|
||||||
EventBus::SubId sarc_sub_id_{0};
|
EventBus::SubId sarc_sub_id_{0};
|
||||||
EventBus::SubId tile_sub_id_{0};
|
EventBus::SubId tile_sub_id_{0};
|
||||||
|
EventBus::SubId exit_sub_id_{0};
|
||||||
};
|
};
|
||||||
@@ -63,9 +63,20 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
|||||||
for (const auto& entity : entities_) {
|
for (const auto& entity : entities_) {
|
||||||
if (!entity->collides()) { continue; }
|
if (!entity->collides()) { continue; }
|
||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
|
const int E_HEIGHT = entity->getHeight();
|
||||||
for (auto* p : players) {
|
for (auto* p : players) {
|
||||||
if (p == nullptr) { continue; }
|
if (p == nullptr || p->isExited()) { continue; }
|
||||||
const auto PBOX = p->getBaseBox();
|
// Filtre per altura: si el jugador està en l'aire i està per
|
||||||
|
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
||||||
|
// És el mecanisme del salt per esquivar momies, roques, etc.
|
||||||
|
// Quan height és 0 (entitat sense altura declarada), el filtre
|
||||||
|
// no es dispara mai i la col·lisió funciona com abans.
|
||||||
|
if (p->isAirborne() && E_HEIGHT > 0 && p->getJumpZ() > static_cast<float>(E_HEIGHT)) { continue; }
|
||||||
|
// Contacte actor-actor amb hit-box petita: la caixa base
|
||||||
|
// 16×16 servia per moure (xoc amb murs), però per a l'event
|
||||||
|
// "toca una entitat" volem precisió — que la momia mate quan
|
||||||
|
// els píxels centrals es solapen, no quan els caps es freguen.
|
||||||
|
const auto PBOX = p->getHitBox();
|
||||||
const bool OVERLAP = PBOX.x + PBOX.w > BOX.x && BOX.x + BOX.w > PBOX.x && PBOX.y + PBOX.h > BOX.y && BOX.y + BOX.h > PBOX.y;
|
const bool OVERLAP = PBOX.x + PBOX.w > BOX.x && BOX.x + BOX.w > PBOX.x && PBOX.y + PBOX.h > BOX.y && BOX.y + BOX.h > PBOX.y;
|
||||||
if (OVERLAP) { entity->onPlayerContact(p); }
|
if (OVERLAP) { entity->onPlayerContact(p); }
|
||||||
}
|
}
|
||||||
@@ -105,6 +116,18 @@ namespace {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Llig `collision.height` (alçada del cos en píxels) de la plantilla.
|
||||||
|
// 0 si no està declarada — ho usen els filtres de col·lisió per altura
|
||||||
|
// del salt: un jugador per damunt d'aquest valor no col·lisiona amb
|
||||||
|
// l'entitat. Per a obstacles "tipus mur" (sarc, porta) el YAML els
|
||||||
|
// dóna un valor molt alt per fer-los infranquejables.
|
||||||
|
auto readHeight(const fkyaml::node& tmpl) -> int {
|
||||||
|
if (!tmpl.contains("collision")) { return 0; }
|
||||||
|
const auto& c = tmpl["collision"];
|
||||||
|
if (!c.contains("height")) { return 0; }
|
||||||
|
return c["height"].get_value<int>();
|
||||||
|
}
|
||||||
|
|
||||||
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
||||||
// de la plantilla d'entitat.
|
// de la plantilla d'entitat.
|
||||||
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
|
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
|
||||||
@@ -208,6 +231,7 @@ namespace {
|
|||||||
if (const auto BOX = readCollisionBox(tmpl, sprite_out.width, sprite_out.height); BOX.has_value()) {
|
if (const auto BOX = readCollisionBox(tmpl, sprite_out.width, sprite_out.height); BOX.has_value()) {
|
||||||
entity->setCollisionBox(*BOX);
|
entity->setCollisionBox(*BOX);
|
||||||
}
|
}
|
||||||
|
entity->setHeight(readHeight(tmpl));
|
||||||
anchor_out = readAnchor(tmpl);
|
anchor_out = readAnchor(tmpl);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@@ -267,6 +291,34 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Col·loca la porta al mur trasser segons `Map::door_col`. La porta
|
||||||
|
// viu sempre al `wall_north`; només la columna varia per nivell. El
|
||||||
|
// sprite (típicament 48×32) es centra horitzontalment sobre la
|
||||||
|
// columna transitable i s'alinea verticalment de manera que la seua
|
||||||
|
// vora inferior coincideixi amb la base del strip del mur trasser
|
||||||
|
// (= `playfield_y`). Amb sprite 48×32 i `back_wall_h = 24` això vol
|
||||||
|
// dir que els 8 px superiors del sprite queden dins del HUD i hi
|
||||||
|
// queden tapats — comportament intencional.
|
||||||
|
auto placeDoor(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite) -> bool {
|
||||||
|
if (Map::door_col <= 0 || Map::tile_size <= 0) {
|
||||||
|
std::cerr << "EntityManager: door requereix `geometry.door_col` definit al mapa\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const float CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
|
||||||
|
const SDL_FRect RENDER{
|
||||||
|
.x = CENTER_X - (sprite.width / 2.0F),
|
||||||
|
.y = static_cast<float>(Map::playfield_y) - sprite.height,
|
||||||
|
.w = sprite.width,
|
||||||
|
.h = sprite.height,
|
||||||
|
};
|
||||||
|
entity->setPixelRect(RENDER);
|
||||||
|
|
||||||
|
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
||||||
|
manager.add(std::move(entity));
|
||||||
|
if (door_ptr != nullptr) { manager.setDoor(door_ptr); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const std::string& anchor, const fkyaml::node& spawn) -> bool {
|
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const std::string& anchor, const fkyaml::node& spawn) -> bool {
|
||||||
if (!spawn.contains("position")) {
|
if (!spawn.contains("position")) {
|
||||||
std::cerr << "EntityManager: " << type << " sense position\n";
|
std::cerr << "EntityManager: " << type << " sense position\n";
|
||||||
@@ -298,17 +350,30 @@ namespace {
|
|||||||
}
|
}
|
||||||
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});
|
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});
|
||||||
|
|
||||||
if (type == "door") {
|
|
||||||
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
|
||||||
manager.add(std::move(entity));
|
manager.add(std::move(entity));
|
||||||
if (door_ptr != nullptr) { manager.setDoor(door_ptr); }
|
|
||||||
} else {
|
|
||||||
manager.add(std::move(entity));
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void EntityManager::reload(const std::string& level_yaml_path) {
|
||||||
|
// Destrucció ordenada: les entitats moren aquí; les seues
|
||||||
|
// subscripcions implícites desapareixen amb la seua vida (cap d'elles
|
||||||
|
// se subscriu directament al bus — només publiquen). El bus i les
|
||||||
|
// subscripcions externes (PaintLayer, HUD, GameScene) no es toquen.
|
||||||
|
entities_.clear();
|
||||||
|
door_ = nullptr;
|
||||||
|
total_sarcs_ = 0;
|
||||||
|
opened_sarcs_ = 0;
|
||||||
|
// No reset de `expected_players_`: GameScene el tornarà a fixar
|
||||||
|
// (amb el mateix valor o un de nou) després del reload, com a part
|
||||||
|
// del respawn dels jugadors. Reseteig dels comptadors d'EXITED i
|
||||||
|
// del flag d'emissió perquè el cicle següent pugui reagregar net.
|
||||||
|
exited_players_ = 0;
|
||||||
|
level_complete_emitted_ = false;
|
||||||
|
|
||||||
|
loadFromLevel(level_yaml_path);
|
||||||
|
}
|
||||||
|
|
||||||
void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
||||||
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
|
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
|
||||||
int loaded = 0;
|
int loaded = 0;
|
||||||
@@ -322,9 +387,15 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
|||||||
auto entity = buildEntity(TYPE, sprite, anchor);
|
auto entity = buildEntity(TYPE, sprite, anchor);
|
||||||
if (entity == nullptr) { continue; }
|
if (entity == nullptr) { continue; }
|
||||||
|
|
||||||
const bool OK = (TYPE == "sarcophagus")
|
bool ok = false;
|
||||||
? placeSarcophagus(*this, std::move(entity), sprite, spawn)
|
if (TYPE == "sarcophagus") {
|
||||||
: placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn);
|
ok = placeSarcophagus(*this, std::move(entity), sprite, spawn);
|
||||||
|
} else if (TYPE == "door") {
|
||||||
|
ok = placeDoor(*this, std::move(entity), sprite);
|
||||||
|
} else {
|
||||||
|
ok = placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn);
|
||||||
|
}
|
||||||
|
const bool OK = ok;
|
||||||
if (OK) { ++loaded; }
|
if (OK) { ++loaded; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,17 +23,41 @@ class Entity;
|
|||||||
// consumir events tipats amb `std::get_if`. Si apareixen més tipus de
|
// consumir events tipats amb `std::get_if`. Si apareixen més tipus de
|
||||||
// payload, s'amplia aquí sense tocar cridants existents.
|
// payload, s'amplia aquí sense tocar cridants existents.
|
||||||
|
|
||||||
// Tile (col, row) — payload de `on_tile_entered`. No és un `pair<int,int>`
|
// Tile (col, row) — payload de `tile_painted` (i altres events de tile sense
|
||||||
// per evitar alias ambigus al variant (dos ints sense nom).
|
// metadada extra). No és un `pair<int,int>` per evitar alias ambigus al
|
||||||
|
// variant (dos ints sense nom).
|
||||||
struct TilePos {
|
struct TilePos {
|
||||||
int col{0};
|
int col{0};
|
||||||
int row{0};
|
int row{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Tile pintat pel jugador — payload de `on_tile_entered`. Porta l'índex
|
||||||
|
// de tile amb què el jugador pinta (cada Player pot pintar amb un tile
|
||||||
|
// diferent, llegit del YAML del mapa). El PaintLayer filtra pintabilitat,
|
||||||
|
// marca i re-publica `tile_painted` amb només la posició.
|
||||||
|
struct PaintedTile {
|
||||||
|
int col{0};
|
||||||
|
int row{0};
|
||||||
|
int tile_index{-1};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Coordenades en píxels al canvas. Payload genèric per a events que
|
||||||
|
// volen indicar "una cosa ha passat aquí" — actualment `player_jumped`
|
||||||
|
// i `player_landed`, que el GameScene escolta per spawnejar polvet.
|
||||||
|
struct WorldPos {
|
||||||
|
float x{0.0F};
|
||||||
|
float y{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
struct Event {
|
struct Event {
|
||||||
std::string name;
|
std::string name;
|
||||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos>;
|
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos>;
|
||||||
Payload payload;
|
Payload payload;
|
||||||
|
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
||||||
|
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
||||||
|
// entitat). Usat per al HUD (dispatch a P1/P2) i per a regles futures
|
||||||
|
// de puntuació que atribueixin accions al jugador concret.
|
||||||
|
int player_id{-1};
|
||||||
};
|
};
|
||||||
|
|
||||||
class EventBus {
|
class EventBus {
|
||||||
@@ -71,6 +95,12 @@ class EventBus {
|
|||||||
void publish(const std::string& name, TilePos tile) {
|
void publish(const std::string& name, TilePos tile) {
|
||||||
publish(Event{.name = name, .payload = tile});
|
publish(Event{.name = name, .payload = tile});
|
||||||
}
|
}
|
||||||
|
void publish(const std::string& name, PaintedTile painted) {
|
||||||
|
publish(Event{.name = name, .payload = painted});
|
||||||
|
}
|
||||||
|
void publish(const std::string& name, WorldPos pos) {
|
||||||
|
publish(Event{.name = name, .payload = pos});
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Subscription {
|
struct Subscription {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
class EventBus;
|
class EventBus;
|
||||||
@@ -21,9 +23,37 @@ class Player : public AnimatedSprite {
|
|||||||
SDL_Scancode down;
|
SDL_Scancode down;
|
||||||
SDL_Scancode left;
|
SDL_Scancode left;
|
||||||
SDL_Scancode right;
|
SDL_Scancode right;
|
||||||
|
SDL_Scancode jump;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Geometria carregada des de `data/player/player.yaml`. `sprite_*` són
|
// Estat d'alt nivell del jugador. Ortogonal a `Facing`/`Cardinal`
|
||||||
|
// (direcció) i a `being_pushed_` (flag d'animació). Determina què pot
|
||||||
|
// fer el jugador aquest frame:
|
||||||
|
// GROUNDED → input normal, pot saltar.
|
||||||
|
// AIRBORNE → física del salt; ignora input direccional, sense
|
||||||
|
// col·lisió amb caminants però sí amb altres
|
||||||
|
// saltadors a la mateixa franja d'alçada (rebot).
|
||||||
|
// LANDING_STUN → 200ms post-aterratge, sense input ni moviment.
|
||||||
|
// HURT → projectat sense control (cau d'algú al damunt o,
|
||||||
|
// en el futur, fuetada): velocitat inicial alta
|
||||||
|
// que decau per inèrcia fins a parar (~2 tiles).
|
||||||
|
// No col·lisiona amb cap altre jugador. Quan
|
||||||
|
// s'atura, torna a GROUNDED.
|
||||||
|
enum class State : std::uint8_t {
|
||||||
|
GROUNDED,
|
||||||
|
AIRBORNE,
|
||||||
|
LANDING_STUN,
|
||||||
|
HURT,
|
||||||
|
// Jugador que ha eixit per la porta de la sala. Despawn lògic:
|
||||||
|
// no es renderitza, no processa input, no es mou, no col·lisiona
|
||||||
|
// amb res ni rep contacte d'entitats. Idempotent: les crides
|
||||||
|
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
|
||||||
|
// (cridada per GameScene en transició OUTRO → INTRO).
|
||||||
|
EXITED,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
||||||
|
// `sprite_*` són
|
||||||
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
|
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
|
||||||
// 12 píxels superiors són el cap i els 12 inferiors la base).
|
// 12 píxels superiors són el cap i els 12 inferiors la base).
|
||||||
// La caixa de col·lisió és un quadrat `collision_size` concèntric amb el
|
// La caixa de col·lisió és un quadrat `collision_size` concèntric amb el
|
||||||
@@ -38,23 +68,167 @@ class Player : public AnimatedSprite {
|
|||||||
inline static int collision_x_offset = 0;
|
inline static int collision_x_offset = 0;
|
||||||
inline static int collision_y_offset = 0;
|
inline static int collision_y_offset = 0;
|
||||||
|
|
||||||
|
// Caixa de "contacte" més petita que la caixa de col·lisió: s'usa per
|
||||||
|
// a les interaccions actor-actor (player↔player, player↔momia/trampa),
|
||||||
|
// on volem que el contacte sigui precís i no dispari a "distància"
|
||||||
|
// de la vora del sprite. La caixa de col·lisió gran continua usant-se
|
||||||
|
// per xocar amb murs i sarcòfags (per mantenir el carril del
|
||||||
|
// passadís). Sense `hit_size` al YAML, s'iguala a `collision_size` i
|
||||||
|
// el comportament és idèntic a l'anterior.
|
||||||
|
inline static int hit_size = 0;
|
||||||
|
inline static int hit_x_offset = 0;
|
||||||
|
inline static int hit_y_offset = 0;
|
||||||
|
|
||||||
|
// Alçada del cos del jugador en píxels (camp `collision.height` del
|
||||||
|
// YAML, mateixa convenció que entitats com mummy o rolling_rock).
|
||||||
|
// L'usen els filtres de col·lisió per altura: si un altre actor ens
|
||||||
|
// creua mentre `jump_z_` és superior a aquest valor, no col·lisionem.
|
||||||
|
inline static int body_height = 0;
|
||||||
|
|
||||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||||
static void loadFrom(const std::string& yaml_path);
|
static void loadFrom(const std::string& yaml_path);
|
||||||
|
|
||||||
Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info);
|
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
||||||
|
|
||||||
|
// Update en una sola crida: manté l'API anterior per als tests i
|
||||||
|
// escenaris amb un sol jugador. Internament és equivalent a cridar
|
||||||
|
// `tick(dt)` seguit d'`applyMovement(dt)`.
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
|
|
||||||
|
// Fase 1: llig l'input d'aquest frame i emmagatzema la velocitat
|
||||||
|
// intencionada a `intended_vx_` / `intended_vy_`. No mou el jugador.
|
||||||
|
// Separada d'`applyMovement` perquè el GameScene puga resoldre
|
||||||
|
// interaccions entre jugadors (empentes, neutralitzacions head-on)
|
||||||
|
// abans que ningú consume la posició.
|
||||||
|
void tick(float delta_time);
|
||||||
|
|
||||||
|
// Fase 2: consumeix `intended_vx_`/`intended_vy_` (possiblement
|
||||||
|
// modificats pel resolver), crida `tryMove` contra el mapa i els
|
||||||
|
// altres jugadors, fa snap al passadís, clamp al playfield, avança
|
||||||
|
// l'animació i publica `on_tile_entered` si ha canviat de tile.
|
||||||
|
void applyMovement(float delta_time);
|
||||||
|
|
||||||
|
// Identificador local (0 = P1, 1 = P2…). S'usa als events del bus
|
||||||
|
// perquè els consumidors (HUD, futures regles de puntuació) sàpiguen
|
||||||
|
// quin jugador ha originat l'event. No té cap vinculació amb la
|
||||||
|
// xarxa — és únic per dins del procés.
|
||||||
|
[[nodiscard]] auto getId() const -> int { return id_; }
|
||||||
|
|
||||||
|
// Accés i mutació de la velocitat intencionada d'aquest frame. El
|
||||||
|
// GameScene els toca entre `tick` i `applyMovement` per implementar
|
||||||
|
// col·lisions jugador↔jugador (escalar a push_factor, anul·lar en
|
||||||
|
// head-on). Expressades en píxels/segon, no en píxels/frame.
|
||||||
|
// Amb inèrcia activada, `intended_v*` pot ser no-zero encara que
|
||||||
|
// l'usuari no estiga prement res (coast residual).
|
||||||
|
[[nodiscard]] auto getIntendedVX() const -> float { return intended_vx_; }
|
||||||
|
[[nodiscard]] auto getIntendedVY() const -> float { return intended_vy_; }
|
||||||
|
void setIntendedVelocity(float vx, float vy) {
|
||||||
|
intended_vx_ = vx;
|
||||||
|
intended_vy_ = vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Velocitat crua d'input d'aquest frame (-SPEED, 0 o +SPEED per eix).
|
||||||
|
// A diferència de `intended_v*`, no té en compte la inèrcia: un jugador
|
||||||
|
// que coasta sense prémer res té `input_v* == 0`. El resolver PvP del
|
||||||
|
// GameScene l'usa per decidir "parat vs en moviment" (criteri: un
|
||||||
|
// coast sense input compta com parat — l'empenta i el head-on només
|
||||||
|
// es disparen quan hi ha intenció explícita del jugador).
|
||||||
|
[[nodiscard]] auto getInputVX() const -> float { return input_vx_; }
|
||||||
|
[[nodiscard]] auto getInputVY() const -> float { return input_vy_; }
|
||||||
|
|
||||||
|
// Llista de tots els jugadors del nivell (incloent-se ell mateix —
|
||||||
|
// la callback ignora `this`). La caixa del jugador es considera un
|
||||||
|
// sòlid a la callback de col·lisió: així `tryMove` no permet que un
|
||||||
|
// jugador travesse l'altre, i els desplaçaments parcials (push
|
||||||
|
// contra mur) s'emparen automàticament.
|
||||||
|
void setOtherPlayers(const std::vector<Player*>* all) { all_players_ = all; }
|
||||||
|
|
||||||
|
// Marcat pel resolver jugador↔jugador quan aquest frame el jugador
|
||||||
|
// és arrossegat per una empenta (no té input propi; es desplaça
|
||||||
|
// perquè l'altre l'empeny). Determina que l'animació del frame siga
|
||||||
|
// `stand_*` encara que la posició canvie. El flag es reseteja a
|
||||||
|
// l'inici de cada `tick`, així que el resolver l'ha de tornar a
|
||||||
|
// posar cada frame que el push s'haja de mantenir.
|
||||||
|
void setBeingPushed(bool pushed) { being_pushed_ = pushed; }
|
||||||
|
|
||||||
|
// Factor d'escala d'empenta jugador↔jugador (viu al YAML del mapa).
|
||||||
|
// L'usa el resolver de GameScene per al push horitzontal, i també
|
||||||
|
// `snapToLane` quan descobreix que un altre jugador li bloqueja el
|
||||||
|
// carril objectiu: per coherència, la pushada del snap va al mateix
|
||||||
|
// ritme que l'horitzontal. Si no es crida, el valor per defecte 1.0
|
||||||
|
// manté el snap a velocitat plena (comportament pre-multiplayer).
|
||||||
|
void setPushFactor(float factor) { push_factor_ = factor; }
|
||||||
|
|
||||||
// Y de la base del jugador (cara inferior de la caixa de colisió). Usada
|
// Y de la base del jugador (cara inferior de la caixa de colisió). Usada
|
||||||
// per al Z-order contra els sarcs: peus per sobre d'un sarc → darrere;
|
// per al Z-order contra els sarcs: peus per sobre d'un sarc → darrere;
|
||||||
// per sota → davant.
|
// per sota → davant. NO restem `jump_z_`: el saltador no canvia de
|
||||||
|
// tile lògica, només s'eleva visualment, i el sorting ha de respectar
|
||||||
|
// la posició al món (un sarc al nord segueix darrere encara que el
|
||||||
|
// sprite del jugador estigui amunt). Veure també `getGroundFeetY`,
|
||||||
|
// que coincideix amb aquest mètode i s'usa específicament per
|
||||||
|
// marcar la posició de la sombra a nivell de sòl.
|
||||||
[[nodiscard]] auto getFeetY() const -> float {
|
[[nodiscard]] auto getFeetY() const -> float {
|
||||||
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
|
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alies semàntic: els peus del jugador al sòl. Coincideix amb
|
||||||
|
// `getFeetY` i s'usa al callsite del render de la sombra per fer
|
||||||
|
// explícit que la sombra es pinta al pla del sòl, no a la posició
|
||||||
|
// visual del sprite.
|
||||||
|
[[nodiscard]] auto getGroundFeetY() const -> float { return getFeetY(); }
|
||||||
|
|
||||||
|
// Estat actual de la màquina d'estats. L'usa el render (animació
|
||||||
|
// jump_*), el resolver de col·lisions (filtre per altura) i el
|
||||||
|
// GameScene (per saber si renderitzar la sombra).
|
||||||
|
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||||
|
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
||||||
|
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
||||||
|
[[nodiscard]] auto isExited() const -> bool { return state_ == State::EXITED; }
|
||||||
|
// Tangible per a contactes jugador↔jugador: només els estats a terra
|
||||||
|
// amb cos sòlid. AIRBORNE i HURT són intangibles (es travessen).
|
||||||
|
// EXITED tampoc — el jugador ha eixit lògicament i ja no és al món.
|
||||||
|
[[nodiscard]] auto isPvpTangible() const -> bool {
|
||||||
|
return state_ == State::GROUNDED || state_ == State::LANDING_STUN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Despawn lògic: el jugador acaba de creuar la porta de sortida.
|
||||||
|
// Idempotent — només la primera crida fa la transició; les
|
||||||
|
// posteriors són no-op. La publicació del `player_exited` al bus
|
||||||
|
// la fa el caller (típicament `Door::onPlayerContact`) per
|
||||||
|
// mantenir aquesta capçalera lleugera (sense incloure event_bus).
|
||||||
|
// Definit inline perquè els consumidors de baix nivell (test
|
||||||
|
// d'entitats) puguen enllaçar Door sense arrossegar player.cpp.
|
||||||
|
void exit() {
|
||||||
|
if (state_ == State::EXITED) { return; }
|
||||||
|
state_ = State::EXITED;
|
||||||
|
intended_vx_ = 0.0F;
|
||||||
|
intended_vy_ = 0.0F;
|
||||||
|
// Important: també resetegem `input_v*` perquè el resolver
|
||||||
|
// jugador↔jugador (`resolvePlayerVsPlayer`) consulta
|
||||||
|
// `getInputVX/Y` per decidir si un jugador "es mou". Un EXITED
|
||||||
|
// amb input fantasma del moment d'entrar a la porta dispararia
|
||||||
|
// la branca head-on i bloquejaria l'altre jugador en sec.
|
||||||
|
input_vx_ = 0.0F;
|
||||||
|
input_vy_ = 0.0F;
|
||||||
|
being_pushed_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
||||||
|
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
||||||
|
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
|
||||||
|
// (es pot cridar des de qualsevol estat).
|
||||||
|
void respawn(float x, float y);
|
||||||
|
|
||||||
|
// Alçada actual del salt (píxels visuals per damunt del sòl).
|
||||||
|
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
||||||
|
// per altura.
|
||||||
|
[[nodiscard]] auto getJumpZ() const -> float { return jump_z_; }
|
||||||
|
|
||||||
// Injecta el bus del nivell. El Player publica `on_tile_entered` cada
|
// Injecta el bus del nivell. El Player publica `on_tile_entered` cada
|
||||||
// vegada que el centre de la seua caixa de col·lisió entra en una
|
// vegada que el centre de la seua caixa de col·lisió entra en una
|
||||||
// cel·la nova del tilemap. Si el bus no s'injecta, no emet res.
|
// cel·la nova del tilemap, amb payload `PaintedTile{col,row,paint_tile_index_}`.
|
||||||
|
// El PaintLayer filtra pintabilitat i re-publica `tile_painted` a qui
|
||||||
|
// correspongui (sarcòfags). Si el bus no s'injecta, no emet res.
|
||||||
void setEventBus(EventBus* bus) { bus_ = bus; }
|
void setEventBus(EventBus* bus) { bus_ = bus; }
|
||||||
|
|
||||||
// Tile actual del jugador (centre de `baseBox()` convertit a
|
// Tile actual del jugador (centre de `baseBox()` convertit a
|
||||||
@@ -63,21 +237,49 @@ class Player : public AnimatedSprite {
|
|||||||
[[nodiscard]] auto getCurrentTile() const -> std::pair<int, int>;
|
[[nodiscard]] auto getCurrentTile() const -> std::pair<int, int>;
|
||||||
|
|
||||||
// Rectangle de col·lisió del jugador, en coordenades del canvas. Usat
|
// Rectangle de col·lisió del jugador, en coordenades del canvas. Usat
|
||||||
// pel mapa de col·lisions i per les entitats que comproven contacte
|
// pel mapa de col·lisions (tryMove contra murs/sarcòfags).
|
||||||
// amb el jugador (momies, trampes, porta).
|
|
||||||
[[nodiscard]] auto getBaseBox() const -> SDL_FRect;
|
[[nodiscard]] auto getBaseBox() const -> SDL_FRect;
|
||||||
|
|
||||||
|
// Rectangle de contacte actor-actor (hit-box), més petit que la caixa
|
||||||
|
// base i concèntric amb ella. L'usen `checkPlayerCollisions`
|
||||||
|
// (momies, trampes, porta) i el resolver jugador↔jugador per
|
||||||
|
// decidir quan es toquen.
|
||||||
|
[[nodiscard]] auto getHitBox() const -> SDL_FRect;
|
||||||
|
|
||||||
|
// Versió estàtica: deriva la hit-box a partir d'una caixa base
|
||||||
|
// arbitrària (p. ex. la posició proposada per `tryMove` abans
|
||||||
|
// d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora
|
||||||
|
// de Player.
|
||||||
|
[[nodiscard]] static auto hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect;
|
||||||
|
|
||||||
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
|
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
|
||||||
// caixa al callback de col·lisió. La base del Player és més petita
|
// caixa al callback de col·lisió. La base del Player és més petita
|
||||||
// que el sprite (el cap pot sobresortir per murs/sarcòfags sense
|
// que el sprite (el cap pot sobresortir per murs/sarcòfags sense
|
||||||
// bloquejar el moviment).
|
// bloquejar el moviment).
|
||||||
[[nodiscard]] auto collisionBox() const -> SDL_FRect override { return getBaseBox(); }
|
[[nodiscard]] auto collisionBox() const -> SDL_FRect override { return getBaseBox(); }
|
||||||
|
|
||||||
|
// Render del jugador. Mentre està en l'aire, el sprite es desplaça
|
||||||
|
// visualment cap amunt `jump_z_` píxels (el cos del jugador, no la
|
||||||
|
// sombra al sòl, que la pinta el GameScene a banda). La col·lisió i
|
||||||
|
// la posició al món no s'alteren.
|
||||||
|
void render() override;
|
||||||
|
|
||||||
|
// Pinta una sombra translúcida sota el jugador a nivell de sòl
|
||||||
|
// (sense aplicar `jump_z_`). El GameScene la crida en una passada
|
||||||
|
// pre-actors, així queda per sota dels actors i del propi jugador.
|
||||||
|
// No-op si el jugador no està en l'aire.
|
||||||
|
void renderShadow() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Alias privat històric. Mantingut perquè viu dins de l'implementació
|
// Alias privat històric. Mantingut perquè viu dins de l'implementació
|
||||||
// (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`.
|
// (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`.
|
||||||
[[nodiscard]] auto baseBox() const -> SDL_FRect { return getBaseBox(); }
|
[[nodiscard]] auto baseBox() const -> SDL_FRect { return getBaseBox(); }
|
||||||
|
|
||||||
|
// Callback de col·lisió per a `tryMove`: true si la caixa proposada
|
||||||
|
// xoca amb el mapa o amb un altre jugador. Membre per mantenir la
|
||||||
|
// complexitat cognitiva d'`applyMovement` sota el llindar.
|
||||||
|
[[nodiscard]] auto collidesWithWorld(const SDL_FRect& box) const -> bool;
|
||||||
|
|
||||||
// Clamp de la caixa de col·lisió al playfield (no al sprite sencer:
|
// Clamp de la caixa de col·lisió al playfield (no al sprite sencer:
|
||||||
// el cap pot sobresortir pel mur superior sense afectar el joc).
|
// el cap pot sobresortir pel mur superior sense afectar el joc).
|
||||||
void clampToPlayfield();
|
void clampToPlayfield();
|
||||||
@@ -101,7 +303,57 @@ class Player : public AnimatedSprite {
|
|||||||
float (Player::*get_pos)() const,
|
float (Player::*get_pos)() const,
|
||||||
void (Player::*set_pos)(float));
|
void (Player::*set_pos)(float));
|
||||||
|
|
||||||
|
// Quan el snap lateral trobaria un altre jugador al carril objectiu,
|
||||||
|
// l'empentem el mateix step en el mateix eix. Així `this` pot entrar
|
||||||
|
// al passadís encara que l'altre estiga estacionat al mitj — mateix
|
||||||
|
// contracte que el push horitzontal però disparat pel snap. Retorna
|
||||||
|
// true si la push ha quedat neta (ningú xoca amb el mapa ni amb un
|
||||||
|
// tercer jugador); false fa que el caller revertisca el propi snap.
|
||||||
|
// Marca els blockers amb `being_pushed_` per a la seua animació.
|
||||||
|
auto tryPushSnapBlockers(
|
||||||
|
float signed_step,
|
||||||
|
float (Player::*get_pos)() const,
|
||||||
|
void (Player::*set_pos)(float)) -> bool;
|
||||||
|
|
||||||
|
int id_{-1};
|
||||||
Controls controls_;
|
Controls controls_;
|
||||||
|
|
||||||
|
// Velocitat intencionada calculada a `tick` i aplicada a
|
||||||
|
// `applyMovement`. El GameScene pot sobreescriure-la entremig per
|
||||||
|
// resoldre interaccions jugador↔jugador (empentes, head-on).
|
||||||
|
// També actua com a estat físic persistent entre frames: amb inèrcia
|
||||||
|
// activada, si l'usuari solta l'input, aquí queda la velocitat
|
||||||
|
// residual que `tick` anirà decaient linealment cap a 0.
|
||||||
|
float intended_vx_{0.0F};
|
||||||
|
float intended_vy_{0.0F};
|
||||||
|
|
||||||
|
// Velocitat desitjada per input aquest frame (teclat + gamepad). Cada
|
||||||
|
// eix és -SPEED, 0 o +SPEED. És la "intenció del jugador"; la física
|
||||||
|
// resultant viu a `intended_v*` i pot diferir (coast sense input).
|
||||||
|
float input_vx_{0.0F};
|
||||||
|
float input_vy_{0.0F};
|
||||||
|
|
||||||
|
// Acumulador de temps en moviment continu. Creix cada frame amb
|
||||||
|
// `|input_v*| > 0` o `|intended_v*| > 0`, i es reseteja a 0 només
|
||||||
|
// quan NO hi ha ni input ni velocitat residual. Activa la inèrcia
|
||||||
|
// quan passa el llindar configurat al YAML (`threshold_ms`); no es
|
||||||
|
// buida mai enmig d'un coast, ni tan sols en canvis de direcció,
|
||||||
|
// així un toc curt en sentit oposat no "frena en sec" un coast llarg.
|
||||||
|
float motion_time_ms_{0.0F};
|
||||||
|
|
||||||
|
// Punter a la llista de tots els jugadors del nivell (propietat del
|
||||||
|
// GameScene). `nullptr` = jugador aïllat, sense col·lisió inter-P.
|
||||||
|
const std::vector<Player*>* all_players_{nullptr};
|
||||||
|
|
||||||
|
// Frame actual el jugador està sent empentat (no té input propi).
|
||||||
|
// Serveix només per a l'elecció d'animació — la posició ja la
|
||||||
|
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
||||||
|
bool being_pushed_{false};
|
||||||
|
|
||||||
|
// Escala d'empenta usada per `snapToLane` quan descobreix un blocker
|
||||||
|
// (mateixa del resolver). 1.0 = sense escalar (comportament si
|
||||||
|
// GameScene no ha cridat setPushFactor).
|
||||||
|
float push_factor_{1.0F};
|
||||||
// Còpia plana de la geometria del mapa + punters a `collides`/
|
// Còpia plana de la geometria del mapa + punters a `collides`/
|
||||||
// `inside_playfield`. POD trivial; copiada al ctor i no muta durant
|
// `inside_playfield`. POD trivial; copiada al ctor i no muta durant
|
||||||
// la vida del Player. Vol dir que aquest Player només és vàlid per
|
// la vida del Player. Vol dir que aquest Player només és vàlid per
|
||||||
@@ -119,6 +371,180 @@ class Player : public AnimatedSprite {
|
|||||||
DOWN };
|
DOWN };
|
||||||
Facing facing_{Facing::SIDE};
|
Facing facing_{Facing::SIDE};
|
||||||
|
|
||||||
|
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
||||||
|
// El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix
|
||||||
|
// la 4a sense reorganitzar el sistema d'animacions.
|
||||||
|
enum class Cardinal : std::uint8_t { NONE,
|
||||||
|
NORTH,
|
||||||
|
SOUTH,
|
||||||
|
EAST,
|
||||||
|
WEST };
|
||||||
|
|
||||||
|
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
||||||
|
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
||||||
|
// el comportament és instantani: soltar input para el jugador en sec.
|
||||||
|
struct InertiaConfig {
|
||||||
|
int brake_time_ms{0}; // Temps en ms perquè la velocitat caiga de ±SPEED a 0.
|
||||||
|
int threshold_ms{0}; // Temps mínim en moviment continu per activar la inèrcia.
|
||||||
|
};
|
||||||
|
static InertiaConfig inertia_config;
|
||||||
|
|
||||||
|
// Integra un eix. Si hi ha input en aquest eix, la velocitat física
|
||||||
|
// salta a aquest valor (acceleració instantània). Si no, i hi ha
|
||||||
|
// input en l'altre eix (`any_input == true`), la velocitat d'aquest
|
||||||
|
// eix es posa a 0 en sec: el jugador està fent un gir de 90°, no
|
||||||
|
// soltant l'input — no volem que arrossegue en diagonal. Només quan
|
||||||
|
// l'usuari deixa anar tot l'input (`any_input == false`) s'activa la
|
||||||
|
// inèrcia de frenada (si `motion_time_ms` ha passat el llindar).
|
||||||
|
static void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input);
|
||||||
|
|
||||||
|
// Configuració del gir de 180°, llegida del bloc `turn:` del YAML del
|
||||||
|
// player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica
|
||||||
|
// queda desactivada i els canvis de direcció són instantanis.
|
||||||
|
struct TurnConfig {
|
||||||
|
int duration_ms{0};
|
||||||
|
Cardinal w_to_e{Cardinal::SOUTH};
|
||||||
|
Cardinal e_to_w{Cardinal::NORTH};
|
||||||
|
Cardinal n_to_s{Cardinal::EAST};
|
||||||
|
Cardinal s_to_n{Cardinal::WEST};
|
||||||
|
};
|
||||||
|
// Definida out-of-class al .cpp: dins de la classe, els initializers
|
||||||
|
// per defecte de TurnConfig encara no són visibles per al compilador.
|
||||||
|
static TurnConfig turn_config;
|
||||||
|
|
||||||
|
// Configuració del salt llegida del bloc `jump:` del YAML del player.
|
||||||
|
// Si `impulse_px_per_s == 0` (o el bloc no existeix) la mecànica del
|
||||||
|
// salt queda desactivada — el botó no té efecte.
|
||||||
|
struct JumpConfig {
|
||||||
|
float impulse_px_per_s{0.0F}; // Velocitat vertical inicial.
|
||||||
|
float gravity_px_per_s2{0.0F}; // Acceleració descendent constant.
|
||||||
|
int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta).
|
||||||
|
int shadow_y_offset{0}; // Px que puja la sombra (compensa transparències al peu del sprite).
|
||||||
|
std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci).
|
||||||
|
std::string land_sound_key; // Clau del so d'aterratge (buit = silenci).
|
||||||
|
};
|
||||||
|
static JumpConfig jump_config;
|
||||||
|
|
||||||
|
// Llig el bloc `collision:` del YAML i n'omple les caixes (size/base/
|
||||||
|
// hit) + `body_height`. Llança si el bloc no existeix. Membre estàtic
|
||||||
|
// per accedir als estàtics privats sense exposar-los.
|
||||||
|
static void readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path);
|
||||||
|
|
||||||
|
// Llig el bloc `jump:` del YAML i n'omple `jump_config`. Si el bloc
|
||||||
|
// manca, deixa la mecànica del salt desactivada.
|
||||||
|
static void readJumpBlock(const fkyaml::node& yaml);
|
||||||
|
|
||||||
|
// Configuració de l'estat HURT (estat post-impacte). Si
|
||||||
|
// `initial_speed_px_per_s == 0` el jugador entra i surt
|
||||||
|
// instantàniament (cap moviment).
|
||||||
|
struct HurtConfig {
|
||||||
|
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
||||||
|
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
||||||
|
};
|
||||||
|
static HurtConfig hurt_config;
|
||||||
|
|
||||||
|
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
||||||
|
static void readHurtBlock(const fkyaml::node& yaml);
|
||||||
|
|
||||||
|
// Activa l'estat HURT: el jugador queda projectat amb velocitat
|
||||||
|
// inicial `hurt_config.initial_speed_px_per_s`. La direcció es tria
|
||||||
|
// així:
|
||||||
|
// - Si `preferred != NONE` i hi cap el recorregut natural complet
|
||||||
|
// (sense topar amb mur abans d'esgotar la inèrcia), s'usa
|
||||||
|
// `preferred`.
|
||||||
|
// - Altrament (o si `preferred == NONE`), prova els 4 cardinals
|
||||||
|
// i tria el que dóna més recorregut lliure.
|
||||||
|
// Decelera linealment fins a parar i, en eixe instant, torna a
|
||||||
|
// GROUNDED. Sense col·lisió amb altres jugadors mentre dura.
|
||||||
|
void enterHurt(Cardinal preferred = Cardinal::NONE);
|
||||||
|
|
||||||
|
// Tria la direcció definitiva del HURT. Si `preferred` cabe el
|
||||||
|
// recorregut natural complet, la retorna; d'altra manera tria la
|
||||||
|
// direcció amb més recorregut lliure entre els 4 cardinals.
|
||||||
|
[[nodiscard]] auto chooseHurtDirection(Cardinal preferred) const -> Cardinal;
|
||||||
|
|
||||||
|
// Recorregut lliure (en píxels, fins a `max_px`) en la direcció
|
||||||
|
// (dx, dy) abans de topar amb un mur o eixir del playfield. La
|
||||||
|
// base-box es projecta `i` píxels i es comprova; retorna `i-1` al
|
||||||
|
// primer xoc.
|
||||||
|
[[nodiscard]] auto reachInDirection(float dx, float dy, int max_px) const -> int;
|
||||||
|
|
||||||
|
// Vectors unitaris dels 4 cardinals. `NONE` retorna (0, 0).
|
||||||
|
[[nodiscard]] static auto deltasFor(Cardinal c) -> std::pair<float, float>;
|
||||||
|
|
||||||
|
// Estat de la màquina d'estats. Mutat per `tick`/`applyMovement`. Per
|
||||||
|
// defecte GROUNDED al spawn.
|
||||||
|
State state_{State::GROUNDED};
|
||||||
|
|
||||||
|
// Física del salt (només té sentit en AIRBORNE; ambdós a 0 a terra).
|
||||||
|
// jump_z_ → altura visual del jugador per damunt del sòl, en píxels.
|
||||||
|
// jump_vz_ → velocitat vertical actual (positiva = puja, negativa = cau).
|
||||||
|
float jump_z_{0.0F};
|
||||||
|
float jump_vz_{0.0F};
|
||||||
|
|
||||||
|
// Temps restant de stun d'aterratge (ms). Mentre > 0, l'estat és
|
||||||
|
// LANDING_STUN i tot l'input queda bloquejat.
|
||||||
|
float landing_stun_remaining_ms_{0.0F};
|
||||||
|
|
||||||
|
// Estat anterior del botó de salt per fer detecció de flanc (saltem
|
||||||
|
// només a la transició no-premut → premut, no mentre es manté). Es
|
||||||
|
// manté un per teclat i un per mando perquè poden conviure.
|
||||||
|
bool jump_held_keyboard_{false};
|
||||||
|
bool jump_held_gamepad_{false};
|
||||||
|
|
||||||
|
// Llig el botó de salt aquest frame (teclat + mando assignat) i,
|
||||||
|
// amb edge-detection, retorna true si s'ha de despegar el salt.
|
||||||
|
[[nodiscard]] auto consumeJumpEdge() -> bool;
|
||||||
|
|
||||||
|
// Inicia un salt: copia `intended_v*` com a velocitat fixada del salt,
|
||||||
|
// posa `jump_vz_ = impulse`, transiciona a AIRBORNE.
|
||||||
|
void startJump();
|
||||||
|
|
||||||
|
// Integra la física del salt aquest frame i, si toca el sòl, marca el
|
||||||
|
// canvi a LANDING_STUN. Retorna true si aquest frame ha aterrat (perquè
|
||||||
|
// `applyMovement` puga disparar la resolució del solapament PvP).
|
||||||
|
auto integrateJump(float delta_time) -> bool;
|
||||||
|
|
||||||
|
// En aterrar, si la base-box del jugador solapa amb la d'un altre,
|
||||||
|
// l'altre entra en `HURT` i surt projectat per `enterHurt`. El
|
||||||
|
// saltador (this) es queda al lloc — el moviment per separar-los
|
||||||
|
// el fa el HURT en cours, no una empenta forçada.
|
||||||
|
void resolveLandingOverlap();
|
||||||
|
|
||||||
|
// Branques de `applyMovement` extretes per acotar la complexitat
|
||||||
|
// cognitiva. Cada estat especial gestiona el seu propi cicle
|
||||||
|
// (animació, tryMove o no, publicació de tile).
|
||||||
|
void applyHurtMovement(float delta_time);
|
||||||
|
void applyLandingStunMovement(float delta_time);
|
||||||
|
|
||||||
|
// Publica `on_tile_entered` al bus si el tile ha canviat respecte a
|
||||||
|
// l'últim publicat. Comú a totes les branques de `applyMovement` que
|
||||||
|
// pinten el sòl (GROUNDED, HURT, LANDING_STUN; en AIRBORNE no).
|
||||||
|
void publishTileEnteredIfChanged();
|
||||||
|
|
||||||
|
// Estat del gir en curs. Mentre `turning_remaining_ms_ > 0`, el facing/
|
||||||
|
// flip es manté forçat a `intermediate_cardinal_` i l'animació es força
|
||||||
|
// a `stand` encara que el moviment continuï. En esgotar-se el timer,
|
||||||
|
// s'aplica `pending_cardinal_`. `last_input_cardinal_` recorda l'últim
|
||||||
|
// cardinal d'input no nul: serveix per disparar el gir només quan
|
||||||
|
// l'usuari canvia d'opinió, no la primera vegada que prem una tecla
|
||||||
|
// des de l'estat idle.
|
||||||
|
float turning_remaining_ms_{0.0F};
|
||||||
|
Cardinal pending_cardinal_{Cardinal::NONE};
|
||||||
|
Cardinal intermediate_cardinal_{Cardinal::NONE};
|
||||||
|
Cardinal last_input_cardinal_{Cardinal::NONE};
|
||||||
|
|
||||||
|
// Aplica un cardinal a (`facing_`, flip). NONE no fa res — preserva el
|
||||||
|
// facing actual (cas idle: l'última orientació es manté).
|
||||||
|
void applyCardinal(Cardinal c);
|
||||||
|
// Actualitza facing_/flip i l'estat del gir a partir de l'input del frame.
|
||||||
|
// Extret de `update` per acotar la complexitat cognitiva (clang-tidy).
|
||||||
|
void updateFacingAndTurn(float delta_time, float vx, float vy);
|
||||||
|
static auto cardinalFromInput(float vx, float vy) -> Cardinal;
|
||||||
|
static auto cardinalFromString(const std::string& s) -> Cardinal;
|
||||||
|
static auto opposite(Cardinal c) -> Cardinal;
|
||||||
|
static auto intermediateFor(Cardinal target) -> Cardinal;
|
||||||
|
|
||||||
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
||||||
// Player només en guarda un punter per publicar events.
|
// Player només en guarda un punter per publicar events.
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
@@ -127,4 +553,10 @@ class Player : public AnimatedSprite {
|
|||||||
// el primer frame en què el bus estigui connectat ja emeti un event.
|
// el primer frame en què el bus estigui connectat ja emeti un event.
|
||||||
int last_tile_col_{-9999};
|
int last_tile_col_{-9999};
|
||||||
int last_tile_row_{-9999};
|
int last_tile_row_{-9999};
|
||||||
|
|
||||||
|
// Índex de tile del tileset amb què aquest jugador pinta. Llegit del
|
||||||
|
// YAML del mapa (`player1_paint_tile` / `player2_paint_tile`) i passat
|
||||||
|
// al ctor per GameScene. -1 desactiva el pintat (p.ex. al test
|
||||||
|
// d'arrencada o en un escenari que no vulgui feedback visual).
|
||||||
|
int paint_tile_index_{-1};
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
@@ -22,14 +21,15 @@ namespace {
|
|||||||
namespace Map {
|
namespace Map {
|
||||||
|
|
||||||
void loadFrom(const std::string& yaml_path) {
|
void loadFrom(const std::string& yaml_path) {
|
||||||
const auto FULL = Resource::Helper::resolve(yaml_path);
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
std::ifstream file(FULL);
|
if (bytes.empty()) {
|
||||||
if (!file.is_open()) {
|
throw std::runtime_error("Map: no s'ha pogut obrir " + yaml_path);
|
||||||
throw std::runtime_error("Map: no s'ha pogut obrir " + FULL);
|
|
||||||
}
|
}
|
||||||
auto yaml = fkyaml::node::deserialize(file);
|
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("geometry")) {
|
if (!yaml.contains("geometry")) {
|
||||||
throw std::runtime_error("Map: manca bloc `geometry:` a " + FULL);
|
throw std::runtime_error("Map: manca bloc `geometry:` a " + yaml_path);
|
||||||
}
|
}
|
||||||
const auto& g = yaml["geometry"];
|
const auto& g = yaml["geometry"];
|
||||||
|
|
||||||
@@ -75,10 +75,25 @@ namespace Map {
|
|||||||
playfield_w = board_w + (2 * outer_corridor);
|
playfield_w = board_w + (2 * outer_corridor);
|
||||||
playfield_h = board_h + (2 * outer_corridor);
|
playfield_h = board_h + (2 * outer_corridor);
|
||||||
|
|
||||||
|
// Porta i passadís de sortida. `door_col` és la columna central
|
||||||
|
// transitable; el passadís són exactament els 16 px d'eixa columna
|
||||||
|
// dins del strip del mur trasser. Si manca la clau, `door_col=0`
|
||||||
|
// i el passadís queda a x=0 amb mida zero — pràcticament inactiu
|
||||||
|
// (el bloqueig del clamp continua sent el rectangle del playfield).
|
||||||
|
door_col = g.contains("door_col") ? g["door_col"].get_value<int>() : 0;
|
||||||
|
door_corridor_x = (door_col * tile_size) + tilemap_origin_x;
|
||||||
|
door_corridor_y = hud_h;
|
||||||
|
door_corridor_w = tile_size;
|
||||||
|
door_corridor_h = back_wall_h;
|
||||||
|
// Cada càrrega de nivell deixa la porta tancada; l'entitat Door
|
||||||
|
// canviarà el flag a true quan transiti a OPEN.
|
||||||
|
door_open = false;
|
||||||
|
|
||||||
std::cout << "Map: geometria " << grid_cols << "x" << grid_rows
|
std::cout << "Map: geometria " << grid_cols << "x" << grid_rows
|
||||||
<< " sarcs " << sarc_w << "x" << sarc_h
|
<< " sarcs " << sarc_w << "x" << sarc_h
|
||||||
<< " pitch " << pitch_x << "x" << pitch_y
|
<< " pitch " << pitch_x << "x" << pitch_y
|
||||||
<< " des de " << FULL << '\n';
|
<< " door_col " << door_col
|
||||||
|
<< " des de " << yaml_path << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
auto getSarcRect(int col, int row) -> SDL_FRect {
|
auto getSarcRect(int col, int row) -> SDL_FRect {
|
||||||
@@ -99,6 +114,18 @@ namespace Map {
|
|||||||
if (overlapStrict(base_box, getSarcRect(col, row))) { return true; }
|
if (overlapStrict(base_box, getSarcRect(col, row))) { return true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Porta tancada: el passadís de sortida actua com a paret. Quan la
|
||||||
|
// porta passa a OPEN, l'entitat Door posa `door_open = true` i
|
||||||
|
// aquesta clàusula deixa de bloquejar — el jugador pot creuar.
|
||||||
|
if (!door_open && door_corridor_w > 0 && door_corridor_h > 0) {
|
||||||
|
const SDL_FRect CORRIDOR{
|
||||||
|
.x = static_cast<float>(door_corridor_x),
|
||||||
|
.y = static_cast<float>(door_corridor_y),
|
||||||
|
.w = static_cast<float>(door_corridor_w),
|
||||||
|
.h = static_cast<float>(door_corridor_h),
|
||||||
|
};
|
||||||
|
if (overlapStrict(base_box, CORRIDOR)) { return true; }
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +133,23 @@ namespace Map {
|
|||||||
return box.x >= static_cast<float>(playfield_x) && box.y >= static_cast<float>(playfield_y) && box.x + box.w <= static_cast<float>(playfield_x + playfield_w) && box.y + box.h <= static_cast<float>(playfield_y + playfield_h);
|
return box.x >= static_cast<float>(playfield_x) && box.y >= static_cast<float>(playfield_y) && box.x + box.w <= static_cast<float>(playfield_x + playfield_w) && box.y + box.h <= static_cast<float>(playfield_y + playfield_h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto isPaintable(int col, int row) -> bool {
|
||||||
|
// Construïm la caixa píxel del tile a partir de l'origen del tilemap.
|
||||||
|
// El grid és 16-alineat respecte a `tilemap_origin_*`; per a un tile
|
||||||
|
// (col,row) la cantonada superior-esquerra cau a:
|
||||||
|
// x = tilemap_origin_x + col * tile_size
|
||||||
|
// y = tilemap_origin_y + row * tile_size
|
||||||
|
const SDL_FRect BOX{
|
||||||
|
.x = static_cast<float>(tilemap_origin_x + (col * tile_size)),
|
||||||
|
.y = static_cast<float>(tilemap_origin_y + (row * tile_size)),
|
||||||
|
.w = static_cast<float>(tile_size),
|
||||||
|
.h = static_cast<float>(tile_size),
|
||||||
|
};
|
||||||
|
if (!insidePlayfield(BOX)) { return false; }
|
||||||
|
if (collides(BOX)) { return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
auto makeInfo() -> Info {
|
auto makeInfo() -> Info {
|
||||||
return {
|
return {
|
||||||
.playfield_x = playfield_x,
|
.playfield_x = playfield_x,
|
||||||
@@ -119,6 +163,10 @@ namespace Map {
|
|||||||
.tile_size = tile_size,
|
.tile_size = tile_size,
|
||||||
.tilemap_origin_x = tilemap_origin_x,
|
.tilemap_origin_x = tilemap_origin_x,
|
||||||
.tilemap_origin_y = tilemap_origin_y,
|
.tilemap_origin_y = tilemap_origin_y,
|
||||||
|
.door_corridor_x = door_corridor_x,
|
||||||
|
.door_corridor_y = door_corridor_y,
|
||||||
|
.door_corridor_w = door_corridor_w,
|
||||||
|
.door_corridor_h = door_corridor_h,
|
||||||
.collides = &collides,
|
.collides = &collides,
|
||||||
.inside_playfield = &insidePlayfield,
|
.inside_playfield = &insidePlayfield,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,6 +62,24 @@ namespace Map {
|
|||||||
inline int playfield_w = 0;
|
inline int playfield_w = 0;
|
||||||
inline int playfield_h = 0;
|
inline int playfield_h = 0;
|
||||||
|
|
||||||
|
// Porta de sortida del nivell. La porta sempre viu al mur trasser
|
||||||
|
// (`wall_north`); només la columna varia per nivell. `door_col` és la
|
||||||
|
// columna central transitable: el sprite 48×32 es dibuixa centrat
|
||||||
|
// sobre aquesta columna i els 16 px centrals (just `door_col`) són
|
||||||
|
// l'únic forat del mur trasser per on el jugador surt. Derivat de
|
||||||
|
// `geometry.door_col` al YAML.
|
||||||
|
inline int door_col = 0;
|
||||||
|
inline int door_corridor_x = 0;
|
||||||
|
inline int door_corridor_y = 0;
|
||||||
|
inline int door_corridor_w = 0;
|
||||||
|
inline int door_corridor_h = 0;
|
||||||
|
// Estat del passadís: si la porta està oberta, el `door_corridor` no
|
||||||
|
// bloqueja el moviment del jugador (vegeu `Map::collides`). Qui muta
|
||||||
|
// aquest flag és l'entitat `Door` quan transita CLOSED → OPEN; el
|
||||||
|
// reset el porta `Map::loadFrom` (cada càrrega de nivell el torna a
|
||||||
|
// posar a false).
|
||||||
|
inline bool door_open = false;
|
||||||
|
|
||||||
// Carrega el bloc `geometry:` del YAML i recalcula tots els derivats.
|
// Carrega el bloc `geometry:` del YAML i recalcula tots els derivats.
|
||||||
// Ha de cridar-se abans d'instanciar `Player` o consultar `collides()`:
|
// Ha de cridar-se abans d'instanciar `Player` o consultar `collides()`:
|
||||||
// GameScene ho fa al ctor.
|
// GameScene ho fa al ctor.
|
||||||
@@ -81,6 +99,12 @@ namespace Map {
|
|||||||
// entitats dinàmiques per validar posicions candidates del seu step.
|
// entitats dinàmiques per validar posicions candidates del seu step.
|
||||||
[[nodiscard]] auto insidePlayfield(const SDL_FRect& box) -> bool;
|
[[nodiscard]] auto insidePlayfield(const SDL_FRect& box) -> bool;
|
||||||
|
|
||||||
|
// true si el tile (col, row) — coordenades del tilemap, amb origen
|
||||||
|
// `tilemap_origin_*` — és "sòl transitable" i per tant pintable: cau
|
||||||
|
// completament dins del playfield i no s'encavalca cap sarcòfag. Ho
|
||||||
|
// usa el `PaintLayer` per filtrar quines cel·les accepten pintat.
|
||||||
|
[[nodiscard]] auto isPaintable(int col, int row) -> bool;
|
||||||
|
|
||||||
// Snapshot plà de la geometria del mapa + punters a `collides` /
|
// Snapshot plà de la geometria del mapa + punters a `collides` /
|
||||||
// `insidePlayfield`. Pensat per ser passat per valor (POD petit,
|
// `insidePlayfield`. Pensat per ser passat per valor (POD petit,
|
||||||
// trivialment copiable). Un cop construïda, llegeix els valors
|
// trivialment copiable). Un cop construïda, llegeix els valors
|
||||||
@@ -98,6 +122,13 @@ namespace Map {
|
|||||||
int tile_size{0};
|
int tile_size{0};
|
||||||
int tilemap_origin_x{0};
|
int tilemap_origin_x{0};
|
||||||
int tilemap_origin_y{0};
|
int tilemap_origin_y{0};
|
||||||
|
// Passadís de sortida (forat del mur trasser a la columna de la
|
||||||
|
// porta). El Player l'usa des del clamp per permetre pujar al
|
||||||
|
// strip del mur quan la base box queda dins del rang X.
|
||||||
|
int door_corridor_x{0};
|
||||||
|
int door_corridor_y{0};
|
||||||
|
int door_corridor_w{0};
|
||||||
|
int door_corridor_h{0};
|
||||||
bool (*collides)(const SDL_FRect&){nullptr};
|
bool (*collides)(const SDL_FRect&){nullptr};
|
||||||
bool (*inside_playfield)(const SDL_FRect&){nullptr};
|
bool (*inside_playfield)(const SDL_FRect&){nullptr};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#include "game/map/paint_layer.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/surface.hpp"
|
||||||
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "game/map/map.hpp"
|
||||||
|
#include "game/map/tilemap.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Blit d'un tile del tileset sobre un buffer ARGB de destí, respectant
|
||||||
|
// l'índex transparent del GIF. Mateixa convenció que la versió a
|
||||||
|
// `tilemap.cpp`: el tileset guarda índexs + paleta, i la destinació
|
||||||
|
// és un buffer ARGB32 lineal. Duplicat intencionat per no acoblar
|
||||||
|
// Tilemap i PaintLayer; si en el futur un tercer consumidor el vol
|
||||||
|
// reutilitzar, es promou a un helper compartit.
|
||||||
|
void blitTile(const Surface& tileset, int tile_size, int src_col, int src_row, std::vector<std::uint32_t>& dest, int dest_w, int dest_x, int dest_y) {
|
||||||
|
const int SRC_X0 = src_col * tile_size;
|
||||||
|
const int SRC_Y0 = src_row * tile_size;
|
||||||
|
const auto TRANSPARENT = tileset.getTransparentColor();
|
||||||
|
for (int y = 0; y < tile_size; ++y) {
|
||||||
|
for (int x = 0; x < tile_size; ++x) {
|
||||||
|
const auto IDX = tileset.getPixel(SRC_X0 + x, SRC_Y0 + y);
|
||||||
|
if (IDX == TRANSPARENT) { continue; }
|
||||||
|
const auto ARGB = tileset.getPaletteColor(IDX);
|
||||||
|
const auto DST_OFFSET =
|
||||||
|
(static_cast<std::size_t>(dest_y + y) * static_cast<std::size_t>(dest_w)) + static_cast<std::size_t>(dest_x + x);
|
||||||
|
dest[DST_OFFSET] = ARGB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PaintLayer::PaintLayer(const Tilemap& tilemap, EventBus* bus)
|
||||||
|
: bus_(bus),
|
||||||
|
cols_(tilemap.getCols()),
|
||||||
|
rows_(tilemap.getRows()),
|
||||||
|
tile_size_(tilemap.getTileSize()),
|
||||||
|
origin_x_(tilemap.getOriginX()),
|
||||||
|
origin_y_(tilemap.getOriginY()),
|
||||||
|
width_px_(tilemap.getWidth()),
|
||||||
|
height_px_(tilemap.getHeight()),
|
||||||
|
tileset_cols_(tilemap.getTilesetCols()),
|
||||||
|
tileset_path_(tilemap.getTilesetPath()) {
|
||||||
|
painted_.assign(static_cast<std::size_t>(rows_), std::vector<int>(static_cast<std::size_t>(cols_), -1));
|
||||||
|
pixels_.assign(
|
||||||
|
static_cast<std::size_t>(width_px_) * static_cast<std::size_t>(height_px_),
|
||||||
|
0U);
|
||||||
|
|
||||||
|
if (bus_ != nullptr) {
|
||||||
|
// Subscripció a `on_tile_entered`: el Player publica aquest event
|
||||||
|
// amb `PaintedTile{col, row, tile_index}`. Filtrem pintabilitat
|
||||||
|
// (sòl transitable) via `Map::isPaintable` i, si aplica, marquem
|
||||||
|
// i re-publiquem `tile_painted` amb només la posició.
|
||||||
|
on_tile_sub_ = bus_->subscribe("on_tile_entered", [this](const Event& ev) {
|
||||||
|
// Re-propaguem el `player_id` de qui ha trepitjat la cel·la, perquè
|
||||||
|
// `tile_painted` (que consumeixen els sarcòfags i el HUD futur)
|
||||||
|
// sàpiga quin jugador ha pintat.
|
||||||
|
const auto* paint = std::get_if<PaintedTile>(&ev.payload);
|
||||||
|
if (paint != nullptr) { tryPaint(paint->col, paint->row, paint->tile_index, ev.player_id); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "PaintLayer: " << cols_ << "x" << rows_ << " tiles ("
|
||||||
|
<< width_px_ << "x" << height_px_ << " px), tileset "
|
||||||
|
<< tileset_path_ << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
PaintLayer::~PaintLayer() {
|
||||||
|
if (bus_ != nullptr && on_tile_sub_ != 0) { bus_->unsubscribe(on_tile_sub_); }
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PaintLayer::isPainted(int col, int row) const -> bool {
|
||||||
|
if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return false; }
|
||||||
|
return painted_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PaintLayer::clear() {
|
||||||
|
// Mateixa geometria que al ctor: matriu de tiles a -1 (no pintat) i
|
||||||
|
// buffer ARGB a zero (alpha=0 = transparent → render no escriu res).
|
||||||
|
painted_.assign(static_cast<std::size_t>(rows_), std::vector<int>(static_cast<std::size_t>(cols_), -1));
|
||||||
|
std::ranges::fill(pixels_, 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PaintLayer::tryPaint(int col, int row, int tile_index, int player_id) -> bool {
|
||||||
|
if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return false; }
|
||||||
|
if (tile_index < 0) { return false; }
|
||||||
|
if (painted_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] >= 0) {
|
||||||
|
// Primer que pinta guanya: no sobreescrivim.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Map::isPaintable(col, row)) { return false; }
|
||||||
|
|
||||||
|
painted_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] = tile_index;
|
||||||
|
blitPaintTile(col, row, tile_index);
|
||||||
|
|
||||||
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "tile_painted",
|
||||||
|
.payload = TilePos{.col = col, .row = row},
|
||||||
|
.player_id = player_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PaintLayer::blitPaintTile(int col, int row, int tile_index) {
|
||||||
|
const auto TILESET = Resource::Cache::get().getSurface(tileset_path_);
|
||||||
|
if (TILESET == nullptr) { return; }
|
||||||
|
const int DX = col * tile_size_;
|
||||||
|
const int DY = row * tile_size_;
|
||||||
|
const int SRC_COL = tile_index % tileset_cols_;
|
||||||
|
const int SRC_ROW = tile_index / tileset_cols_;
|
||||||
|
blitTile(*TILESET, tile_size_, SRC_COL, SRC_ROW, pixels_, width_px_, DX, DY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PaintLayer::render() const {
|
||||||
|
// Còpia ARGB al canvas, anàloga a `Tilemap::render`. Píxels amb
|
||||||
|
// alpha=0 (tiles no pintats) es salten per deixar visible el tilemap
|
||||||
|
// base per sota.
|
||||||
|
auto& screen = Screen::get();
|
||||||
|
auto* canvas = screen.getCanvasArgb();
|
||||||
|
const int CANVAS_W = screen.getCanvasWidth();
|
||||||
|
const int CANVAS_H = screen.getCanvasHeight();
|
||||||
|
|
||||||
|
const int Y_START = std::max(0, -origin_y_);
|
||||||
|
const int Y_END = std::min(height_px_, CANVAS_H - origin_y_);
|
||||||
|
const int X_START = std::max(0, -origin_x_);
|
||||||
|
const int X_END = std::min(width_px_, CANVAS_W - origin_x_);
|
||||||
|
|
||||||
|
constexpr std::uint32_t ALPHA_MASK = 0xFF000000U;
|
||||||
|
for (int y = Y_START; y < Y_END; ++y) {
|
||||||
|
const auto* src_row = &pixels_[static_cast<std::size_t>(y) * static_cast<std::size_t>(width_px_)];
|
||||||
|
auto* dst_row =
|
||||||
|
&canvas[(static_cast<std::size_t>(origin_y_ + y) * static_cast<std::size_t>(CANVAS_W)) + static_cast<std::size_t>(origin_x_)];
|
||||||
|
for (int x = X_START; x < X_END; ++x) {
|
||||||
|
const auto PIXEL = src_row[x];
|
||||||
|
if ((PIXEL & ALPHA_MASK) != 0U) { dst_row[x] = PIXEL; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/entities/event_bus.hpp"
|
||||||
|
|
||||||
|
class Tilemap;
|
||||||
|
|
||||||
|
// Capa visual de "pintat" sobre el tilemap base. Quan el jugador (o
|
||||||
|
// qualsevol altre actor futur) publica `on_tile_entered` amb payload
|
||||||
|
// `PaintedTile{col, row, tile_index}`, el PaintLayer filtra per
|
||||||
|
// `Map::isPaintable`, marca el tile i re-publica `tile_painted` amb
|
||||||
|
// només `TilePos` — abstracció del "el tile s'ha pintat" independent de
|
||||||
|
// qui ho ha fet. Els sarcòfags escolten aquest segon event per comptar
|
||||||
|
// el seu perímetre.
|
||||||
|
//
|
||||||
|
// Font única de veritat del pintat: evita duplicar l'estat dins de cada
|
||||||
|
// sarcòfag i permet que el render mostri l'efecte al instant.
|
||||||
|
//
|
||||||
|
// Geometria: hereta `tile_size`, origen i dimensions del Tilemap
|
||||||
|
// (mateixa graella). Reutilitza el tileset del Tilemap per blitejar els
|
||||||
|
// tiles pintats a un buffer ARGB propi, anàleg a `Tilemap::pixels_`.
|
||||||
|
//
|
||||||
|
// Política de repintat: el primer que pinta un tile guanya. Intents
|
||||||
|
// posteriors (amb el mateix índex o un altre) no tenen efecte —
|
||||||
|
// garanteix coherència visual i evita notificar dos cops al mateix sarc.
|
||||||
|
class PaintLayer {
|
||||||
|
public:
|
||||||
|
PaintLayer(const Tilemap& tilemap, EventBus* bus);
|
||||||
|
~PaintLayer();
|
||||||
|
|
||||||
|
PaintLayer(const PaintLayer&) = delete;
|
||||||
|
PaintLayer(PaintLayer&&) = delete;
|
||||||
|
auto operator=(const PaintLayer&) -> PaintLayer& = delete;
|
||||||
|
auto operator=(PaintLayer&&) -> PaintLayer& = delete;
|
||||||
|
|
||||||
|
// Còpia del buffer ARGB sobre el canvas del Screen, respectant alpha
|
||||||
|
// (píxels no pintats queden al buffer amb alpha=0 i no s'escriuen).
|
||||||
|
// Pensat per cridar entre `Tilemap::render()` i el bloc d'actors.
|
||||||
|
void render() const;
|
||||||
|
|
||||||
|
// Intent de pintat directe (exposat per a tests i actors que pinten
|
||||||
|
// sense passar pel bus). Retorna true si el tile és pintable i no
|
||||||
|
// estava ja pintat; en aquest cas, publica `tile_painted`.
|
||||||
|
auto tryPaint(int col, int row, int tile_index, int player_id = -1) -> bool;
|
||||||
|
|
||||||
|
[[nodiscard]] auto isPainted(int col, int row) const -> bool;
|
||||||
|
|
||||||
|
// Buida el pintat acumulat. Marca tots els tiles com a no pintats i
|
||||||
|
// reseteja el buffer ARGB a alpha=0 (transparent). Cridat per
|
||||||
|
// GameScene a la transició ROOM_OUTRO → ROOM_INTRO. La subscripció
|
||||||
|
// al bus es manté — el PaintLayer continuarà reaccionant a
|
||||||
|
// `on_tile_entered` un cop els jugadors hagin reaparegut.
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void blitPaintTile(int col, int row, int tile_index);
|
||||||
|
|
||||||
|
EventBus* bus_{nullptr};
|
||||||
|
EventBus::SubId on_tile_sub_{0};
|
||||||
|
|
||||||
|
int cols_{0};
|
||||||
|
int rows_{0};
|
||||||
|
int tile_size_{0};
|
||||||
|
int origin_x_{0};
|
||||||
|
int origin_y_{0};
|
||||||
|
int width_px_{0};
|
||||||
|
int height_px_{0};
|
||||||
|
int tileset_cols_{0};
|
||||||
|
std::string tileset_path_;
|
||||||
|
|
||||||
|
// `-1` = no pintat. Altres valors = índex de tile del tileset usat
|
||||||
|
// per pintar. No cal per al render (el buffer `pixels_` ja el conté),
|
||||||
|
// però facilita consultes ràpides d'estat i futures extensions.
|
||||||
|
std::vector<std::vector<int>> painted_;
|
||||||
|
std::vector<std::uint32_t> pixels_;
|
||||||
|
};
|
||||||
@@ -2,12 +2,10 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <fstream>
|
#include <cstdint>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <unordered_map>
|
#include <vector>
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
@@ -15,37 +13,12 @@
|
|||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#include "game/map/tilemap_writer.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Cada símbol del grid es mapeja a (col, fila) dins del tileset.
|
|
||||||
using TileCoord = std::pair<int, int>;
|
|
||||||
using Legend = std::unordered_map<char, TileCoord>;
|
|
||||||
|
|
||||||
auto splitLines(const std::string& text) -> std::vector<std::string> {
|
|
||||||
std::vector<std::string> lines;
|
|
||||||
std::istringstream iss(text);
|
|
||||||
std::string line;
|
|
||||||
while (std::getline(iss, line)) {
|
|
||||||
if (!line.empty()) { lines.push_back(line); }
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
// La llegenda usa índexs lineals del tileset (esq→dre, dalt→baix).
|
|
||||||
// Els convertim a (col, fila) amb el nombre de tiles per fila del tileset.
|
|
||||||
auto parseLegend(const fkyaml::node& node, int tileset_cols) -> Legend {
|
|
||||||
Legend legend;
|
|
||||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
|
||||||
const auto KEY_STR = it.key().get_value<std::string>();
|
|
||||||
if (KEY_STR.empty()) { continue; }
|
|
||||||
const auto INDEX = it.value().get_value<int>();
|
|
||||||
const int COL = INDEX % tileset_cols;
|
|
||||||
const int ROW = INDEX / tileset_cols;
|
|
||||||
legend[KEY_STR.front()] = {COL, ROW};
|
|
||||||
}
|
|
||||||
return legend;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copia un tile (col, fila) del tileset al buffer ARGB a la posició de
|
// Copia un tile (col, fila) del tileset al buffer ARGB a la posició de
|
||||||
// destí (dest_x, dest_y). Els píxels amb l'índex de transparència del
|
// destí (dest_x, dest_y). Els píxels amb l'índex de transparència del
|
||||||
// tileset es deixen com estan al buffer (alpha=0 per defecte).
|
// tileset es deixen com estan al buffer (alpha=0 per defecte).
|
||||||
@@ -68,16 +41,16 @@ namespace {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Tilemap::Tilemap(const std::string& yaml_path) {
|
Tilemap::Tilemap(const std::string& yaml_path) {
|
||||||
const auto FULL = Resource::Helper::resolve(yaml_path);
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
std::ifstream file(FULL);
|
if (bytes.empty()) {
|
||||||
if (!file.is_open()) {
|
throw std::runtime_error("Tilemap: no s'ha pogut obrir " + yaml_path);
|
||||||
throw std::runtime_error("Tilemap: no s'ha pogut obrir " + FULL);
|
|
||||||
}
|
}
|
||||||
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
|
|
||||||
auto yaml = fkyaml::node::deserialize(file);
|
tileset_path_ = yaml["tileset"].get_value<std::string>();
|
||||||
|
tile_size_ = yaml["tile_size"].get_value<int>();
|
||||||
const auto TILESET_PATH = yaml["tileset"].get_value<std::string>();
|
|
||||||
const auto TILE_SIZE = yaml["tile_size"].get_value<int>();
|
|
||||||
if (yaml.contains("origin")) {
|
if (yaml.contains("origin")) {
|
||||||
origin_x_ = yaml["origin"][0].get_value<int>();
|
origin_x_ = yaml["origin"][0].get_value<int>();
|
||||||
origin_y_ = yaml["origin"][1].get_value<int>();
|
origin_y_ = yaml["origin"][1].get_value<int>();
|
||||||
@@ -85,37 +58,101 @@ Tilemap::Tilemap(const std::string& yaml_path) {
|
|||||||
|
|
||||||
// Carreguem el tileset al cache (idempotent) i el pillem per accedir
|
// Carreguem el tileset al cache (idempotent) i el pillem per accedir
|
||||||
// als seus píxels i paleta. La paleta del tileset ve del GIF mateix.
|
// als seus píxels i paleta. La paleta del tileset ve del GIF mateix.
|
||||||
// Ens cal abans de la llegenda per saber quants tiles per fila té.
|
// Ens cal abans de llegir `data` per saber quants tiles per fila té.
|
||||||
const auto& cache_name = TILESET_PATH;
|
Resource::Cache::get().loadSurface(tileset_path_, tileset_path_);
|
||||||
Resource::Cache::get().loadSurface(cache_name, TILESET_PATH);
|
const auto TILESET = Resource::Cache::get().getSurface(tileset_path_);
|
||||||
const auto TILESET = Resource::Cache::get().getSurface(cache_name);
|
tileset_cols_ = TILESET->getWidth() / tile_size_;
|
||||||
const int TILESET_COLS = TILESET->getWidth() / TILE_SIZE;
|
|
||||||
|
|
||||||
const auto LEGEND = parseLegend(yaml["legend"], TILESET_COLS);
|
// `data` és una seqüència de seqüències d'enters: cada fila és una
|
||||||
const auto LINES = splitLines(yaml["data"].get_value<std::string>());
|
// llista d'índexs lineals al tileset (esq→dre, dalt→baix). Un valor
|
||||||
|
// negatiu (per convenció -1) deixa la cel·la buida (alpha=0).
|
||||||
const int ROWS = static_cast<int>(LINES.size());
|
const auto& data = yaml["data"];
|
||||||
const int COLS = ROWS > 0 ? static_cast<int>(LINES.front().size()) : 0;
|
if (!data.is_sequence() || data.empty()) {
|
||||||
width_px_ = COLS * TILE_SIZE;
|
throw std::runtime_error("Tilemap: `data` buit o amb format incorrecte a " + yaml_path);
|
||||||
height_px_ = ROWS * TILE_SIZE;
|
}
|
||||||
|
rows_ = static_cast<int>(data.size());
|
||||||
|
cols_ = static_cast<int>(data[0].size());
|
||||||
|
width_px_ = cols_ * tile_size_;
|
||||||
|
height_px_ = rows_ * tile_size_;
|
||||||
pixels_.assign(
|
pixels_.assign(
|
||||||
static_cast<std::size_t>(width_px_) * static_cast<std::size_t>(height_px_),
|
static_cast<std::size_t>(width_px_) * static_cast<std::size_t>(height_px_),
|
||||||
0U);
|
0U);
|
||||||
|
|
||||||
for (int row = 0; row < ROWS; ++row) {
|
#ifdef _DEBUG
|
||||||
const auto& line = LINES[row];
|
data_.assign(static_cast<std::size_t>(rows_), std::vector<int>(static_cast<std::size_t>(cols_), -1));
|
||||||
for (int col = 0; col < COLS && col < static_cast<int>(line.size()); ++col) {
|
#endif
|
||||||
const char SYMBOL = line[col];
|
|
||||||
const auto IT = LEGEND.find(SYMBOL);
|
for (int row = 0; row < rows_; ++row) {
|
||||||
if (IT == LEGEND.end()) { continue; }
|
const auto& line = data[row];
|
||||||
blitTile(*TILESET, TILE_SIZE, IT->second.first, IT->second.second, pixels_, width_px_, col * TILE_SIZE, row * TILE_SIZE);
|
const int LINE_COLS = static_cast<int>(line.size());
|
||||||
|
for (int col = 0; col < cols_ && col < LINE_COLS; ++col) {
|
||||||
|
const int INDEX = line[col].get_value<int>();
|
||||||
|
#ifdef _DEBUG
|
||||||
|
data_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] = INDEX;
|
||||||
|
#endif
|
||||||
|
if (INDEX < 0) { continue; }
|
||||||
|
const int SRC_COL = INDEX % tileset_cols_;
|
||||||
|
const int SRC_ROW = INDEX / tileset_cols_;
|
||||||
|
blitTile(*TILESET, tile_size_, SRC_COL, SRC_ROW, pixels_, width_px_, col * tile_size_, row * tile_size_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Tilemap: " << COLS << "x" << ROWS << " tiles ("
|
std::cout << "Tilemap: " << cols_ << "x" << rows_ << " tiles ("
|
||||||
<< width_px_ << "x" << height_px_ << " px) des de " << FULL << '\n';
|
<< width_px_ << "x" << height_px_ << " px) des de " << yaml_path << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
void Tilemap::setTileAt(int col, int row, int index) {
|
||||||
|
if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return; }
|
||||||
|
if (index < 0) { return; } // El mapa no admet cel·les buides (política de projecte).
|
||||||
|
// No validem l'índex contra el total de tiles del tileset: la
|
||||||
|
// invariant la garanteix el TilePicker, que només ofereix índexs en
|
||||||
|
// rang.
|
||||||
|
|
||||||
|
data_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] = index;
|
||||||
|
|
||||||
|
// Netegem el rectangle del tile al buffer de píxels i re-blitegem.
|
||||||
|
const int DX = col * tile_size_;
|
||||||
|
const int DY = row * tile_size_;
|
||||||
|
for (int y = 0; y < tile_size_; ++y) {
|
||||||
|
auto* dst = &pixels_[(static_cast<std::size_t>(DY + y) * static_cast<std::size_t>(width_px_)) + static_cast<std::size_t>(DX)];
|
||||||
|
for (int x = 0; x < tile_size_; ++x) { dst[x] = 0U; }
|
||||||
|
}
|
||||||
|
const auto TILESET = Resource::Cache::get().getSurface(tileset_path_);
|
||||||
|
const int SRC_COL = index % tileset_cols_;
|
||||||
|
const int SRC_ROW = index / tileset_cols_;
|
||||||
|
blitTile(*TILESET, tile_size_, SRC_COL, SRC_ROW, pixels_, width_px_, DX, DY);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Tilemap::save(const std::string& yaml_path) const -> bool {
|
||||||
|
return TilemapWriter::saveDataBlock(yaml_path, data_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tilemap::renderTilePreview(int canvas_x, int canvas_y, int tile_index) const {
|
||||||
|
if (tile_index < 0) { return; }
|
||||||
|
const auto TILESET = Resource::Cache::get().getSurface(tileset_path_);
|
||||||
|
if (TILESET == nullptr) { return; }
|
||||||
|
auto& screen = Screen::get();
|
||||||
|
auto* buf = screen.getCanvasArgb();
|
||||||
|
const int CW = screen.getCanvasWidth();
|
||||||
|
const int CH = screen.getCanvasHeight();
|
||||||
|
const auto TRANSPARENT = TILESET->getTransparentColor();
|
||||||
|
const int SX0 = (tile_index % tileset_cols_) * tile_size_;
|
||||||
|
const int SY0 = (tile_index / tileset_cols_) * tile_size_;
|
||||||
|
for (int y = 0; y < tile_size_; ++y) {
|
||||||
|
const int DY = canvas_y + y;
|
||||||
|
if (DY < 0 || DY >= CH) { continue; }
|
||||||
|
for (int x = 0; x < tile_size_; ++x) {
|
||||||
|
const int DX = canvas_x + x;
|
||||||
|
if (DX < 0 || DX >= CW) { continue; }
|
||||||
|
const auto IDX = TILESET->getPixel(SX0 + x, SY0 + y);
|
||||||
|
if (IDX == TRANSPARENT) { continue; }
|
||||||
|
buf[(static_cast<std::size_t>(DY) * static_cast<std::size_t>(CW)) + static_cast<std::size_t>(DX)] = TILESET->getPaletteColor(IDX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Tilemap::render() const {
|
void Tilemap::render() const {
|
||||||
auto& screen = Screen::get();
|
auto& screen = Screen::get();
|
||||||
auto* canvas = screen.getCanvasArgb();
|
auto* canvas = screen.getCanvasArgb();
|
||||||
|
|||||||
@@ -27,10 +27,55 @@ class Tilemap {
|
|||||||
[[nodiscard]] auto getOriginX() const -> int { return origin_x_; }
|
[[nodiscard]] auto getOriginX() const -> int { return origin_x_; }
|
||||||
[[nodiscard]] auto getOriginY() const -> int { return origin_y_; }
|
[[nodiscard]] auto getOriginY() const -> int { return origin_y_; }
|
||||||
|
|
||||||
|
// Metadades per a consumidors que necessiten conèixer la geometria
|
||||||
|
// lògica del tilemap (p.ex. l'editor per a la conversió px→cel·la).
|
||||||
|
// Sempre disponibles (en Debug i Release) perquè són barates i no
|
||||||
|
// lliguen amb cap codi específic de l'editor.
|
||||||
|
[[nodiscard]] auto getTileSize() const -> int { return tile_size_; }
|
||||||
|
[[nodiscard]] auto getCols() const -> int { return cols_; }
|
||||||
|
[[nodiscard]] auto getRows() const -> int { return rows_; }
|
||||||
|
[[nodiscard]] auto getTilesetPath() const -> const std::string& { return tileset_path_; }
|
||||||
|
[[nodiscard]] auto getTilesetCols() const -> int { return tileset_cols_; }
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
// API d'edició (només Debug). L'editor és absent en Release, així que
|
||||||
|
// aquestes operacions no formen part de l'API pública del joc final.
|
||||||
|
//
|
||||||
|
// `setTileAt` valida els límits i reemplaça el tile de (col,row) per
|
||||||
|
// `index`. Internament neteja el rectangle `tile_size×tile_size` del
|
||||||
|
// buffer de píxels i re-blita el tile nou; el render al pròxim frame
|
||||||
|
// ja reflectirà el canvi.
|
||||||
|
//
|
||||||
|
// `save` persisteix `data_` al YAML indicat via `TilemapWriter`, que
|
||||||
|
// només toca el bloc `data:` i preserva la resta byte a byte.
|
||||||
|
void setTileAt(int col, int row, int index);
|
||||||
|
[[nodiscard]] auto save(const std::string& yaml_path) const -> bool;
|
||||||
|
|
||||||
|
// Pinta un únic tile del tileset sobre el canvas del Screen a la
|
||||||
|
// posició (canvas_x, canvas_y). Els píxels transparents del tileset
|
||||||
|
// es respecten (deixen visible el que hi ha sota). Usat per l'editor
|
||||||
|
// com a preview del brush sota el cursor, sense tocar `pixels_` ni
|
||||||
|
// `data_` fins que l'usuari fa click.
|
||||||
|
void renderTilePreview(int canvas_x, int canvas_y, int tile_index) const;
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int width_px_{0};
|
int width_px_{0};
|
||||||
int height_px_{0};
|
int height_px_{0};
|
||||||
int origin_x_{0};
|
int origin_x_{0};
|
||||||
int origin_y_{0};
|
int origin_y_{0};
|
||||||
|
int tile_size_{0};
|
||||||
|
int cols_{0};
|
||||||
|
int rows_{0};
|
||||||
|
int tileset_cols_{0};
|
||||||
|
std::string tileset_path_;
|
||||||
std::vector<std::uint32_t> pixels_;
|
std::vector<std::uint32_t> pixels_;
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
// Graella en memòria només necessària per a l'editor: guardar-la en
|
||||||
|
// Release gastaria ~1 KB sense retorn, perquè el `pixels_` ja té tot
|
||||||
|
// el que cal per renderitzar. En Debug la usem com a font de veritat
|
||||||
|
// de l'edició i com a payload de `save()`.
|
||||||
|
std::vector<std::vector<int>> data_;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include "game/map/tilemap_writer.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
namespace TilemapWriter {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Retorna la posició de l'inici de la línia que conté "data:" a
|
||||||
|
// columna 0. La detecció requereix que els 5 caràcters siguin al
|
||||||
|
// principi d'una línia i que el següent siga un terminador típic
|
||||||
|
// (fi de línia, espai, tabulador o comentari). Així evitem trigger
|
||||||
|
// amb claus com "data_version:" o valors escalars que comencin
|
||||||
|
// amb "data:".
|
||||||
|
auto findDataLine(const std::string& content) -> std::size_t {
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (pos < content.size()) {
|
||||||
|
const bool AT_LINE_START = (pos == 0 || content[pos - 1] == '\n');
|
||||||
|
if (AT_LINE_START && content.compare(pos, 5, "data:") == 0) {
|
||||||
|
const std::size_t AFTER = pos + 5;
|
||||||
|
if (AFTER >= content.size() || content[AFTER] == '\n' || content[AFTER] == '\r' || content[AFTER] == ' ' || content[AFTER] == '\t' || content[AFTER] == '#') {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto NL = content.find('\n', pos);
|
||||||
|
if (NL == std::string::npos) { break; }
|
||||||
|
pos = NL + 1;
|
||||||
|
}
|
||||||
|
return std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avança mentre trobe línies indentades (espai/tab). S'atura al
|
||||||
|
// primer caràcter a columna 0 (nova clau top-level) o davant una
|
||||||
|
// línia buida (separadors de seccions). Així els comentaris i el
|
||||||
|
// blanc que precedeixen a la pròxima secció queden fora del bloc.
|
||||||
|
auto advancePastIndented(const std::string& content, std::size_t pos) -> std::size_t {
|
||||||
|
while (pos < content.size()) {
|
||||||
|
const char C = content[pos];
|
||||||
|
if (C != ' ' && C != '\t') { break; }
|
||||||
|
const auto NL = content.find('\n', pos);
|
||||||
|
pos = (NL == std::string::npos) ? content.size() : NL + 1;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto buildBlock(const std::vector<std::vector<int>>& data) -> std::string {
|
||||||
|
int max_width = 1;
|
||||||
|
for (const auto& row : data) {
|
||||||
|
for (const int V : row) {
|
||||||
|
const int W = static_cast<int>(std::to_string(V).size());
|
||||||
|
max_width = std::max(max_width, W);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out;
|
||||||
|
for (const auto& row : data) {
|
||||||
|
out += " - [";
|
||||||
|
for (std::size_t i = 0; i < row.size(); ++i) {
|
||||||
|
if (i > 0) { out += ","; }
|
||||||
|
const auto S = std::to_string(row[i]);
|
||||||
|
const int PAD = max_width - static_cast<int>(S.size());
|
||||||
|
out.append(static_cast<std::size_t>(PAD), ' ');
|
||||||
|
out += S;
|
||||||
|
}
|
||||||
|
out += "]\n";
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto saveDataBlock(const std::string& path, const std::vector<std::vector<int>>& data) -> bool {
|
||||||
|
std::ifstream in(path, std::ios::binary);
|
||||||
|
if (!in) { return false; }
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << in.rdbuf();
|
||||||
|
const std::string CONTENT = ss.str();
|
||||||
|
in.close();
|
||||||
|
|
||||||
|
const auto DATA_POS = findDataLine(CONTENT);
|
||||||
|
if (DATA_POS == std::string::npos) { return false; }
|
||||||
|
|
||||||
|
const auto LINE_END = CONTENT.find('\n', DATA_POS);
|
||||||
|
const auto AFTER_DATA = (LINE_END == std::string::npos) ? CONTENT.size() : LINE_END + 1;
|
||||||
|
const auto BLOCK_END = advancePastIndented(CONTENT, AFTER_DATA);
|
||||||
|
|
||||||
|
const std::string NEW_CONTENT =
|
||||||
|
CONTENT.substr(0, AFTER_DATA) + buildBlock(data) + CONTENT.substr(BLOCK_END);
|
||||||
|
|
||||||
|
const auto TMP_PATH = path + ".tmp";
|
||||||
|
{
|
||||||
|
std::ofstream out(TMP_PATH, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!out) { return false; }
|
||||||
|
out.write(NEW_CONTENT.data(), static_cast<std::streamsize>(NEW_CONTENT.size()));
|
||||||
|
out.close();
|
||||||
|
if (!out) {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove(TMP_PATH, ec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::rename(TMP_PATH, path, ec);
|
||||||
|
if (ec) {
|
||||||
|
std::filesystem::remove(TMP_PATH, ec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TilemapWriter
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Reescriu NOMÉS el bloc `data:` d'un YAML del tilemap, preservant byte a
|
||||||
|
// byte la resta del fitxer: comentaris, ordre de claus, format de
|
||||||
|
// `geometry:` i `entities:`, encoding i line endings entre línies no
|
||||||
|
// tocades. Pensat exclusivament per a l'editor (Debug): només la graella
|
||||||
|
// canvia durant l'edició; el preàmbul i l'epíleg mai.
|
||||||
|
//
|
||||||
|
// Semàntica del bloc:
|
||||||
|
// - Línia de capçalera: primera aparició de "data:" a columna 0.
|
||||||
|
// - Contingut: línies següents indentades (espai o tab). Una línia buida
|
||||||
|
// o una línia a columna 0 marquen la fi del bloc.
|
||||||
|
//
|
||||||
|
// Format de sortida:
|
||||||
|
// - Cada fila en flow style: ` - [v0, v1, v2, ...]`, valors separats
|
||||||
|
// per `,` i alineats a dreta dins de `max_width` caràcters (calculat
|
||||||
|
// a partir del valor més llarg de la graella), cadascun precedit
|
||||||
|
// d'espais de farciment per mantenir columnes visuals.
|
||||||
|
// - Line endings: '\n' al bloc generat (el preàmbul i l'epíleg es
|
||||||
|
// copien byte a byte, així que no els toquem).
|
||||||
|
//
|
||||||
|
// Escriptura atòmica: via `<path>.tmp` + `std::filesystem::rename`, així
|
||||||
|
// un crash a mitja escriptura mai deixa el YAML corrupte.
|
||||||
|
//
|
||||||
|
// Retorna false si el fitxer no es pot obrir, no conté "data:" a columna
|
||||||
|
// 0, o l'escriptura/renombrament atòmic falla.
|
||||||
|
namespace TilemapWriter {
|
||||||
|
|
||||||
|
auto saveDataBlock(const std::string& path, const std::vector<std::vector<int>>& data) -> bool;
|
||||||
|
|
||||||
|
} // namespace TilemapWriter
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ namespace {
|
|||||||
Options::Window g_window{};
|
Options::Window g_window{};
|
||||||
Options::Video g_video{};
|
Options::Video g_video{};
|
||||||
Options::Audio g_audio{};
|
Options::Audio g_audio{};
|
||||||
|
Options::Controls g_controls{};
|
||||||
std::vector<Options::PostFXPreset> g_postfx_presets{};
|
std::vector<Options::PostFXPreset> g_postfx_presets{};
|
||||||
std::vector<Options::CrtPiPreset> g_crtpi_presets{};
|
std::vector<Options::CrtPiPreset> g_crtpi_presets{};
|
||||||
|
|
||||||
@@ -160,6 +162,42 @@ namespace {
|
|||||||
readField(node, "volume", g_audio.sound.volume);
|
readField(node, "volume", g_audio.sound.volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void parseGamepadAssignment(const fkyaml::node& parent, const char* key, Options::GamepadAssignment& dst) {
|
||||||
|
if (!parent.contains(key)) { return; }
|
||||||
|
const auto& node = parent[key];
|
||||||
|
readField(node, "name", dst.name);
|
||||||
|
readField(node, "path", dst.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseControls(const fkyaml::node& yaml) {
|
||||||
|
if (!yaml.contains("controls")) { return; }
|
||||||
|
const auto& node = yaml["controls"];
|
||||||
|
parseGamepadAssignment(node, "gamepad_p1", g_controls.gamepad_p1);
|
||||||
|
parseGamepadAssignment(node, "gamepad_p2", g_controls.gamepad_p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica les assignacions persistides al GamepadManager. Ordre garantit
|
||||||
|
// pel Director: GamepadManager::init (obre tots els mandos físics) →
|
||||||
|
// Options::loadFromFile (llig YAML) → aquesta funció (casa
|
||||||
|
// mandos↔jugadors) → auto-assign per ordre els slots que hagen quedat
|
||||||
|
// buits (equivalent al patró de `coffee_crisis_arcade_edition`).
|
||||||
|
void applyControlsToGamepadManager() {
|
||||||
|
auto& gm = GamepadManager::get();
|
||||||
|
gm.assignToPlayer(0, g_controls.gamepad_p1.name, g_controls.gamepad_p1.path);
|
||||||
|
gm.assignToPlayer(1, g_controls.gamepad_p2.name, g_controls.gamepad_p2.path);
|
||||||
|
const auto AUTO = gm.autoAssignFreeSlots();
|
||||||
|
if (AUTO.empty()) { return; }
|
||||||
|
// Sincronitzem Options amb el que el manager ha vinculat per
|
||||||
|
// ordre, per persistir-ho al config.yaml. No cridem els setters
|
||||||
|
// individuals perquè cada un dispararia un saveToFile; preferim
|
||||||
|
// fer un sol save al final.
|
||||||
|
for (const auto& a : AUTO) {
|
||||||
|
auto& slot = (a.player_id == 0) ? g_controls.gamepad_p1 : g_controls.gamepad_p2;
|
||||||
|
slot.name = a.name;
|
||||||
|
slot.path = a.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void parseAudio(const fkyaml::node& yaml) {
|
void parseAudio(const fkyaml::node& yaml) {
|
||||||
if (!yaml.contains("audio")) { return; }
|
if (!yaml.contains("audio")) { return; }
|
||||||
const auto& node = yaml["audio"];
|
const auto& node = yaml["audio"];
|
||||||
@@ -442,6 +480,7 @@ namespace Options {
|
|||||||
auto window() -> const Window& { return g_window; }
|
auto window() -> const Window& { return g_window; }
|
||||||
auto video() -> const Video& { return g_video; }
|
auto video() -> const Video& { return g_video; }
|
||||||
auto audio() -> const Audio& { return g_audio; }
|
auto audio() -> const Audio& { return g_audio; }
|
||||||
|
auto controls() -> const Controls& { return g_controls; }
|
||||||
auto locale() -> const std::string& { return g_locale; }
|
auto locale() -> const std::string& { return g_locale; }
|
||||||
auto configVersion() -> const std::string& { return g_config_version; }
|
auto configVersion() -> const std::string& { return g_config_version; }
|
||||||
auto postfxPresets() -> const std::vector<PostFXPreset>& { return g_postfx_presets; }
|
auto postfxPresets() -> const std::vector<PostFXPreset>& { return g_postfx_presets; }
|
||||||
@@ -513,6 +552,15 @@ namespace Options {
|
|||||||
saveToFile();
|
saveToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setGamepadAssignment(int player_id, const std::string& name, const std::string& path) {
|
||||||
|
if (player_id < 0 || player_id > 1) { return; }
|
||||||
|
auto& slot = (player_id == 0) ? g_controls.gamepad_p1 : g_controls.gamepad_p2;
|
||||||
|
slot.name = name;
|
||||||
|
slot.path = path;
|
||||||
|
GamepadManager::get().assignToPlayer(player_id, name, path);
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
void setConfigFile(const std::string& path) {
|
void setConfigFile(const std::string& path) {
|
||||||
g_config_file_path = path;
|
g_config_file_path = path;
|
||||||
}
|
}
|
||||||
@@ -522,7 +570,11 @@ namespace Options {
|
|||||||
|
|
||||||
const std::string CONTENT = readFileOrEmpty(g_config_file_path);
|
const std::string CONTENT = readFileOrEmpty(g_config_file_path);
|
||||||
if (CONTENT.empty()) {
|
if (CONTENT.empty()) {
|
||||||
// Primera execució: creem el fitxer amb els valors per defecte.
|
// Primera execució: fem l'auto-assign inicial dels mandos
|
||||||
|
// (per a que P1 obtingui el primer mando connectat sense que
|
||||||
|
// l'usuari haja de passar pel menú) i després escrivim el
|
||||||
|
// fitxer amb els valors resultants.
|
||||||
|
applyControlsToGamepadManager();
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,6 +600,17 @@ namespace Options {
|
|||||||
parseWindow(yaml);
|
parseWindow(yaml);
|
||||||
parseVideo(yaml);
|
parseVideo(yaml);
|
||||||
parseAudio(yaml);
|
parseAudio(yaml);
|
||||||
|
parseControls(yaml);
|
||||||
|
// Captura les assignacions inicials per detectar si
|
||||||
|
// l'auto-assign (dins d'`applyControls...`) les ha tocat i
|
||||||
|
// cal reescriure el fitxer amb els valors nous.
|
||||||
|
const auto BEFORE_P1 = g_controls.gamepad_p1;
|
||||||
|
const auto BEFORE_P2 = g_controls.gamepad_p2;
|
||||||
|
applyControlsToGamepadManager();
|
||||||
|
const bool CHANGED =
|
||||||
|
(BEFORE_P1.name != g_controls.gamepad_p1.name || BEFORE_P1.path != g_controls.gamepad_p1.path ||
|
||||||
|
BEFORE_P2.name != g_controls.gamepad_p2.name || BEFORE_P2.path != g_controls.gamepad_p2.path);
|
||||||
|
if (CHANGED) { saveToFile(); }
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
|
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
|
||||||
@@ -594,7 +657,15 @@ namespace Options {
|
|||||||
out << " volume: " << g_audio.music.volume << "\n";
|
out << " volume: " << g_audio.music.volume << "\n";
|
||||||
out << " sound:\n";
|
out << " sound:\n";
|
||||||
out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\n";
|
out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\n";
|
||||||
out << " volume: " << g_audio.sound.volume << "\n";
|
out << " volume: " << g_audio.sound.volume << "\n\n";
|
||||||
|
|
||||||
|
out << "controls:\n";
|
||||||
|
out << " gamepad_p1:\n";
|
||||||
|
out << " name: \"" << g_controls.gamepad_p1.name << "\"\n";
|
||||||
|
out << " path: \"" << g_controls.gamepad_p1.path << "\"\n";
|
||||||
|
out << " gamepad_p2:\n";
|
||||||
|
out << " name: \"" << g_controls.gamepad_p2.name << "\"\n";
|
||||||
|
out << " path: \"" << g_controls.gamepad_p2.path << "\"\n";
|
||||||
|
|
||||||
return writeFile(g_config_file_path, out.str());
|
return writeFile(g_config_file_path, out.str());
|
||||||
}
|
}
|
||||||
|
|||||||