Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb07e7e564 | ||
|
|
03f1c5f926 | ||
|
|
6800786194 | ||
|
|
9c9b6028e7 | ||
|
|
2cb21211ef | ||
|
|
87a9f098b5 | ||
|
|
d31483124e | ||
|
|
2883df57da | ||
|
|
510ab55a33 | ||
|
|
a7e681a347 | ||
|
|
b19a6e25da | ||
|
|
d9f6fb45f8 | ||
|
|
64e0178ee9 | ||
|
|
a3b03b87be | ||
|
|
32dcf264c4 | ||
|
|
a3592d6c73 | ||
|
|
13ae4a51a4 | ||
|
|
ee8dc25181 | ||
|
|
e6b612d164 |
No files matched your search
@@ -14,3 +14,4 @@ _aee_2026
|
|||||||
.cache/
|
.cache/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.claude/
|
||||||
@@ -76,7 +76,12 @@ set(APP_SOURCES
|
|||||||
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
|
||||||
|
source/game/debug_config.cpp
|
||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
|
source/game/scenes/placeholder_input.cpp
|
||||||
|
source/game/scenes/logo_scene.cpp
|
||||||
|
source/game/scenes/title_scene.cpp
|
||||||
|
source/game/scenes/intro_scene.cpp
|
||||||
source/game/scenes/game_scene.cpp
|
source/game/scenes/game_scene.cpp
|
||||||
source/game/entities/player.cpp
|
source/game/entities/player.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
@@ -150,14 +155,19 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
|||||||
|
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
||||||
|
|
||||||
# Copia recursius de la carpeta data/ al costat del binari per que les rutes
|
# Còpia recursiva de data/ al costat del binari perquè les rutes relatives
|
||||||
# relatives ("data/player/player.gif") funcionen sense desplegament.
|
# ("data/player/player.gif") funcionin sense desplegament. Es defineix com
|
||||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
# a custom target amb `ALL` — s'executa a cada `cmake --build` encara que
|
||||||
|
# el binari no hagi canviat, de manera que editar YAML/GIF sense tocar codi
|
||||||
|
# igualment sincronitza el build. Abans era un POST_BUILD, i si només es
|
||||||
|
# modificava una dada la còpia no es disparava.
|
||||||
|
add_custom_target(sync_data ALL
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${CMAKE_SOURCE_DIR}/data"
|
"${CMAKE_SOURCE_DIR}/data"
|
||||||
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/data"
|
"${CMAKE_BINARY_DIR}/data"
|
||||||
COMMENT "Copying data/ next to the binary"
|
COMMENT "Copying data/ next to the binary"
|
||||||
)
|
)
|
||||||
|
add_dependencies(${PROJECT_NAME} sync_data)
|
||||||
|
|
||||||
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
|
||||||
@@ -255,6 +265,7 @@ option(BUILD_TESTS "Build smoke tests" ON)
|
|||||||
if(BUILD_TESTS)
|
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/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
${ENTITY_SOURCES}
|
${ENTITY_SOURCES}
|
||||||
)
|
)
|
||||||
@@ -268,4 +279,25 @@ if(BUILD_TESTS)
|
|||||||
add_test(NAME entities_smoke
|
add_test(NAME entities_smoke
|
||||||
COMMAND test_entities_smoke
|
COMMAND test_entities_smoke
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
|
# Smoke del flux d'escenes i del parser de debug.yaml. Stand-alone:
|
||||||
|
# només enllaça els helpers purs (debug_config, placeholder_input,
|
||||||
|
# key_config) perquè la integració completa dels placeholders arrossegaria
|
||||||
|
# tot l'stack SDL (Screen/Notifier/RenderInfo), coberta pel smoke xvfb.
|
||||||
|
add_executable(test_scene_flow
|
||||||
|
tests/test_scene_flow.cpp
|
||||||
|
source/game/debug_config.cpp
|
||||||
|
source/game/scenes/placeholder_input.cpp
|
||||||
|
source/core/input/key_config.cpp
|
||||||
|
source/core/resources/resource_helper.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_scene_flow PRIVATE
|
||||||
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(test_scene_flow PRIVATE SDL3::SDL3)
|
||||||
|
target_compile_options(test_scene_flow PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
add_test(NAME scene_flow
|
||||||
|
COMMAND test_scene_flow
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
endif()
|
endif()
|
||||||
@@ -14,47 +14,66 @@ endif
|
|||||||
|
|
||||||
CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH)
|
CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH)
|
||||||
|
|
||||||
.PHONY: all configure build run clean rebuild release show-version format format-check tidy tidy-fix cppcheck hooks-install
|
.PHONY: all debug release run run-debug clean rebuild show-version format format-check tidy tidy-fix cppcheck hooks-install help
|
||||||
|
|
||||||
all: build
|
# ==============================================================================
|
||||||
|
# COMPILACIÓ
|
||||||
|
# ==============================================================================
|
||||||
|
# Convenció alineada amb ../aee i ../projecte_2026:
|
||||||
|
# make → Release (binari d'ús normal)
|
||||||
|
# make debug → Debug (flux complet + `debug.yaml` per triar escena inicial)
|
||||||
|
# make release → Release + empaquetat per a distribució (TODO, pendent)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
configure:
|
all:
|
||||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
|
||||||
|
|
||||||
build: configure
|
|
||||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
|
||||||
|
|
||||||
run: build
|
|
||||||
@./$(BUILDDIR)/$(PROJECT)
|
|
||||||
|
|
||||||
release:
|
|
||||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
|
||||||
|
debug:
|
||||||
|
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||||
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
|
||||||
|
run: all
|
||||||
|
@./$(BUILDDIR)/$(PROJECT)
|
||||||
|
|
||||||
|
run-debug: debug
|
||||||
|
@./$(BUILDDIR)/$(PROJECT)
|
||||||
|
|
||||||
|
# Release + empaquetat per distribució (zip/dmg/tar.gz). De moment només
|
||||||
|
# compila amb CMAKE_BUILD_TYPE=Release; l'empaquetat OS-specific està
|
||||||
|
# pendent (veure Makefile de ../aee com a referència).
|
||||||
|
release: all
|
||||||
|
@echo "TODO: empaquetat per a distribució (zip/dmg/tar.gz) — pendent"
|
||||||
|
|
||||||
show-version:
|
show-version:
|
||||||
@echo "$(PROJECT) $(VERSION) ($(GIT_HASH))"
|
@echo "$(PROJECT) $(VERSION) ($(GIT_HASH))"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -rf $(BUILDDIR)
|
@rm -rf $(BUILDDIR)
|
||||||
|
|
||||||
rebuild: clean build
|
rebuild: clean all
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CODE QUALITY (delegados a cmake)
|
# CODE QUALITY (delegados a cmake)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
format: configure
|
format:
|
||||||
|
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) --target format
|
@cmake --build $(BUILDDIR) --target format
|
||||||
|
|
||||||
format-check: configure
|
format-check:
|
||||||
|
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) --target format-check
|
@cmake --build $(BUILDDIR) --target format-check
|
||||||
|
|
||||||
tidy: configure
|
tidy:
|
||||||
|
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) --target tidy
|
@cmake --build $(BUILDDIR) --target tidy
|
||||||
|
|
||||||
tidy-fix: configure
|
tidy-fix:
|
||||||
|
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) --target tidy-fix
|
@cmake --build $(BUILDDIR) --target tidy-fix
|
||||||
|
|
||||||
cppcheck: configure
|
cppcheck:
|
||||||
|
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) --target cppcheck
|
@cmake --build $(BUILDDIR) --target cppcheck
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -63,3 +82,35 @@ cppcheck: configure
|
|||||||
hooks-install:
|
hooks-install:
|
||||||
@git config core.hooksPath .githooks
|
@git config core.hooksPath .githooks
|
||||||
@echo "Git hooks activats: $(shell pwd)/.githooks"
|
@echo "Git hooks activats: $(shell pwd)/.githooks"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# AJUDA
|
||||||
|
# ==============================================================================
|
||||||
|
help:
|
||||||
|
@echo "Makefile per a $(PROJECT)"
|
||||||
|
@echo ""
|
||||||
|
@echo " Compilació:"
|
||||||
|
@echo " make - Compilar amb cmake (Release)"
|
||||||
|
@echo " make debug - Compilar amb cmake (Debug)"
|
||||||
|
@echo ""
|
||||||
|
@echo " Execució:"
|
||||||
|
@echo " make run - Compilar (Release) i executar"
|
||||||
|
@echo " make run-debug - Compilar (Debug) i executar"
|
||||||
|
@echo ""
|
||||||
|
@echo " Release:"
|
||||||
|
@echo " make release - Release + empaquetat (TODO pendent)"
|
||||||
|
@echo ""
|
||||||
|
@echo " Qualitat de codi:"
|
||||||
|
@echo " make format - Formatar codi amb clang-format"
|
||||||
|
@echo " make format-check - Verificar format sense modificar"
|
||||||
|
@echo " make tidy - Anàlisi estàtic amb clang-tidy"
|
||||||
|
@echo " make tidy-fix - Anàlisi estàtic amb auto-fix"
|
||||||
|
@echo " make cppcheck - Anàlisi estàtic amb cppcheck"
|
||||||
|
@echo ""
|
||||||
|
@echo " Altres:"
|
||||||
|
@echo " make clean - Esborrar carpeta $(BUILDDIR)/"
|
||||||
|
@echo " make rebuild - clean + all"
|
||||||
|
@echo " make show-version - Mostrar versió"
|
||||||
|
@echo " make hooks-install - Activar git hooks del projecte"
|
||||||
|
@echo ""
|
||||||
|
@echo " Versió actual: $(VERSION) ($(GIT_HASH))"
|
||||||
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 1.8 KiB |
@@ -4,15 +4,32 @@ sprite:
|
|||||||
path: mummy.gif
|
path: mummy.gif
|
||||||
size: [12, 24]
|
size: [12, 24]
|
||||||
tile_size: [1, 1]
|
tile_size: [1, 1]
|
||||||
|
# La momia usa falsa perspectiva: el sprite fa 12×24 (dos 12×12: cap i
|
||||||
|
# peus) però la caja de col·lisió amb el mapa és 16×16, concèntrica amb
|
||||||
|
# el rectangle-base 12×12 dels peus (igual que fa el jugador). Així la
|
||||||
|
# caixa s'estén 2 px a cada costat i 2 px per sota dels peus visuals,
|
||||||
|
# i amb `anchor: feet` s'alinea exactament amb el tile 16×16 on naix —
|
||||||
|
# la momia va pel centre del corredor i la caja ocupa la cel·la completa.
|
||||||
|
anchor: feet
|
||||||
|
collision:
|
||||||
|
size: [16, 16]
|
||||||
|
base_size: [12, 12]
|
||||||
flags:
|
flags:
|
||||||
collides: true
|
collides: true
|
||||||
interactable: false
|
interactable: false
|
||||||
|
# Strip de 12 frames en graella 12×1 (mateix esquema que el jugador):
|
||||||
|
# 0..3 → walk_down (cara avall)
|
||||||
|
# 4..7 → walk_up (cara amunt)
|
||||||
|
# 8..11 → walk (lateral; flip horitzontal segons el sentit)
|
||||||
|
# La momia no té variants `stand_*` perquè mai no para.
|
||||||
animations:
|
animations:
|
||||||
idle: { frames: [0], fps: 1, loop: true }
|
walk_down: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
||||||
walk: { frames: [0], fps: 8, loop: true }
|
walk_up: { frames: [4, 5, 6, 7], fps: 8, loop: true }
|
||||||
|
walk: { frames: [8, 9, 10, 11], fps: 8, loop: true }
|
||||||
ai:
|
ai:
|
||||||
type: corridor_wanderer
|
type: corridor_wanderer
|
||||||
direction_choice: random
|
direction_choice: random
|
||||||
|
initial_direction: right
|
||||||
stats:
|
stats:
|
||||||
speed: 40
|
speed: 30
|
||||||
damage: 1
|
damage: 1
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 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 # ·
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,132 @@
|
|||||||
|
# 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 # ·
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,132 @@
|
|||||||
|
# Font: ari-w9500-condensed-display — generado desde ari-w9500-condensed-display.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 3 # !
|
||||||
|
34 6 # "
|
||||||
|
35 9 # #
|
||||||
|
36 7 # $
|
||||||
|
37 11 # %
|
||||||
|
38 8 # &
|
||||||
|
39 3 # '
|
||||||
|
40 5 # (
|
||||||
|
41 5 # )
|
||||||
|
42 5 # *
|
||||||
|
43 7 # +
|
||||||
|
44 4 # ,
|
||||||
|
45 6 # -
|
||||||
|
46 3 # .
|
||||||
|
47 6 # /
|
||||||
|
48 7 # 0
|
||||||
|
49 6 # 1
|
||||||
|
50 7 # 2
|
||||||
|
51 7 # 3
|
||||||
|
52 8 # 4
|
||||||
|
53 7 # 5
|
||||||
|
54 7 # 6
|
||||||
|
55 7 # 7
|
||||||
|
56 7 # 8
|
||||||
|
57 7 # 9
|
||||||
|
58 3 # :
|
||||||
|
59 4 # ;
|
||||||
|
60 7 # <
|
||||||
|
61 7 # =
|
||||||
|
62 7 # >
|
||||||
|
63 7 # ?
|
||||||
|
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 3 # I
|
||||||
|
74 6 # J
|
||||||
|
75 8 # K
|
||||||
|
76 7 # L
|
||||||
|
77 9 # M
|
||||||
|
78 8 # N
|
||||||
|
79 9 # O
|
||||||
|
80 7 # P
|
||||||
|
81 9 # Q
|
||||||
|
82 7 # R
|
||||||
|
83 7 # S
|
||||||
|
84 7 # T
|
||||||
|
85 7 # U
|
||||||
|
86 7 # V
|
||||||
|
87 10 # W
|
||||||
|
88 8 # X
|
||||||
|
89 7 # Y
|
||||||
|
90 8 # Z
|
||||||
|
91 5 # [
|
||||||
|
92 6 # \
|
||||||
|
93 5 # ]
|
||||||
|
94 6 # ^
|
||||||
|
95 7 # _
|
||||||
|
96 5 # `
|
||||||
|
97 7 # a
|
||||||
|
98 7 # b
|
||||||
|
99 7 # c
|
||||||
|
100 7 # d
|
||||||
|
101 7 # e
|
||||||
|
102 6 # 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 6 # r
|
||||||
|
115 6 # s
|
||||||
|
116 6 # t
|
||||||
|
117 7 # u
|
||||||
|
118 7 # v
|
||||||
|
119 8 # w
|
||||||
|
120 7 # x
|
||||||
|
121 7 # y
|
||||||
|
122 7 # z
|
||||||
|
123 6 # {
|
||||||
|
124 3 # |
|
||||||
|
125 6 # }
|
||||||
|
126 8 # ~
|
||||||
|
192 8 # À
|
||||||
|
193 8 # Á
|
||||||
|
200 7 # È
|
||||||
|
201 7 # É
|
||||||
|
204 3 # Ì
|
||||||
|
205 3 # Í
|
||||||
|
210 9 # Ò
|
||||||
|
211 9 # Ó
|
||||||
|
217 7 # Ù
|
||||||
|
218 7 # Ú
|
||||||
|
209 8 # Ñ
|
||||||
|
199 8 # Ç
|
||||||
|
224 7 # à
|
||||||
|
225 7 # á
|
||||||
|
232 7 # è
|
||||||
|
233 7 # é
|
||||||
|
236 4 # ì
|
||||||
|
237 3 # í
|
||||||
|
242 7 # ò
|
||||||
|
243 7 # ó
|
||||||
|
249 7 # ù
|
||||||
|
250 7 # ú
|
||||||
|
241 7 # ñ
|
||||||
|
231 7 # ç
|
||||||
|
161 3 # ¡
|
||||||
|
191 7 # ¿
|
||||||
|
171 10 # «
|
||||||
|
187 10 # »
|
||||||
|
183 3 # ·
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,132 @@
|
|||||||
|
# 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 # ·
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,132 @@
|
|||||||
|
# Font: ari-w9500-display — generado desde ari-w9500-display.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 3 # !
|
||||||
|
34 6 # "
|
||||||
|
35 9 # #
|
||||||
|
36 8 # $
|
||||||
|
37 12 # %
|
||||||
|
38 9 # &
|
||||||
|
39 3 # '
|
||||||
|
40 5 # (
|
||||||
|
41 5 # )
|
||||||
|
42 5 # *
|
||||||
|
43 7 # +
|
||||||
|
44 4 # ,
|
||||||
|
45 6 # -
|
||||||
|
46 3 # .
|
||||||
|
47 6 # /
|
||||||
|
48 8 # 0
|
||||||
|
49 6 # 1
|
||||||
|
50 8 # 2
|
||||||
|
51 8 # 3
|
||||||
|
52 8 # 4
|
||||||
|
53 8 # 5
|
||||||
|
54 8 # 6
|
||||||
|
55 8 # 7
|
||||||
|
56 8 # 8
|
||||||
|
57 8 # 9
|
||||||
|
58 3 # :
|
||||||
|
59 4 # ;
|
||||||
|
60 7 # <
|
||||||
|
61 7 # =
|
||||||
|
62 7 # >
|
||||||
|
63 8 # ?
|
||||||
|
64 12 # @
|
||||||
|
65 9 # A
|
||||||
|
66 8 # B
|
||||||
|
67 9 # C
|
||||||
|
68 9 # D
|
||||||
|
69 8 # E
|
||||||
|
70 8 # F
|
||||||
|
71 9 # G
|
||||||
|
72 8 # H
|
||||||
|
73 3 # I
|
||||||
|
74 7 # J
|
||||||
|
75 9 # K
|
||||||
|
76 8 # L
|
||||||
|
77 10 # M
|
||||||
|
78 9 # N
|
||||||
|
79 10 # O
|
||||||
|
80 8 # P
|
||||||
|
81 10 # Q
|
||||||
|
82 8 # R
|
||||||
|
83 8 # S
|
||||||
|
84 9 # T
|
||||||
|
85 8 # U
|
||||||
|
86 9 # V
|
||||||
|
87 12 # W
|
||||||
|
88 9 # X
|
||||||
|
89 9 # Y
|
||||||
|
90 9 # Z
|
||||||
|
91 5 # [
|
||||||
|
92 6 # \
|
||||||
|
93 5 # ]
|
||||||
|
94 7 # ^
|
||||||
|
95 7 # _
|
||||||
|
96 5 # `
|
||||||
|
97 8 # a
|
||||||
|
98 8 # b
|
||||||
|
99 8 # c
|
||||||
|
100 8 # d
|
||||||
|
101 8 # e
|
||||||
|
102 6 # f
|
||||||
|
103 8 # g
|
||||||
|
104 8 # h
|
||||||
|
105 3 # i
|
||||||
|
106 4 # j
|
||||||
|
107 8 # k
|
||||||
|
108 3 # l
|
||||||
|
109 11 # m
|
||||||
|
110 8 # n
|
||||||
|
111 8 # o
|
||||||
|
112 8 # p
|
||||||
|
113 8 # q
|
||||||
|
114 6 # r
|
||||||
|
115 7 # s
|
||||||
|
116 6 # t
|
||||||
|
117 8 # u
|
||||||
|
118 8 # v
|
||||||
|
119 9 # w
|
||||||
|
120 8 # x
|
||||||
|
121 8 # y
|
||||||
|
122 8 # z
|
||||||
|
123 6 # {
|
||||||
|
124 3 # |
|
||||||
|
125 6 # }
|
||||||
|
126 9 # ~
|
||||||
|
192 9 # À
|
||||||
|
193 9 # Á
|
||||||
|
200 8 # È
|
||||||
|
201 8 # É
|
||||||
|
204 3 # Ì
|
||||||
|
205 3 # Í
|
||||||
|
210 10 # Ò
|
||||||
|
211 10 # Ó
|
||||||
|
217 8 # Ù
|
||||||
|
218 8 # Ú
|
||||||
|
209 9 # Ñ
|
||||||
|
199 9 # Ç
|
||||||
|
224 8 # à
|
||||||
|
225 8 # á
|
||||||
|
232 8 # è
|
||||||
|
233 8 # é
|
||||||
|
236 4 # ì
|
||||||
|
237 3 # í
|
||||||
|
242 8 # ò
|
||||||
|
243 8 # ó
|
||||||
|
249 8 # ù
|
||||||
|
250 8 # ú
|
||||||
|
241 8 # ñ
|
||||||
|
231 8 # ç
|
||||||
|
161 3 # ¡
|
||||||
|
191 8 # ¿
|
||||||
|
171 10 # «
|
||||||
|
187 10 # »
|
||||||
|
183 3 # ·
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,132 @@
|
|||||||
|
# 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 # ·
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -130,3 +130,5 @@ entities:
|
|||||||
position: [9, 1]
|
position: [9, 1]
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 7]
|
position: [10, 7]
|
||||||
|
- type: mummy
|
||||||
|
position: [7, 9]
|
||||||
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 1.8 KiB |
@@ -12,21 +12,26 @@ collision:
|
|||||||
base_size: 12
|
base_size: 12
|
||||||
|
|
||||||
animations:
|
animations:
|
||||||
|
- name: stand_down
|
||||||
|
frames: [0]
|
||||||
|
|
||||||
|
- name: stand_up
|
||||||
|
frames: [4]
|
||||||
|
|
||||||
- name: stand
|
- name: stand
|
||||||
frames: [1]
|
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
|
- name: walk
|
||||||
speed: 0.07
|
speed: 0.07
|
||||||
loopFrom: 0
|
loopFrom: 0
|
||||||
frames: [2, 3, 4, 1]
|
frames: [8, 9, 10, 11]
|
||||||
|
|
||||||
- name: turn_walk
|
|
||||||
speed: [0.1, 0.07, 0.07, 0.07, 0.07]
|
|
||||||
loopFrom: 1
|
|
||||||
frames: [0, 2, 3, 4, 1]
|
|
||||||
|
|
||||||
- name: jump
|
|
||||||
frames: [6]
|
|
||||||
|
|
||||||
- name: jump_peak
|
|
||||||
frames: [5]
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# `docs/`
|
||||||
|
|
||||||
|
| Fitxer | Tipus | Estat |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| [audit-core-audio.md](audit-core-audio.md) | Auditoria de `source/core/audio/` | Arxivada (tancada amb `refactor/audio-engine-class`) |
|
||||||
|
| [audit-core-rendering.md](audit-core-rendering.md) | Auditoria de `source/core/rendering/` | Arxivada (tots els ítems DONE/WONTFIX/DEFERRED documentats) |
|
||||||
|
| [audit-game.md](audit-game.md) | Auditoria de `source/game/` | Arxivada (tancada amb `refactor/options-class`) |
|
||||||
|
| [convencions.md](convencions.md) | Convencions del repo (commits, `debug.yaml`) | Viu |
|
||||||
|
| [entities-system.md](entities-system.md) | Disseny del sistema d'entitats (factory, bus, EntityManager, hazards) | Viu |
|
||||||
|
| [player-animations.md](player-animations.md) | Notes d'animacions direccionals i `stand` automàtic del Player | Viu |
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
Rama: `audit/game`.
|
Rama: `audit/game`.
|
||||||
Cobertura: `source/game/{entities,map,scenes,ui}` + `source/game/options.{hpp,cpp}` (12 fitxers, ~1.486 línies).
|
Cobertura: `source/game/{entities,map,scenes,ui}` + `source/game/options.{hpp,cpp}` (12 fitxers, ~1.486 línies).
|
||||||
|
|
||||||
Estado global: **28 DONE, 2 DONE (parcial), 5 WONTFIX, 4 DEFERRED** sobre 39 ítems (el top 5 compta dins dels seus IDs individuals). Cada canvi té un commit aïllat i s'ha provat amb `SDL_VIDEODRIVER=dummy` + `SIGINT` (el log de `Game start` a `Bye!` és idèntic entre commits; la branca `config.yaml` de backup verificada manualment amb un fitxer corrupte). Els 4 DEFERRED (COUP-02, COUP-03, CONV-02 resta, PATCH-01) van a la futura branca `refactor/options-class`.
|
Estado global: **32 DONE, 1 DONE (parcial), 5 WONTFIX, 0 DEFERRED** sobre 39 ítems (el top 5 compta dins dels seus IDs individuals). Cada canvi té un commit aïllat i s'ha provat amb `SDL_VIDEODRIVER=dummy` + `SIGINT` (el log de `Game start` a `Bye!` és idèntic entre commits; la branca `config.yaml` de backup verificada manualment amb un fitxer corrupte). Els 4 DEFERRED inicials (COUP-02, COUP-03, CONV-02 part Options, PATCH-01) s'han tancat a la branca `refactor/options-class` (registre de commits al final del fitxer); l'únic ítem que queda en DONE (parcial) és INCONS-01, intencionadament — la nota n'explica el motiu.
|
||||||
|
|
||||||
Estados posibles:
|
Estados posibles:
|
||||||
- `PENDING` — identificat, sense tocar
|
- `PENDING` — identificat, sense tocar
|
||||||
@@ -69,14 +69,14 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer
|
|||||||
|----|--------------|----------|-----------|-------|-------|
|
|----|--------------|----------|-----------|-------|-------|
|
||||||
| COUP-01 | `core/input/global_inputs.cpp:15` | `core/` inclou `game/ui/notification_presets.hpp` | Crítico | DONE | `notification_presets.hpp` mogut a `core/rendering/` (al costat del `Notifier` que consumeix). `global_inputs.cpp` i el comentari de `notifier.hpp` actualitzats |
|
| COUP-01 | `core/input/global_inputs.cpp:15` | `core/` inclou `game/ui/notification_presets.hpp` | Crítico | DONE | `notification_presets.hpp` mogut a `core/rendering/` (al costat del `Notifier` que consumeix). `global_inputs.cpp` i el comentari de `notifier.hpp` actualitzats |
|
||||||
| COUP-02 | `player.cpp` + `Map::PLAYFIELD_*`, `PITCH_*`, `GRID_*`, `collides` | `Player` coneix la geometria interna del mapa | Importante | DEFERRED | Player només llegeix globals de `Map::`, no muta res. Injectar `MapInfo&` té sentit quan hi haja múltiples mapes simultanis o un sistema tipus ECS. Mentre hi haja un Map actiu global, l'acoblament és acceptable. Es refacta amb COUP-03 a `refactor/options-class` |
|
| COUP-02 | `player.cpp` + `Map::PLAYFIELD_*`, `PITCH_*`, `GRID_*`, `collides` | `Player` coneix la geometria interna del mapa | Importante | DEFERRED | Player només llegeix globals de `Map::`, no muta res. Injectar `MapInfo&` té sentit quan hi haja múltiples mapes simultanis o un sistema tipus ECS. Mentre hi haja un Map actiu global, l'acoblament és acceptable. Es refacta amb COUP-03 a `refactor/options-class` |
|
||||||
| COUP-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | DEFERRED | Encapsular tots els globals darrere setters + observers per Screen/Audio/RenderInfo és un refactor ample (equivalent a COUP-01 de l'audit d'àudio, que va anar a una branca pròpia). Es tractarà a `refactor/options-class` perquè toca tots els callers de core/input. Es fa en branca separada |
|
| COUP-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | DONE | Estat mutable mogut a anonymous namespace dins `options.cpp`; API pública són getters const-ref (`window()`, `video()`, `audio()`, `locale()`, `postfxPresets()`, `crtpiPresets()`) + setters específics (`setWindowZoom`, `setShaderEnabled`, `setActiveShaderName`, ...) que muten i persisteixen via `saveToFile()`. Els setters NO notifiquen els subsistemes: el caller (`global_inputs.cpp`) crida després `Screen::setWindowZoom`, `reapplyShader()`, etc. La part "observers" del audit es deixa WONTFIX (veure CONV-02): notificar des d'Options invertiria la direcció `Director→Screen/Audio` i Options aprenria del layout dels subsistemes |
|
||||||
| COUP-04 | `tilemap.cpp:117-138` | Clip+blit directe al canvas ARGB que podria viure a `Surface` | Menor | WONTFIX | Únic caller en tot el projecte. Promoure a `Surface::blitInto` ara afegiria una API genèrica per a un ús únic (YAGNI). Si un segon client apareix, es mourea en aquell moment |
|
| COUP-04 | `tilemap.cpp:117-138` | Clip+blit directe al canvas ARGB que podria viure a `Surface` | Menor | WONTFIX | Únic caller en tot el projecte. Promoure a `Surface::blitInto` ara afegiria una API genèrica per a un ús únic (YAGNI). Si un segon client apareix, es mourea en aquell moment |
|
||||||
|
|
||||||
## 5. Inconsistències internes
|
## 5. Inconsistències internes
|
||||||
|
|
||||||
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes |
|
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes |
|
||||||
|----|--------------|----------|-----------|-------|-------|
|
|----|--------------|----------|-----------|-------|-------|
|
||||||
| INCONS-01 | `game/*` | 3 estils de mòdul: namespace+inline (Options), classe all-static (Map), OOP (Player/Tilemap), namespace+constexpr (UiColor/Notification) | Importante | DONE (parcial) | Resolt "classe all-static" amb PATCH-07 (Map → namespace). Queden 3 estils diferenciats per responsabilitat: `namespace` per a estat compartit (Options, Map), classes per a entitats amb estat (Player, Tilemap), `namespace+constexpr` per a alias/presets (UiColor, Notification). La distinció ara és intencional, no fortuïta |
|
| INCONS-01 | `game/*` | 3 estils de mòdul: namespace+inline (Options), classe all-static (Map), OOP (Player/Tilemap), namespace+constexpr (UiColor/Notification) | Importante | DONE (parcial) | Resolt "classe all-static" amb PATCH-07 (Map → namespace). Queden 3 estils diferenciats per responsabilitat: `namespace` per a estat compartit (Options, Map), classes per a entitats amb estat (Player, Tilemap), `namespace+constexpr` per a alias/presets (UiColor, Notification). La distinció ara és intencional, no fortuïta. NOTA (post `refactor/options-class`): "encapsular" en COUP-03 vol dir ocultar l'estat mutable darrere de l'API del namespace (anonymous namespace + getters/setters), no convertir Options a classe — el patró `namespace per a estat compartit` queda preservat |
|
||||||
| INCONS-02 | `options.cpp:372,477,530,579,607` | `std::cerr` en comptes de `std::cout` (convenció del projecte) | Importante | DONE | Totes les ocurrències (10, incloent les afegides a PATCH-04) substituïdes per `std::cout`. Test manual: amb config.yaml trencat el missatge de backup va per stdout |
|
| INCONS-02 | `options.cpp:372,477,530,579,607` | `std::cerr` en comptes de `std::cout` (convenció del projecte) | Importante | DONE | Totes les ocurrències (10, incloent les afegides a PATCH-04) substituïdes per `std::cout`. Test manual: amb config.yaml trencat el missatge de backup va per stdout |
|
||||||
| INCONS-03 | `options.cpp` vs `tilemap.cpp:77-78` | Diferent política en YAML absent: `contains()` vs accés directe que llança | Menor | WONTFIX | Les dues polítiques són correctes per al seu cas: `Tilemap`/`Map::loadFrom` tenen fitxers de definició (asset obligatori → llançar); `Options` parseja preferències de l'usuari (camp absent → manté default). No és inconsistència sinó dos contractes diferents |
|
| INCONS-03 | `options.cpp` vs `tilemap.cpp:77-78` | Diferent política en YAML absent: `contains()` vs accés directe que llança | Menor | WONTFIX | Les dues polítiques són correctes per al seu cas: `Tilemap`/`Map::loadFrom` tenen fitxers de definició (asset obligatori → llançar); `Options` parseja preferències de l'usuari (camp absent → manté default). No és inconsistència sinó dos contractes diferents |
|
||||||
| INCONS-04 | `options.cpp`, `player.cpp` | Locals ALL-CAPS (`CONTENT`, `IT`, `DT`, `OLD_X`) barrejats amb `snake_case` | Menor | WONTFIX | El `.clang-tidy` del projecte exigeix `LocalConstantCase = UPPER_CASE` i `VariableCase = lower_case` — els ALL-CAPS són per a `const`/`constexpr` i els snake_case per a mutables. La barreja es correcta i està dictada pel linter |
|
| INCONS-04 | `options.cpp`, `player.cpp` | Locals ALL-CAPS (`CONTENT`, `IT`, `DT`, `OLD_X`) barrejats amb `snake_case` | Menor | WONTFIX | El `.clang-tidy` del projecte exigeix `LocalConstantCase = UPPER_CASE` i `VariableCase = lower_case` — els ALL-CAPS són per a `const`/`constexpr` i els snake_case per a mutables. La barreja es correcta i està dictada pel linter |
|
||||||
@@ -97,6 +97,16 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer
|
|||||||
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes |
|
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes |
|
||||||
|----|--------------|----------|-----------|-------|-------|
|
|----|--------------|----------|-----------|-------|-------|
|
||||||
| CONV-01 | `player.*`, `map.*`, `ui/*`, `game_scene.cpp` | Speed, caixa de col·lisió, geometria, índexs paleta, rutes d'assets, nom del track: tot cablejat | Crítico | DONE | Tapat per HC-01/02/03/04/05/06: speed a Defaults::Player, col·lisió a player.yaml, geometria a pyramid.yaml, índexs paleta a Defaults::UI, rutes a Defaults::Pyramid + Config |
|
| CONV-01 | `player.*`, `map.*`, `ui/*`, `game_scene.cpp` | Speed, caixa de col·lisió, geometria, índexs paleta, rutes d'assets, nom del track: tot cablejat | Crítico | DONE | Tapat per HC-01/02/03/04/05/06: speed a Defaults::Player, col·lisió a player.yaml, geometria a pyramid.yaml, índexs paleta a Defaults::UI, rutes a Defaults::Pyramid + Config |
|
||||||
| CONV-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | DONE (parcial) | Map resolt amb PATCH-07 (namespace). La part d'Options/observers va amb COUP-03 (DEFERRED a `refactor/options-class`) |
|
| CONV-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | DONE | Map resolt amb PATCH-07 (namespace). Options resolt amb COUP-03: estat mutable encapsulat darrere de l'API del namespace (anonymous ns + getters/setters). La part "observers" (notificacions automàtiques a Screen/Audio) queda WONTFIX justificat: invertiria la direcció de dependència `Director→Screen/Audio` que el projecte manté intencionadament |
|
||||||
| CONV-03 | `options.cpp` | `std::cerr` viola la regla "std::cout only" | Importante | DONE | Resolt amb INCONS-02 |
|
| CONV-03 | `options.cpp` | `std::cerr` viola la regla "std::cout only" | Importante | DONE | Resolt amb INCONS-02 |
|
||||||
| CONV-04 | `player.cpp:27` | `SDL_GetKeyboardState` directe en `Player::update` (fuga de SDL al gameplay) | Menor | WONTFIX | `SDL_GetKeyboardState` és l'accés correcte per a state-based polling a 60Hz; `EventBuffer` és per a esdeveniments discrets. La fuga és cosmètica — abstraure-la no aportaria valor |
|
| CONV-04 | `player.cpp:27` | `SDL_GetKeyboardState` directe en `Player::update` (fuga de SDL al gameplay) | Menor | WONTFIX | `SDL_GetKeyboardState` és l'accés correcte per a state-based polling a 60Hz; `EventBuffer` és per a esdeveniments discrets. La fuga és cosmètica — abstraure-la no aportaria valor |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registro de commits — `refactor/options-class`
|
||||||
|
|
||||||
|
Branca per a tancar els 4 ítems DEFERRED de `audit/game`. Base: `main` (32dcf26).
|
||||||
|
|
||||||
|
- `a3b03b8` — COUP-03 + CONV-02: estat mutable d'`Options` mogut a anonymous ns + API de getters/setters; `global_inputs.cpp` i `director.cpp` adaptats. CONV-02 (part Options) tanca al pas. La part "observers" del audit es deixa WONTFIX (invertiria `Director→Screen/Audio`)
|
||||||
|
- `64e0178` — COUP-02: `Map::Info` plana injectada al `Player` (només Player; momia/roca/loadlevel resten amb `Map::*` global, com diu la nota original del audit)
|
||||||
|
- `d9f6fb4` — PATCH-01: `tryMove(dx, dy, CollisionFn)` a `MovingSprite` + hook virtual `collisionBox()` a `Sprite`; `Player::update` deixa de fer `update + revertir + reaplicar` i passa a `animate(dt) + tryMove`
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Convencions del repo
|
||||||
|
|
||||||
|
## Missatges de commit
|
||||||
|
|
||||||
|
- En **valencià**, frases curtes i compactes.
|
||||||
|
- **Sense firma**: res de `Co-Authored-By:`, `Generated with Claude Code`, ni cap altra firma automàtica.
|
||||||
|
- Un commit per unitat lògica. Si el canvi afecta àrees separades (feature + docs + test), separar en commits diferents.
|
||||||
|
- Prefix opcional estil convencional (`feat:`, `fix:`, `refactor:`, `docs:`, `test:`) quan ajude a la llegibilitat.
|
||||||
|
|
||||||
|
Exemple correcte:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(scenes): afegir placeholders LogoScene/TitleScene/IntroScene
|
||||||
|
|
||||||
|
Tres escenes de 5 s cadascuna que precedeixen GameScene. Cada classe
|
||||||
|
té el seu fitxer perquè divergiran quan es desenvolupe el contingut
|
||||||
|
real (logo animat, menú de títol, cutscene).
|
||||||
|
```
|
||||||
|
|
||||||
|
## `debug.yaml` (ajust de dev, no versionat)
|
||||||
|
|
||||||
|
Fitxer personal de desenvolupament, **fora del repo**. Viu a la mateixa carpeta
|
||||||
|
de sistema que `config.yaml`:
|
||||||
|
|
||||||
|
- Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml`
|
||||||
|
- Per defecte: `~/.config/jailgames/aee_2026/debug.yaml`
|
||||||
|
|
||||||
|
### Comportament segons build
|
||||||
|
|
||||||
|
Convenció alineada amb `../aee` i `../projecte_2026`:
|
||||||
|
|
||||||
|
- **Release** (`make` — binari d'ús normal): `debug.yaml` s'ignora completament.
|
||||||
|
El joc arranca sempre a `LOGO` i recorre el flux sencer
|
||||||
|
`LOGO → TITLE → INTRO → GAME`, tal com ho veurà el jugador final.
|
||||||
|
- **Debug** (`make debug`): es llig `debug.yaml`. Si no existeix, es crea
|
||||||
|
automàticament amb `start_scene: GAME` perquè el dia a dia de gameplay no
|
||||||
|
arrossegui els 15 s de placeholders. Editant-lo a mà, es pot triar el punt
|
||||||
|
d'entrada.
|
||||||
|
- `make release` fa el Release + empaquetat per a distribució (zip/dmg/tar.gz);
|
||||||
|
l'empaquetat concret està pendent d'implementar.
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
start_scene: GAME # valors vàlids: LOGO, TITLE, INTRO, GAME
|
||||||
|
```
|
||||||
|
|
||||||
|
- `start_scene: LOGO` → arranca a `LogoScene` i recorre `LOGO → TITLE → INTRO → GAME`.
|
||||||
|
- `start_scene: TITLE` → arranca a `TitleScene` i recorre `TITLE → INTRO → GAME`.
|
||||||
|
- `start_scene: INTRO` → arranca a `IntroScene` i recorre `INTRO → GAME`.
|
||||||
|
- `start_scene: GAME` → arranca directament a `GameScene` (valor que s'escriu
|
||||||
|
per defecte en la creació automàtica).
|
||||||
|
|
||||||
|
### Comportament davant d'errors (build debug)
|
||||||
|
|
||||||
|
- Fitxer buit, valor desconegut o YAML invàlid → `GAME` amb warning a stderr.
|
||||||
|
El fitxer no es reescriu automàticament: es respecta l'edició manual per si
|
||||||
|
l'usuari ha teclejat alguna cosa intencionadament.
|
||||||
|
|
||||||
|
### Durada dels placeholders
|
||||||
|
|
||||||
|
Cada escena placeholder (LOGO, TITLE, INTRO) mostra el seu nom i un compte
|
||||||
|
regressiu de 5 segons al centre de la pantalla. Es pot saltar amb qualsevol
|
||||||
|
tecla excepte les registrades a `data/input/keys.yaml` (F1–F12 i Escape),
|
||||||
|
que mantenen el seu comportament de hotkey global.
|
||||||
@@ -4,6 +4,22 @@ Registre del disseny, els treballs fets i els pendents per al sistema d'entitats
|
|||||||
|
|
||||||
Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
|
Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
|
||||||
|
|
||||||
|
## Convenció de col·lisió i anchors (fase 3)
|
||||||
|
|
||||||
|
- **`sprite.size`** (YAML entitat): mida en píxels del frame de render.
|
||||||
|
- **`tile_size`** (YAML entitat): footprint lògic en tiles. Només el
|
||||||
|
consumeix Sarcophagus per al perímetre; a la resta és declaratiu.
|
||||||
|
- **`collision: { size: [w, h], offset: [x, y] }`** (opcional al YAML
|
||||||
|
entitat): sub-rect de col·lisió dins del sprite. Si `offset` falta, es
|
||||||
|
calcula automàticament bottom-center (falsa perspectiva típica).
|
||||||
|
Sense bloc, `Entity::getCollisionRect()` = `pixel_rect_` sencer.
|
||||||
|
- **`anchor`** (opcional al YAML entitat): `top_left` (per defecte) o
|
||||||
|
`feet`. `feet` vol dir que el `position: [col, row]` del map és el
|
||||||
|
tile dels peus; el sprite es centra horitzontalment sobre el tile i
|
||||||
|
s'alinea la vora inferior del sprite amb la vora inferior del tile.
|
||||||
|
Els sarcòfags tenen bottom-align hardcoded i no usen `anchor`.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Context i objectiu inicial
|
## 1. Context i objectiu inicial
|
||||||
@@ -169,17 +185,17 @@ Es respecta tant a la fase 1 com a la 2.
|
|||||||
|
|
||||||
| ID | Descripció | Estat |
|
| ID | Descripció | Estat |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ENT-IA-01 | **Moviment viu de la momia**: el `chooseDirection` està al seu lloc però el `step` no itera interseccions ni valida direcció contra el mapa. La momia es dibuixa estàtica. | DEFERRED |
|
| ENT-IA-01 | **Moviment viu de la momia**: `Mummy::step` camina en línia recta validant `Map::collides` + `Map::insidePlayfield` a cada frame. En topar, recull perpendiculars lliures (evitant U-turn llevat de cul-de-sac) i crida `chooseDirection`. Cobert per T13. | DONE |
|
||||||
| ENT-IA-02 | **IA de la momia amb bias cap al jugador** (weighted_towards_player). Hook `chooseDirection` deixat preparat; només cal una subclasse/override. | DEFERRED |
|
| ENT-IA-02 | **IA de la momia amb bias cap al jugador** (weighted_towards_player). Hook `chooseDirection` deixat preparat; només cal una subclasse/override. | DEFERRED |
|
||||||
| ENT-HAZ-01 | **Trampes actives amb dany real al jugador**. `onPlayerContact` publica `player_hit` però encara no hi ha sistema de vides. Caldrà cablejar vides al Player. | DEFERRED |
|
| ENT-HAZ-01 | **Trampes actives amb dany real al jugador**. `onPlayerContact` publica `player_hit` via `EntityManager::checkPlayerCollisions`, però encara no hi ha sistema de vides. Caldrà cablejar vides al Player. | DEFERRED |
|
||||||
| ENT-ROCK-01 | **Col·lisió de la roca amb sarcs/murs**. `DynamicHazard::step` desplaça sense validar `Map::collides`. Una roca rodant a tota velocitat travessaria sarcs. | DEFERRED |
|
| ENT-ROCK-01 | **Col·lisió de la roca amb sarcs/murs**. `RollingRock::step` valida `Map::collides`/`insidePlayfield` i s'atura quan bloqueja (setDirection 0,0). No rebota per disseny. | DONE |
|
||||||
| ENT-DOOR-01 | **Retorn a la pantalla de títol al `level_complete`**. Ara només log a stdout. Pendent cablejar la transició de Scene. | DEFERRED |
|
| ENT-DOOR-01 | **Retorn a la pantalla de títol al `level_complete`**. Ara només log a stdout. Pendent cablejar la transició de Scene. | DEFERRED |
|
||||||
|
|
||||||
### Render i geometria
|
### Render i geometria
|
||||||
|
|
||||||
| ID | Descripció | Estat |
|
| ID | Descripció | Estat |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| REND-01 | **Z-order fi** entre entitats i Player (interleaving per `feetY`). Ara les entitats sempre queden sota el Player. Veure comentari a [game_scene.cpp:109-110](../source/game/scenes/game_scene.cpp#L109-L110). | DEFERRED |
|
| REND-01 | **Z-order fi** entre entitats i Player: `GameScene::render` construeix una llista {Entity*, Player*, feetY}, la ordena amb `stable_sort` ascendent i dibuixa per ordre. Els sprites amb peus més amunt queden darrere dels que tenen peus més avall. | DONE |
|
||||||
| REND-02 | **Animacions multi-frame**. `renderAll` clipa el frame actual via `src_rect` calculat amb `Entity::getAnimation()->currentFrame()` sobre una graella left-to-right / top-to-bottom; columnes deduïdes de `surface->getWidth() / pixel_rect.w`. Así un strip 1×N, N×1 o N×M funciona igual. Torch primer consumidor real (4 frames @ 8 fps). | DONE |
|
| REND-02 | **Animacions multi-frame**. `renderAll` clipa el frame actual via `src_rect` calculat amb `Entity::getAnimation()->currentFrame()` sobre una graella left-to-right / top-to-bottom; columnes deduïdes de `surface->getWidth() / pixel_rect.w`. Así un strip 1×N, N×1 o N×M funciona igual. Torch primer consumidor real (4 frames @ 8 fps). | DONE |
|
||||||
| REND-03 | **Paleta compartida** amb el tilemap. Cada GIF placeholder té la seua paleta pròpia (Pillow); funciona perquè `Surface` desa paleta per surface. Amb l'art real pot caldre unificar per evitar mismatch cromàtic. | DEFERRED |
|
| REND-03 | **Paleta compartida** amb el tilemap. Cada GIF placeholder té la seua paleta pròpia (Pillow); funciona perquè `Surface` desa paleta per surface. Amb l'art real pot caldre unificar per evitar mismatch cromàtic. | DEFERRED |
|
||||||
|
|
||||||
@@ -204,14 +220,66 @@ Es respecta tant a la fase 1 com a la 2.
|
|||||||
| ID | Descripció | Estat |
|
| ID | Descripció | Estat |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| TST-01 | **Test d'integració amb `loadFromLevel` real**: el smoke actual evita el Cache per no arrossegar dependències. Un test separat amb el Cache enllaçat podria verificar el blit, però demana instanciar Screen/Canvas. | WONTFIX (l'smoke d'arrencada sota `xvfb-run` ja cobreix el camí viu) |
|
| TST-01 | **Test d'integració amb `loadFromLevel` real**: el smoke actual evita el Cache per no arrossegar dependències. Un test separat amb el Cache enllaçat podria verificar el blit, però demana instanciar Screen/Canvas. | WONTFIX (l'smoke d'arrencada sota `xvfb-run` ja cobreix el camí viu) |
|
||||||
| TST-02 | **Test de moviment de la momia** (quan ENT-IA-01 estigui fet). Pendent juntament amb la IA. | DEFERRED |
|
| TST-02 | **Test de moviment de la momia**: T13 exercita step lliure (avança) i step contra paret (gira sense avançar). | DONE |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Estat global
|
## 6. Fase 4 — Moviment i interacció (DONE)
|
||||||
|
|
||||||
- **Andamiatge i integració bàsica**: DONE. Els 5 actors del `pyramid.yaml` es carreguen, s'assignen surfaces i es renderitzen estàticament sobre el mapa. El bus connecta Player ↔ sarcs ↔ porta.
|
Tanca els hooks d'IA i col·lisió que quedaven apuntalats a les fases 1–3.
|
||||||
- **Comportament dinàmic (IA, dany, transicions de scene)**: DEFERRED a iteracions posteriors. El hooks existeixen, falta omplir la lògica viva.
|
|
||||||
- **Smoke tests**: 9/9 passant; regla durable de cobrir amb smoke tota lògica no trivial respectada a les dues fases.
|
- **`Mummy::step`** implementat: desplaçament recte validant playfield i
|
||||||
|
sarcs; gir a cul-de-sac o intersecció via `chooseDirection`.
|
||||||
|
- **`RollingRock::step`** igual, però s'atura en lloc de girar.
|
||||||
|
- **`Entity::getCollisionRect()` + `getFeetY()`**: caixa AABB encastada al
|
||||||
|
sprite (falsa perspectiva, opcional via bloc `collision:` del YAML).
|
||||||
|
- **Anchor `feet`** al loader: `position: [col, row]` és el tile dels peus;
|
||||||
|
el sprite es centra en X i bottom-aligned en Y.
|
||||||
|
- **Sarcòfag amb caixa lògica**: `placeSarcophagus` aplica un `CollisionBox`
|
||||||
|
que retalla la meitat superior del sprite (alçada) perquè no dispari
|
||||||
|
falsos contactes.
|
||||||
|
- **`Map::insidePlayfield(box)`**: valida la caixa candidata dels `step`.
|
||||||
|
- **`EntityManager::checkPlayerCollisions(players)`**: detecta solapament
|
||||||
|
base-box del player amb `getCollisionRect` de cada entitat amb
|
||||||
|
`collides()=true`, i crida `onPlayerContact`. Cablejat al `GameScene::update`.
|
||||||
|
- **Z-order**: `GameScene::render` ordena {entitats, players} per `getFeetY()`
|
||||||
|
ascendent i dibuixa en aquell ordre (interleaved).
|
||||||
|
- **`Player::getBaseBox()`** exposat com a API pública.
|
||||||
|
- **Smoke tests**: T12 (CollisionBox), T13 (Mummy::step), T11 (sarc YAML),
|
||||||
|
T10 (torch anim). Total 14/14.
|
||||||
|
|
||||||
|
## 7. Fase 5 — Convenció de caixa "peus" com el jugador + animacions direccionals (DONE)
|
||||||
|
|
||||||
|
Alinea la momia amb la convenció de col·lisió i animació del Player, i soluciona regressions de navegació derivades del canvi de caixa.
|
||||||
|
|
||||||
|
### Caixa concèntrica amb el rectangle-base
|
||||||
|
|
||||||
|
- **`collision.base_size: [bw, bh]`** (nou camp YAML, opcional). Si apareix, la caja no s'alinea a la vora inferior del sprite sinó que queda **concèntrica amb el rectangle-base `bw×bh` dels peus** (els `bh` píxels inferiors del sprite). Fórmula: `off_y = sprite_h − (base_h + H) / 2`. Sense `base_size`, comportament antic (alineada a la vora inferior del sprite).
|
||||||
|
- **`anchor: feet` ara alinea la caja, no el sprite**. Si l'entitat té `CollisionBox`, el loader col·loca `pixel_rect_` perquè la caja quedi exactament al tile. Sense caja, fallback a la convenció antiga (sprite centrat horitzontalment + vora inferior al tile). Per la momia: sprite 12×24 a (tile_x+2, tile_y−10); caja 16×16 a (tile_x, tile_y) — omple el tile sencer com fa el jugador.
|
||||||
|
- **`mummy.yaml`**: `collision: { size: [16, 16], base_size: [12, 12] }`. Abans era `[12, 12]` alineada a baix; visualment quedava "enganxada al mur inferior" dins dels corredors horitzontals.
|
||||||
|
|
||||||
|
### Snap-to-edge per girar al playfield
|
||||||
|
|
||||||
|
La caja 16×16 té exactament l'amplada del corredor (16 px). Amb el moviment contínu `speed × delta_time` (~0.66 px/frame), la momia s'aturava a una posició subpíxel dins de l'última cel·la (p. ex. `x=279.87` en lloc de `x=280`) i qualsevol gir perpendicular col·lidia amb el sarc adjacent al limit. [`Mummy::snapToPlayfieldEdge`](../source/game/entities/concrete/mummy.cpp) redueix el `dx/dy` fins a la vora exacta quan el candidat sortiria del playfield; el gir posterior ja parteix d'una posició alineada amb el lane.
|
||||||
|
|
||||||
|
### Animacions direccionals
|
||||||
|
|
||||||
|
- **`mummy.gif` a 12 frames** (graella 12×1) amb el mateix esquema que el jugador: 0–3 walk_down, 4–7 walk_up, 8–11 walk lateral. Sense variants `stand_*` (mai no para).
|
||||||
|
- **`Mummy` té tres `AnimationComponent`** privats + un helper `changeDirection(dx, dy)` (únic punt d'entrada per canviar direcció) que selecciona l'animació i aplica el flip: `dx ≠ 0` → lateral amb flip horitzontal segons signe; `dy < 0` → up; `dy > 0` → down.
|
||||||
|
- **`Entity::flip_` a la base** + `getFlip()`. `renderEntity` el passa a `Surface::render`, que ja suportava `SDL_FlipMode`. Les entitats estàtiques queden a `SDL_FLIP_NONE` automàticament.
|
||||||
|
|
||||||
|
### Pipeline de build: sincronització de `data/`
|
||||||
|
|
||||||
|
Canvi a [CMakeLists.txt](../CMakeLists.txt): la còpia de `data/` al build era un `add_custom_command(POST_BUILD)`, que només es dispara quan el binari es relinkeja. Si s'editava només un YAML o un GIF sense tocar codi, `build/data/` quedava obsolet. Ara és un `add_custom_target(sync_data ALL ...)` amb dependència del target principal — els custom targets sense BYPRODUCTS es consideren sempre out-of-date, així que la còpia s'executa a cada `cmake --build`.
|
||||||
|
|
||||||
|
### Refactors de suport
|
||||||
|
|
||||||
|
- `Mummy::step` partit: `snapToPlayfieldEdge`, `collectDirectionPools`, `applyChoice` — per mantenir la complexitat cognitiva sota el llindar de clang-tidy (< 25).
|
||||||
|
|
||||||
|
## 8. Estat global
|
||||||
|
|
||||||
|
- **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ó.
|
||||||
|
- **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).
|
||||||
|
- **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.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Animacions i direccionalitat del jugador
|
||||||
|
|
||||||
|
Documenta els afegits d'aquesta sessió a `source/game/entities/player.*`: sis
|
||||||
|
animacions direccionals amb `facing_` persistent, regla de flip només per al
|
||||||
|
lateral i transició automàtica a `stand` quan la col·lisió bloqueja el
|
||||||
|
moviment. Tot el canvi és local al `Player`; no s'ha tocat l'EntityManager
|
||||||
|
ni el bus.
|
||||||
|
|
||||||
|
## 1. YAML — sis animacions
|
||||||
|
|
||||||
|
El fitxer `data/player/player.yaml` defineix ara sis animacions, agrupades
|
||||||
|
en tres files de 4 frames sobre `player.gif` (convenció habitual: d'esquerra
|
||||||
|
a dreta i de dalt a baix):
|
||||||
|
|
||||||
|
| Nom | Frames | Descripció |
|
||||||
|
|---------------|------------|----------------------------|
|
||||||
|
| `stand_down` | `[0]` | parat mirant cap avall |
|
||||||
|
| `walk_down` | `[0..3]` | caminant cap avall |
|
||||||
|
| `stand_up` | `[4]` | parat mirant cap amunt |
|
||||||
|
| `walk_up` | `[4..7]` | caminant cap amunt |
|
||||||
|
| `stand` | `[8]` | parat lateral |
|
||||||
|
| `walk` | `[8..11]` | caminant lateral |
|
||||||
|
|
||||||
|
Les laterals (`stand` i `walk`) ja existien; les variants `_up` i `_down`
|
||||||
|
són noves i permeten distingir visualment la direcció de moviment.
|
||||||
|
|
||||||
|
## 2. `Facing` persistent
|
||||||
|
|
||||||
|
S'ha afegit l'enum privat `Facing { SIDE, UP, DOWN }` amb el membre
|
||||||
|
`facing_{SIDE}`. La tria es fa a cada `update`, amb prevalença lateral
|
||||||
|
per si un dia es permetessin diagonals:
|
||||||
|
|
||||||
|
- `vx != 0` → `SIDE` (inclou diagonals).
|
||||||
|
- `vx == 0 && vy < 0` → `UP`.
|
||||||
|
- `vx == 0 && vy > 0` → `DOWN`.
|
||||||
|
- Cap input → es conserva l'últim `facing_` perquè l'animació `stand_*`
|
||||||
|
coincideixi amb l'última direcció caminada.
|
||||||
|
|
||||||
|
## 3. Regla de flip
|
||||||
|
|
||||||
|
- `SIDE`: el sprite es flipeja horitzontalment si `vx < 0`, i es restaura
|
||||||
|
a `FLIP_NONE` si `vx > 0`. El sprite original mira a la dreta.
|
||||||
|
- `UP` / `DOWN`: el flip es força explícitament a `FLIP_NONE`. Els sprites
|
||||||
|
frontals/dorsals no s'han d'espillar només perquè l'últim lateral havia
|
||||||
|
estat cap a l'esquerra.
|
||||||
|
|
||||||
|
## 4. `stand` automàtic a la col·lisió
|
||||||
|
|
||||||
|
Quan el jugador prem una tecla però la col·lisió (mapa o sarcòfag) li
|
||||||
|
impedeix avançar, l'animació `walk*` s'atura i es força la variant
|
||||||
|
`stand_*` corresponent al facing. L'algorisme:
|
||||||
|
|
||||||
|
1. Al principi del frame es captura `FRAME_START_X` / `FRAME_START_Y`.
|
||||||
|
2. S'aplica snap al passadís perpendicular si hi ha pur moviment H o V.
|
||||||
|
3. `animate(delta_time)` avança el frame de l'animació actual sense
|
||||||
|
tocar la posició.
|
||||||
|
4. `tryMove(vx*dt, vy*dt, collidesFn)` (a `MovingSprite`) aplica el
|
||||||
|
moviment per eix: si la `collisionBox()` resultant col·lisiona —
|
||||||
|
el Player la sobreescriu retornant `getBaseBox()` —, l'eix es
|
||||||
|
reverteix. Així el jugador llisca contra murs/sarcòfags en lloc de
|
||||||
|
bloquejar-se en sec.
|
||||||
|
5. Després de `clampToPlayfield`, es compara la posició final amb el
|
||||||
|
valor capturat al pas 1. Si és idèntica, `MOVED = false` i es tria
|
||||||
|
`stand` / `stand_up` / `stand_down` segons `facing_`. Si hi ha
|
||||||
|
hagut moviment real (incloent-hi snap), s'aplica la variant
|
||||||
|
`walk*` del mateix facing.
|
||||||
|
|
||||||
|
D'aquesta manera, xocar contra un mur o un sarcòfag fa que el jugador es
|
||||||
|
quede immòbil en compte d'animar el caminat "en el lloc". El facing
|
||||||
|
segueix reflectint la intenció (on vol anar), no el resultat, perquè
|
||||||
|
l'`stand_up` d'empentar-se contra un mur ha de mostrar el personatge
|
||||||
|
mirant amunt.
|
||||||
|
|
||||||
|
## 5. Estat
|
||||||
|
|
||||||
|
- DONE: sis animacions al YAML, `facing_` persistent, flip condicionat,
|
||||||
|
`stand` automàtic a la col·lisió.
|
||||||
|
- Build i smoke tests (13/13) verdes. (Aquesta nota deia "14/14" abans;
|
||||||
|
el comptatge real al `tests/test_entities_smoke.cpp` són 13 funcions
|
||||||
|
`test*` invocades des de `main()` — corregit a `refactor/options-class`.)
|
||||||
|
- Sense smoke test dedicat: la lògica està entrellaçada amb teclat i
|
||||||
|
`Map::collides`; l'smoke actual de `Player` no cobreix teclat simulat.
|
||||||
|
- Pendent (ajornat): si algun dia es vol feedback auditiu de wall-bump
|
||||||
|
(so en xocar), la condició natural és `!MOVED && (vx != 0 || vy != 0)`
|
||||||
|
al final d'`update`.
|
||||||
@@ -42,83 +42,86 @@ namespace GlobalInputs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onDecZoom() {
|
void onDecZoom() {
|
||||||
if (Options::window.fullscreen) { return; }
|
if (Options::window().fullscreen) { return; }
|
||||||
if (Options::window.zoom <= MIN_ZOOM) { return; }
|
if (Options::window().zoom <= MIN_ZOOM) { return; }
|
||||||
Options::window.zoom--;
|
const int NEW_ZOOM = Options::window().zoom - 1;
|
||||||
Screen::get().setWindowZoom(Options::window.zoom);
|
Options::setWindowZoom(NEW_ZOOM);
|
||||||
Options::saveToFile();
|
Screen::get().setWindowZoom(NEW_ZOOM);
|
||||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom));
|
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(NEW_ZOOM));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onIncZoom() {
|
void onIncZoom() {
|
||||||
if (Options::window.fullscreen) { return; }
|
if (Options::window().fullscreen) { return; }
|
||||||
if (Options::window.zoom >= MAX_ZOOM) { return; }
|
if (Options::window().zoom >= MAX_ZOOM) { return; }
|
||||||
Options::window.zoom++;
|
const int NEW_ZOOM = Options::window().zoom + 1;
|
||||||
Screen::get().setWindowZoom(Options::window.zoom);
|
Options::setWindowZoom(NEW_ZOOM);
|
||||||
Options::saveToFile();
|
Screen::get().setWindowZoom(NEW_ZOOM);
|
||||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom));
|
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(NEW_ZOOM));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFullscreen() {
|
void onFullscreen() {
|
||||||
Options::window.fullscreen = !Options::window.fullscreen;
|
const bool NEW_FS = !Options::window().fullscreen;
|
||||||
Screen::get().setFullscreen(Options::window.fullscreen);
|
Options::setWindowFullscreen(NEW_FS);
|
||||||
Options::saveToFile();
|
Screen::get().setFullscreen(NEW_FS);
|
||||||
notifyLocale(Options::window.fullscreen ? "input.fullscreen_on" : "input.fullscreen_off");
|
notifyLocale(NEW_FS ? "input.fullscreen_on" : "input.fullscreen_off");
|
||||||
}
|
}
|
||||||
|
|
||||||
void onToggleVSync() {
|
void onToggleVSync() {
|
||||||
Options::video.vsync = !Options::video.vsync;
|
const bool NEW_VSYNC = !Options::video().vsync;
|
||||||
Screen::get().setVSync(Options::video.vsync);
|
Options::setVideoVSync(NEW_VSYNC);
|
||||||
Options::saveToFile();
|
Screen::get().setVSync(NEW_VSYNC);
|
||||||
notifyLocale(Options::video.vsync ? "input.vsync_on" : "input.vsync_off");
|
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
|
||||||
}
|
}
|
||||||
|
|
||||||
void onToggleShader() {
|
void onToggleShader() {
|
||||||
Options::video.shader.enabled = !Options::video.shader.enabled;
|
const bool NEW_ENABLED = !Options::video().shader.enabled;
|
||||||
|
Options::setShaderEnabled(NEW_ENABLED);
|
||||||
reapplyShader();
|
reapplyShader();
|
||||||
Options::saveToFile();
|
notifyLocale(NEW_ENABLED ? "input.shader_on" : "input.shader_off");
|
||||||
notifyLocale(Options::video.shader.enabled ? "input.shader_on" : "input.shader_off");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onNextShader() {
|
void onNextShader() {
|
||||||
Options::video.shader.current_shader = (Options::video.shader.current_shader == "crtpi") ? "postfx" : "crtpi";
|
const std::string NEW_NAME = (Options::video().shader.current_shader == "crtpi") ? "postfx" : "crtpi";
|
||||||
|
Options::setActiveShaderName(NEW_NAME);
|
||||||
reapplyShader();
|
reapplyShader();
|
||||||
Options::saveToFile();
|
Notification::notify(Locale::get().text("input.shader") + ": " + NEW_NAME);
|
||||||
Notification::notify(Locale::get().text("input.shader") + ": " + Options::video.shader.current_shader);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca l'entrada actual al vector de presets i passa a la següent
|
// Busca l'entrada actual al vector de presets i passa a la següent
|
||||||
// (wrap-around). Si no en troba cap s'evita el canvi silenciosament.
|
// (wrap-around). Si no en troba cap s'evita el canvi silenciosament.
|
||||||
void onNextPreset() {
|
void onNextPreset() {
|
||||||
const bool IS_CRTPI = Options::video.shader.current_shader == "crtpi";
|
const bool IS_CRTPI = Options::video().shader.current_shader == "crtpi";
|
||||||
|
std::string new_preset;
|
||||||
if (IS_CRTPI) {
|
if (IS_CRTPI) {
|
||||||
if (Options::crtpi_presets.empty()) { return; }
|
const auto& presets = Options::crtpiPresets();
|
||||||
auto it = std::ranges::find_if(Options::crtpi_presets,
|
if (presets.empty()) { return; }
|
||||||
[](const auto& preset) { return preset.name == Options::video.shader.current_crtpi_preset; });
|
auto it = std::ranges::find_if(presets,
|
||||||
std::size_t idx = (it == Options::crtpi_presets.end()) ? 0 : std::distance(Options::crtpi_presets.begin(), it);
|
[](const auto& preset) { return preset.name == Options::video().shader.current_crtpi_preset; });
|
||||||
idx = (idx + 1) % Options::crtpi_presets.size();
|
std::size_t idx = (it == presets.end()) ? 0 : std::distance(presets.begin(), it);
|
||||||
Options::video.shader.current_crtpi_preset = Options::crtpi_presets[idx].name;
|
idx = (idx + 1) % presets.size();
|
||||||
|
new_preset = presets[idx].name;
|
||||||
|
Options::setCrtPiPreset(new_preset);
|
||||||
} else {
|
} else {
|
||||||
if (Options::postfx_presets.empty()) { return; }
|
const auto& presets = Options::postfxPresets();
|
||||||
auto it = std::ranges::find_if(Options::postfx_presets,
|
if (presets.empty()) { return; }
|
||||||
[](const auto& preset) { return preset.name == Options::video.shader.current_postfx_preset; });
|
auto it = std::ranges::find_if(presets,
|
||||||
std::size_t idx = (it == Options::postfx_presets.end()) ? 0 : std::distance(Options::postfx_presets.begin(), it);
|
[](const auto& preset) { return preset.name == Options::video().shader.current_postfx_preset; });
|
||||||
idx = (idx + 1) % Options::postfx_presets.size();
|
std::size_t idx = (it == presets.end()) ? 0 : std::distance(presets.begin(), it);
|
||||||
Options::video.shader.current_postfx_preset = Options::postfx_presets[idx].name;
|
idx = (idx + 1) % presets.size();
|
||||||
|
new_preset = presets[idx].name;
|
||||||
|
Options::setPostFXPreset(new_preset);
|
||||||
}
|
}
|
||||||
reapplyShader();
|
reapplyShader();
|
||||||
Options::saveToFile();
|
Notification::notify(Locale::get().text("input.preset") + ": " + new_preset);
|
||||||
const std::string& name = IS_CRTPI ? Options::video.shader.current_crtpi_preset : Options::video.shader.current_postfx_preset;
|
|
||||||
Notification::notify(Locale::get().text("input.preset") + ": " + name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onNextScalingMode() {
|
void onNextScalingMode() {
|
||||||
using Mode = Defaults::ScalingMode;
|
using Mode = Defaults::ScalingMode;
|
||||||
Options::video.scaling_mode = static_cast<Mode>((static_cast<int>(Options::video.scaling_mode) + 1) % 5);
|
const auto NEW_MODE = static_cast<Mode>((static_cast<int>(Options::video().scaling_mode) + 1) % 5);
|
||||||
Screen::get().setScalingMode(Options::video.scaling_mode);
|
Options::setVideoScalingMode(NEW_MODE);
|
||||||
Options::saveToFile();
|
Screen::get().setScalingMode(NEW_MODE);
|
||||||
const char* mode_key = nullptr;
|
const char* mode_key = nullptr;
|
||||||
switch (Options::video.scaling_mode) {
|
switch (NEW_MODE) {
|
||||||
case Mode::DISABLED:
|
case Mode::DISABLED:
|
||||||
mode_key = "input.scaling_disabled";
|
mode_key = "input.scaling_disabled";
|
||||||
break;
|
break;
|
||||||
@@ -139,14 +142,12 @@ namespace GlobalInputs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onCycleRenderInfo() {
|
void onCycleRenderInfo() {
|
||||||
const int NEXT = (static_cast<int>(Options::video.render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT;
|
const int NEXT = (static_cast<int>(Options::video().render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT;
|
||||||
Options::video.render_info_position = static_cast<Defaults::RenderInfoPosition>(NEXT);
|
Options::setVideoRenderInfoPosition(static_cast<Defaults::RenderInfoPosition>(NEXT));
|
||||||
Options::saveToFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onToggleRenderTime() {
|
void onToggleRenderTime() {
|
||||||
Options::video.render_info_show_time = !Options::video.render_info_show_time;
|
Options::setVideoRenderInfoShowTime(!Options::video().render_info_show_time);
|
||||||
Options::saveToFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onExit() {
|
void onExit() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
auto KeyConfig::get() -> KeyConfig& {
|
auto KeyConfig::get() -> KeyConfig& {
|
||||||
@@ -12,16 +13,17 @@ auto KeyConfig::get() -> KeyConfig& {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void KeyConfig::load(const std::string& file_path) {
|
void KeyConfig::load(const std::string& file_path) {
|
||||||
std::ifstream file(file_path);
|
const auto FULL = Resource::Helper::resolve(file_path);
|
||||||
|
std::ifstream file(FULL);
|
||||||
if (!file.is_open()) {
|
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);
|
auto yaml = fkyaml::node::deserialize(file);
|
||||||
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
|
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
|
||||||
std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n';
|
std::cerr << "KeyConfig: falta la secció 'keys' a " << FULL << '\n';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bindings_.clear();
|
bindings_.clear();
|
||||||
@@ -36,9 +38,9 @@ void KeyConfig::load(const std::string& file_path) {
|
|||||||
}
|
}
|
||||||
bindings_[ID] = SC;
|
bindings_[ID] = SC;
|
||||||
}
|
}
|
||||||
std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << file_path << '\n';
|
std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << FULL << '\n';
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "KeyConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
std::cerr << "KeyConfig: error parsejant " << FULL << ": " << e.what() << '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class KeyConfig {
|
|||||||
void load(const std::string& file_path);
|
void load(const std::string& file_path);
|
||||||
|
|
||||||
[[nodiscard]] auto scancode(const std::string& id) const -> SDL_Scancode;
|
[[nodiscard]] auto scancode(const std::string& id) const -> SDL_Scancode;
|
||||||
|
[[nodiscard]] auto bindings() const -> const std::unordered_map<std::string, SDL_Scancode>& { return bindings_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
KeyConfig() = default;
|
KeyConfig() = default;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -33,9 +34,10 @@ auto Locale::get() -> Locale& {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Locale::load(const std::string& file_path) {
|
void Locale::load(const std::string& file_path) {
|
||||||
std::ifstream file(file_path);
|
const auto FULL = Resource::Helper::resolve(file_path);
|
||||||
|
std::ifstream file(FULL);
|
||||||
if (!file.is_open()) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,9 +45,9 @@ void Locale::load(const std::string& file_path) {
|
|||||||
auto yaml = fkyaml::node::deserialize(file);
|
auto yaml = fkyaml::node::deserialize(file);
|
||||||
strings_.clear();
|
strings_.clear();
|
||||||
flatten(yaml, "", strings_);
|
flatten(yaml, "", strings_);
|
||||||
std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n';
|
std::cout << "Locale: " << strings_.size() << " traduccions des de " << FULL << '\n';
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "Locale: error parsejant " << file_path << ": " << e.what() << '\n';
|
std::cerr << "Locale: error parsejant " << FULL << ": " << e.what() << '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -198,10 +198,33 @@ namespace GIF {
|
|||||||
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
|
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
|
||||||
|
|
||||||
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
|
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
|
||||||
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
|
const int IMG_W = image_descriptor.image_width;
|
||||||
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
|
const int IMG_H = image_descriptor.image_height;
|
||||||
|
std::vector<uint8_t> uncompressed_data(static_cast<std::size_t>(IMG_W) * IMG_H);
|
||||||
|
|
||||||
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
|
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
|
||||||
|
|
||||||
|
// Interlaced GIF (flag 0x40 al byte de fields): les files no arriben
|
||||||
|
// en ordre lineal, sinó en 4 passades (8:0, 8:4, 4:2, 2:1). Reordena
|
||||||
|
// al buffer lineal que espera Surface. Sense aquest pas, les fuentes
|
||||||
|
// i qualsevol GIF entrellaçat es veien completament trinxats.
|
||||||
|
constexpr uint8_t INTERLACE_FLAG = 0x40;
|
||||||
|
if ((image_descriptor.fields & INTERLACE_FLAG) != 0) {
|
||||||
|
constexpr int PASS_START[4] = {0, 4, 2, 1};
|
||||||
|
constexpr int PASS_STEP[4] = {8, 8, 4, 2};
|
||||||
|
std::vector<uint8_t> deinterlaced(static_cast<std::size_t>(IMG_W) * IMG_H);
|
||||||
|
int src_row = 0;
|
||||||
|
for (int pass = 0; pass < 4; ++pass) {
|
||||||
|
for (int dst_row = PASS_START[pass]; dst_row < IMG_H; dst_row += PASS_STEP[pass]) {
|
||||||
|
std::memcpy(deinterlaced.data() + (static_cast<std::size_t>(dst_row) * IMG_W),
|
||||||
|
uncompressed_data.data() + (static_cast<std::size_t>(src_row) * IMG_W),
|
||||||
|
IMG_W);
|
||||||
|
++src_row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deinterlaced;
|
||||||
|
}
|
||||||
|
|
||||||
return uncompressed_data;
|
return uncompressed_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,3 +72,12 @@ void MovingSprite::setPosY(float value) {
|
|||||||
y_ = value;
|
y_ = value;
|
||||||
pos_.y = static_cast<int>(y_);
|
pos_.y = static_cast<int>(y_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MovingSprite::tryMove(float dx, float dy, const CollisionFn& collides) {
|
||||||
|
const float OLD_X = getPosX();
|
||||||
|
setPosX(OLD_X + dx);
|
||||||
|
if (collides(collisionBox())) { setPosX(OLD_X); }
|
||||||
|
const float OLD_Y = getPosY();
|
||||||
|
setPosY(OLD_Y + dy);
|
||||||
|
if (collides(collisionBox())) { setPosY(OLD_Y); }
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "core/rendering/sprite/sprite.hpp"
|
#include "core/rendering/sprite/sprite.hpp"
|
||||||
@@ -40,6 +41,15 @@ class MovingSprite : public Sprite {
|
|||||||
auto getFlip() -> SDL_FlipMode { return flip_; }
|
auto getFlip() -> SDL_FlipMode { return flip_; }
|
||||||
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; }
|
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; }
|
||||||
|
|
||||||
|
// Moviment eix-separat amb col·lisió: aplica `dx` (i si la nova
|
||||||
|
// `collisionBox()` col·lisiona, el reverteix); després aplica `dy`
|
||||||
|
// (idem). El caller calcula els deltes (típicament `vx_*dt`,
|
||||||
|
// `vy_*dt`) i passa la funció de detecció. Aquesta separació deixa
|
||||||
|
// que el sprite "llisqui" contra obstacles en compte de quedar-se
|
||||||
|
// bloquejat en sec quan només un eix col·lisiona.
|
||||||
|
using CollisionFn = std::function<bool(const SDL_FRect&)>;
|
||||||
|
void tryMove(float dx, float dy, const CollisionFn& collides);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void move(float delta_time);
|
void move(float delta_time);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ class Sprite {
|
|||||||
// Hook d'actualització per frame. Base buida (no hi ha Sprite pur a
|
// Hook d'actualització per frame. Base buida (no hi ha Sprite pur a
|
||||||
// actualitzar); les subclasses mòbils/animades fan override.
|
// actualitzar); les subclasses mòbils/animades fan override.
|
||||||
virtual void update(float /*delta_time*/) {}
|
virtual void update(float /*delta_time*/) {}
|
||||||
|
|
||||||
|
// Caixa que `MovingSprite::tryMove` envia al callback de col·lisió per
|
||||||
|
// decidir si el moviment és vàlid. Per defecte coincideix amb `pos_`
|
||||||
|
// (el sprite sencer); les subclasses que usen falsa perspectiva (p.ex.
|
||||||
|
// `Player`) la sobreescriuen amb una sub-caixa més petita (només els
|
||||||
|
// peus) per a que el cap puga sobresortir sense bloquejar el
|
||||||
|
// moviment.
|
||||||
|
[[nodiscard]] virtual auto collisionBox() const -> SDL_FRect { return pos_; }
|
||||||
|
|
||||||
virtual void render();
|
virtual void render();
|
||||||
// Renderitza substituint l'índex source_color per target_color (ús típic:
|
// Renderitza substituint l'índex source_color per target_color (ús típic:
|
||||||
// la classe Text, que recolora glifs monocroms per paràmetre).
|
// la classe Text, que recolora glifs monocroms per paràmetre).
|
||||||
|
|||||||
@@ -2,20 +2,41 @@
|
|||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace Resource::Helper {
|
namespace Resource::Helper {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string g_data_root;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
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> {
|
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t> {
|
||||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
const auto FULL = resolve(filepath);
|
||||||
|
std::ifstream file(FULL, std::ios::binary | std::ios::ate);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
std::cerr << "Resource::Helper: cannot open " << filepath << '\n';
|
std::cerr << "Resource::Helper: cannot open " << FULL << '\n';
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::streamsize size = file.tellg();
|
std::streamsize size = file.tellg();
|
||||||
file.seekg(0, std::ios::beg);
|
file.seekg(0, std::ios::beg);
|
||||||
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
|
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
|
||||||
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
||||||
std::cerr << "Resource::Helper: failed to read " << filepath << '\n';
|
std::cerr << "Resource::Helper: failed to read " << FULL << '\n';
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -6,7 +6,23 @@
|
|||||||
|
|
||||||
namespace Resource::Helper {
|
namespace Resource::Helper {
|
||||||
|
|
||||||
|
// Estableix el prefix absolut des d'on es resolen totes les rutes
|
||||||
|
// relatives passades a `loadFile`/`resolve`. Pensat per a cridar-se una
|
||||||
|
// sola vegada al primer pas del Director amb `SDL_GetBasePath()`: així el
|
||||||
|
// binari troba `data/` sense dependre del CWD des d'on s'ha llançat (és
|
||||||
|
// obligatori per a .app de macOS i útil a Linux per no haver d'estar
|
||||||
|
// dins de `build/`).
|
||||||
|
void setDataRoot(const std::string& absolute_path);
|
||||||
|
|
||||||
|
// Resol una ruta relativa contra el data root. Si la ruta ja és absoluta
|
||||||
|
// (comença per '/') la retorna sense modificar — així els fitxers de
|
||||||
|
// config (system_folder_ + "config.yaml") segueixen funcionant sense
|
||||||
|
// canvis al call site.
|
||||||
|
[[nodiscard]] auto resolve(const std::string& filepath) -> std::string;
|
||||||
|
|
||||||
// Carga un fitxer del filesystem (a aquesta fase encara no hi ha pack).
|
// Carga un fitxer del filesystem (a aquesta fase encara no hi ha pack).
|
||||||
|
// Les rutes relatives es resolen via `resolve()`; les absolutes s'obrin
|
||||||
|
// directament.
|
||||||
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
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
@@ -19,10 +20,15 @@
|
|||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/rendering/text.hpp"
|
#include "core/rendering/text.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.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 "game/debug_config.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/scenes/game_scene.hpp"
|
#include "game/scenes/game_scene.hpp"
|
||||||
|
#include "game/scenes/intro_scene.hpp"
|
||||||
|
#include "game/scenes/logo_scene.hpp"
|
||||||
|
#include "game/scenes/title_scene.hpp"
|
||||||
#include "game/ui/ui_palette.hpp"
|
#include "game/ui/ui_palette.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
@@ -31,6 +37,15 @@ 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
|
||||||
|
// retorna la carpeta del binari al desplegament portable i Contents/
|
||||||
|
// Resources/ dins d'un .app a macOS. Ha de fer-se abans de qualsevol
|
||||||
|
// loader que passi per Resource::Helper::resolve (Locale, KeyConfig,
|
||||||
|
// Resource::Cache, PaletteLoader, etc.).
|
||||||
|
if (const char* base = SDL_GetBasePath()) {
|
||||||
|
Resource::Helper::setDataRoot(base);
|
||||||
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
@@ -61,16 +76,21 @@ Director::Director() {
|
|||||||
|
|
||||||
// Traduccions per a tots els textos visibles. Si el fitxer no existeix
|
// Traduccions per a tots els textos visibles. Si el fitxer no existeix
|
||||||
// o hi ha un error de parse, Locale::text() caurà a retornar la clau.
|
// o hi ha un error de parse, Locale::text() caurà a retornar la clau.
|
||||||
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale + ".yaml");
|
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml");
|
||||||
|
|
||||||
// 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");
|
||||||
|
|
||||||
// Recursos compartits d'UI (paleta + font). 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.
|
// 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).
|
||||||
Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal");
|
Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal");
|
||||||
Resource::Cache::get().loadSurface("8bithud.gif", "data/font/8bithud.gif");
|
const std::string HUD_FONT_GIF_KEY = std::string(Defaults::UI::Font::HUD_NAME) + ".gif";
|
||||||
Resource::Cache::get().getSurface("8bithud.gif")->setPalette(Resource::Cache::get().getPalette("ui"));
|
Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, Defaults::UI::Font::HUD_GIF);
|
||||||
|
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";
|
||||||
|
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, Defaults::UI::Font::PLACEHOLDER_GIF);
|
||||||
|
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í
|
||||||
// perquè forma part del preload d'assets, com la font i la paleta.
|
// perquè forma part del preload d'assets, com la font i la paleta.
|
||||||
@@ -84,14 +104,58 @@ Director::Director() {
|
|||||||
// 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("8bithud.gif");
|
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||||||
auto hud_text = std::make_shared<Text>(hud_font_surface, "data/font/8bithud.fnt");
|
auto hud_text = std::make_shared<Text>(hud_font_surface, Defaults::UI::Font::HUD_FNT);
|
||||||
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(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW), hud_text);
|
||||||
Notifier::get().init(hud_text, ui_palette);
|
Notifier::get().init(hud_text, ui_palette);
|
||||||
|
|
||||||
current_scene_ = std::make_unique<GameScene>();
|
// Font dels placeholders (LOGO / TITLE / INTRO). És una font distinta a
|
||||||
|
// la de HUD perquè els placeholders desapareixeran amb les seues escenes
|
||||||
|
// finals; mentrestant tenen estètica pròpia.
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Flux d'escenes LOGO → TITLE → INTRO → GAME. Cada placeholder rep una
|
||||||
|
// factory que construeix la següent, construïda bottom-up des de GAME.
|
||||||
|
//
|
||||||
|
// Release: sempre s'arranca a LOGO (el flux complet, el que veu el
|
||||||
|
// jugador final). Debug: s'obri `debug.yaml`; si no existeix, el creem
|
||||||
|
// amb `start_scene: GAME` perquè el dia a dia de gameplay no es mengi
|
||||||
|
// els 15 s de placeholders. L'usuari pot editar-lo a mà per escollir
|
||||||
|
// un punt d'entrada intermedi.
|
||||||
|
#ifdef _DEBUG
|
||||||
|
const auto DEBUG_YAML = system_folder_ + "debug.yaml";
|
||||||
|
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
||||||
|
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
||||||
|
#else
|
||||||
|
const auto START = DebugConfig::StartScene::LOGO;
|
||||||
|
#endif
|
||||||
|
std::cout << "Director: start_scene=" << DebugConfig::toString(START) << '\n';
|
||||||
|
|
||||||
|
auto make_game = []() -> std::unique_ptr<Scene> { return std::make_unique<GameScene>(); };
|
||||||
|
auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr<Scene> {
|
||||||
|
return std::make_unique<IntroScene>(placeholder_text, make_game);
|
||||||
|
};
|
||||||
|
auto make_title = [placeholder_text, make_intro]() -> std::unique_ptr<Scene> {
|
||||||
|
return std::make_unique<TitleScene>(placeholder_text, make_intro);
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (START) {
|
||||||
|
case DebugConfig::StartScene::LOGO:
|
||||||
|
current_scene_ = std::make_unique<LogoScene>(placeholder_text, make_title);
|
||||||
|
break;
|
||||||
|
case DebugConfig::StartScene::TITLE:
|
||||||
|
current_scene_ = make_title();
|
||||||
|
break;
|
||||||
|
case DebugConfig::StartScene::INTRO:
|
||||||
|
current_scene_ = make_intro();
|
||||||
|
break;
|
||||||
|
case DebugConfig::StartScene::GAME:
|
||||||
|
current_scene_ = make_game();
|
||||||
|
break;
|
||||||
|
}
|
||||||
last_ticks_ms_ = SDL_GetTicks();
|
last_ticks_ms_ = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +207,8 @@ void Director::processEvents() {
|
|||||||
void Director::switchSceneIfRequested() {
|
void Director::switchSceneIfRequested() {
|
||||||
if (!current_scene_) { return; }
|
if (!current_scene_) { return; }
|
||||||
if (auto next = current_scene_->nextScene()) {
|
if (auto next = current_scene_->nextScene()) {
|
||||||
|
// TODO: migrar a logger unificat (INC-03 a docs/audit-core-rendering.md).
|
||||||
|
std::cout << "Director: scene transition\n";
|
||||||
current_scene_ = std::move(next);
|
current_scene_ = std::move(next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#include "game/debug_config.hpp"
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
namespace DebugConfig {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto readFile(const std::string& path) -> std::string {
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file) { return {}; }
|
||||||
|
std::stringstream buffer;
|
||||||
|
buffer << file.rdbuf();
|
||||||
|
return buffer.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fromName(const std::string& name) -> StartScene {
|
||||||
|
if (name == "LOGO") { return StartScene::LOGO; }
|
||||||
|
if (name == "TITLE") { return StartScene::TITLE; }
|
||||||
|
if (name == "INTRO") { return StartScene::INTRO; }
|
||||||
|
if (name == "GAME") { return StartScene::GAME; }
|
||||||
|
return StartScene::GAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isKnown(const std::string& name) -> bool {
|
||||||
|
return name == "LOGO" || name == "TITLE" || name == "INTRO" || name == "GAME";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto parseStartScene(const std::string& yaml_content) -> StartScene {
|
||||||
|
if (yaml_content.empty()) { return StartScene::GAME; }
|
||||||
|
try {
|
||||||
|
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||||
|
if (!yaml.contains("start_scene")) { return StartScene::GAME; }
|
||||||
|
const auto RAW = yaml["start_scene"].get_value<std::string>();
|
||||||
|
if (!isKnown(RAW)) {
|
||||||
|
std::cerr << "debug.yaml: start_scene \"" << RAW << "\" invàlid, arrancant a GAME\n";
|
||||||
|
return StartScene::GAME;
|
||||||
|
}
|
||||||
|
return fromName(RAW);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "debug.yaml: error parsejant: " << e.what() << " — arrancant a GAME\n";
|
||||||
|
return StartScene::GAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto resolveStartScene(const std::string& file_path) -> StartScene {
|
||||||
|
std::error_code error;
|
||||||
|
if (!std::filesystem::exists(file_path, error)) {
|
||||||
|
// Silenciós: la no-existència és el cas per defecte en una màquina
|
||||||
|
// neta, no un error. L'usuari crea el fitxer quan el necessita.
|
||||||
|
return StartScene::GAME;
|
||||||
|
}
|
||||||
|
const auto CONTENT = readFile(file_path);
|
||||||
|
if (CONTENT.empty()) {
|
||||||
|
std::cerr << "debug.yaml: fitxer buit a " << file_path << ", arrancant a GAME\n";
|
||||||
|
return StartScene::GAME;
|
||||||
|
}
|
||||||
|
return parseStartScene(CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto toString(StartScene scene) -> const char* {
|
||||||
|
switch (scene) {
|
||||||
|
case StartScene::LOGO:
|
||||||
|
return "LOGO";
|
||||||
|
case StartScene::TITLE:
|
||||||
|
return "TITLE";
|
||||||
|
case StartScene::INTRO:
|
||||||
|
return "INTRO";
|
||||||
|
case StartScene::GAME:
|
||||||
|
return "GAME";
|
||||||
|
}
|
||||||
|
return "GAME";
|
||||||
|
}
|
||||||
|
|
||||||
|
void createDefaultIfMissing(const std::string& file_path) {
|
||||||
|
std::error_code error;
|
||||||
|
if (std::filesystem::exists(file_path, error)) { return; }
|
||||||
|
std::ofstream file(file_path);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "debug.yaml: no s'ha pogut crear " << file_path << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
||||||
|
file << "# Valors vàlids: LOGO, TITLE, INTRO, GAME.\n";
|
||||||
|
file << "start_scene: GAME\n";
|
||||||
|
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace DebugConfig
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
|
||||||
|
// carpeta de sistema (`$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml`). No es
|
||||||
|
// versiona: cada dev el crea a la seua màquina. Si no existeix, s'aplica el
|
||||||
|
// valor per defecte (GAME) sense warning de nivell error.
|
||||||
|
namespace DebugConfig {
|
||||||
|
|
||||||
|
enum class StartScene : std::uint8_t { LOGO,
|
||||||
|
TITLE,
|
||||||
|
INTRO,
|
||||||
|
GAME };
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
[[nodiscard]] auto parseStartScene(const std::string& yaml_content) -> StartScene;
|
||||||
|
|
||||||
|
// Llig el fitxer i delega a parseStartScene. Si el fitxer no existeix
|
||||||
|
// retorna GAME sense warning (és el cas per defecte en una màquina neta);
|
||||||
|
// si existeix però el valor és invàlid, imprimeix un warning i retorna
|
||||||
|
// GAME.
|
||||||
|
[[nodiscard]] auto resolveStartScene(const std::string& file_path) -> StartScene;
|
||||||
|
|
||||||
|
// Nom canònic de l'enum (útil per a logs): "LOGO" / "TITLE" / "INTRO" / "GAME".
|
||||||
|
[[nodiscard]] auto toString(StartScene scene) -> const char*;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// toca — preservem qualsevol edició manual de l'usuari. Usat només al
|
||||||
|
// path de debug del Director (el binari release ignora `debug.yaml`).
|
||||||
|
void createDefaultIfMissing(const std::string& file_path);
|
||||||
|
|
||||||
|
} // namespace DebugConfig
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
#include "game/entities/concrete/mummy.hpp"
|
#include "game/entities/concrete/mummy.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#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/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -17,9 +21,36 @@ namespace {
|
|||||||
return k_rng_;
|
return k_rng_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
|
||||||
|
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una
|
||||||
|
// animació d'1 frame loopant, neutra — la momia es podrà moure però
|
||||||
|
// visualment es quedarà estàtica.
|
||||||
|
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||||
|
std::vector<int> frames = {0};
|
||||||
|
float fps = 8.0F;
|
||||||
|
bool loop = true;
|
||||||
|
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
|
} // namespace
|
||||||
|
|
||||||
Mummy::Mummy(const fkyaml::node& yaml) {
|
Mummy::Mummy(const fkyaml::node& yaml)
|
||||||
|
: anim_walk_down_(buildAnimation(yaml, "walk_down")),
|
||||||
|
anim_walk_up_(buildAnimation(yaml, "walk_up")),
|
||||||
|
anim_walk_lateral_(buildAnimation(yaml, "walk")) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
||||||
collides_ = true;
|
collides_ = true;
|
||||||
interactable_ = false;
|
interactable_ = false;
|
||||||
@@ -30,6 +61,44 @@ Mummy::Mummy(const fkyaml::node& yaml) {
|
|||||||
if (s.contains("speed")) { setSpeed(s["speed"].get_value<float>()); }
|
if (s.contains("speed")) { setSpeed(s["speed"].get_value<float>()); }
|
||||||
if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); }
|
if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direcció inicial llegida del bloc `ai:`. Per defecte dreta, per
|
||||||
|
// coherència amb el que fa RollingRock. Valors acceptats: right, left,
|
||||||
|
// up, down. Qualsevol altre s'interpreta com a "sense direcció" (la
|
||||||
|
// momia no es mou fins que step() no triï una direcció).
|
||||||
|
std::string initial = "right";
|
||||||
|
if (yaml.contains("ai") && yaml["ai"].contains("initial_direction")) {
|
||||||
|
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
if (initial == "right") {
|
||||||
|
changeDirection(1.0F, 0.0F);
|
||||||
|
} else if (initial == "left") {
|
||||||
|
changeDirection(-1.0F, 0.0F);
|
||||||
|
} else if (initial == "up") {
|
||||||
|
changeDirection(0.0F, -1.0F);
|
||||||
|
} else if (initial == "down") {
|
||||||
|
changeDirection(0.0F, 1.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Mummy::changeDirection(float dx, float dy) {
|
||||||
|
setDirection(dx, dy);
|
||||||
|
// Prevalença lateral: si hi ha component horitzontal s'usa l'anim
|
||||||
|
// lateral + flip segons el signe. Només direccions purament verticals
|
||||||
|
// disparen walk_up/walk_down. Quan la direcció queda (0,0) (cul-de-sac
|
||||||
|
// amb NONE) no toquem l'animació: la momia es quedarà al frame actual
|
||||||
|
// fins que step() li torni a assignar direcció.
|
||||||
|
if (dx != 0.0F) {
|
||||||
|
anim_ = anim_walk_lateral_;
|
||||||
|
flip_ = (dx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
||||||
|
} else if (dy < 0.0F) {
|
||||||
|
anim_ = anim_walk_up_;
|
||||||
|
flip_ = SDL_FLIP_NONE;
|
||||||
|
} else if (dy > 0.0F) {
|
||||||
|
anim_ = anim_walk_down_;
|
||||||
|
flip_ = SDL_FLIP_NONE;
|
||||||
|
}
|
||||||
|
if (anim_.has_value()) { anim_->reset(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
|
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
|
||||||
@@ -38,6 +107,115 @@ auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction
|
|||||||
return allowed[dist(rng())];
|
return allowed[dist(rng())];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Mummy::candidateBox(float dx, float dy) const -> SDL_FRect {
|
||||||
|
auto box = getCollisionRect();
|
||||||
|
box.x += dx;
|
||||||
|
box.y += dy;
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Mummy::snapToPlayfieldEdge(float dx, float dy) {
|
||||||
|
// La caja 16×16 té exactament l'amplada del corredor; si ens aturàvem
|
||||||
|
// a una posició subpíxel dins de l'última cel·la, qualsevol gir
|
||||||
|
// perpendicular col·lidiria amb el sarc adjacent al limit. Per això
|
||||||
|
// reduïm el dx/dy fins a la vora exacta del playfield en l'eix actiu
|
||||||
|
// (l'altre sempre és 0 perquè la momia només es mou en 1D per frame).
|
||||||
|
const auto BOX = getCollisionRect();
|
||||||
|
float snap_dx = 0.0F;
|
||||||
|
float snap_dy = 0.0F;
|
||||||
|
if (dx > 0.0F) {
|
||||||
|
snap_dx = std::max(0.0F, static_cast<float>(Map::playfield_x + Map::playfield_w) - BOX.x - BOX.w);
|
||||||
|
} else if (dx < 0.0F) {
|
||||||
|
snap_dx = std::min(0.0F, static_cast<float>(Map::playfield_x) - BOX.x);
|
||||||
|
}
|
||||||
|
if (dy > 0.0F) {
|
||||||
|
snap_dy = std::max(0.0F, static_cast<float>(Map::playfield_y + Map::playfield_h) - BOX.y - BOX.h);
|
||||||
|
} else if (dy < 0.0F) {
|
||||||
|
snap_dy = std::min(0.0F, static_cast<float>(Map::playfield_y) - BOX.y);
|
||||||
|
}
|
||||||
|
if (!Map::collides(candidateBox(snap_dx, snap_dy))) {
|
||||||
|
pixel_rect_.x += snap_dx;
|
||||||
|
pixel_rect_.y += snap_dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Mummy::step(float delta_time) {
|
||||||
|
// Desplaçament nominal en aquest frame segons la direcció actual.
|
||||||
|
const float DX = dir_x_ * speed_ * delta_time;
|
||||||
|
const float DY = dir_y_ * speed_ * delta_time;
|
||||||
|
|
||||||
|
// Si no té direcció (p.ex. YAML sense `initial_direction`), el frame és
|
||||||
|
// no-op fins que algú la injecte amb `setDirection`.
|
||||||
|
if (DX == 0.0F && DY == 0.0F) { return; }
|
||||||
|
|
||||||
|
// Prova d'avançar. Si la caixa candidata és vàlida (dins del playfield
|
||||||
|
// i sense trepitjar cap sarc), s'aplica el moviment i s'acaba el frame.
|
||||||
|
const auto CANDIDATE = candidateBox(DX, DY);
|
||||||
|
if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) {
|
||||||
|
pixel_rect_.x += DX;
|
||||||
|
pixel_rect_.y += DY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bloqueig per la vora del playfield: amarrem la caja a la vora exacta.
|
||||||
|
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
|
||||||
|
|
||||||
|
// Cal girar. Preferim perpendiculars; només fem U-turn si no hi ha cap
|
||||||
|
// altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu).
|
||||||
|
const auto POOLS = collectDirectionPools(speed_ * delta_time);
|
||||||
|
const auto& pool = !POOLS.perp.empty() ? POOLS.perp : POOLS.uturn;
|
||||||
|
applyChoice(chooseDirection(pool));
|
||||||
|
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||||
|
struct Candidate {
|
||||||
|
float dx;
|
||||||
|
float dy;
|
||||||
|
Direction dir;
|
||||||
|
};
|
||||||
|
const std::array<Candidate, 4> ALL{{
|
||||||
|
{.dx = step, .dy = 0.0F, .dir = Direction::RIGHT},
|
||||||
|
{.dx = -step, .dy = 0.0F, .dir = Direction::LEFT},
|
||||||
|
{.dx = 0.0F, .dy = -step, .dir = Direction::UP},
|
||||||
|
{.dx = 0.0F, .dy = step, .dir = Direction::DOWN},
|
||||||
|
}};
|
||||||
|
DirectionPools pools;
|
||||||
|
for (const auto& c : ALL) {
|
||||||
|
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
|
||||||
|
const auto TEST = candidateBox(c.dx, c.dy);
|
||||||
|
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
|
||||||
|
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
|
||||||
|
if (IS_UTURN) {
|
||||||
|
pools.uturn.push_back(c.dir);
|
||||||
|
} else {
|
||||||
|
pools.perp.push_back(c.dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pools;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Mummy::applyChoice(Direction dir) {
|
||||||
|
switch (dir) {
|
||||||
|
case Direction::UP:
|
||||||
|
changeDirection(0.0F, -1.0F);
|
||||||
|
break;
|
||||||
|
case Direction::DOWN:
|
||||||
|
changeDirection(0.0F, 1.0F);
|
||||||
|
break;
|
||||||
|
case Direction::LEFT:
|
||||||
|
changeDirection(-1.0F, 0.0F);
|
||||||
|
break;
|
||||||
|
case Direction::RIGHT:
|
||||||
|
changeDirection(1.0F, 0.0F);
|
||||||
|
break;
|
||||||
|
case Direction::NONE:
|
||||||
|
changeDirection(0.0F, 0.0F);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Mummy::onPlayerContact(Player* /*player*/) {
|
void Mummy::onPlayerContact(Player* /*player*/) {
|
||||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/animation_component.hpp"
|
||||||
#include "game/entities/hazard.hpp"
|
#include "game/entities/hazard.hpp"
|
||||||
|
|
||||||
// Mòmia: perill dinàmic que deambula pels passadís. A cada intersecció
|
// Mòmia: perill dinàmic que deambula pels passadís. A cada intersecció
|
||||||
@@ -31,6 +32,49 @@ 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
|
||||||
|
// pròxim tram la bloquejaria (mur del playfield o sarc). Quan troba un
|
||||||
|
// obstacle, recull les direccions perpendiculars vàlides (evitant
|
||||||
|
// U-turn llevat que no hi hagi alternativa) i crida `chooseDirection`
|
||||||
|
// per decidir-ne una. No consumeix moviment el frame de gir.
|
||||||
|
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.
|
||||||
void onPlayerContact(Player* player) override;
|
void onPlayerContact(Player* player) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
|
||||||
|
[[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect;
|
||||||
|
|
||||||
|
// Amarra la caja a la vora exacta del playfield quan el candidat hi
|
||||||
|
// sortiria — sense això, amb la caja 16×16 iguala el corredor, la
|
||||||
|
// posició subpíxel dins de l'última cel·la impediria girar a cap
|
||||||
|
// perpendicular (col·lisionaria amb el sarc adjacent). Es crida abans
|
||||||
|
// de la lògica de gir.
|
||||||
|
void snapToPlayfieldEdge(float dx, float dy);
|
||||||
|
|
||||||
|
// Recull les direccions alternatives viables des de la posició actual:
|
||||||
|
// `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou
|
||||||
|
// perquè ja sabem que està bloquejada. Cada candidata es valida amb
|
||||||
|
// `Map::insidePlayfield` + `Map::collides` sobre la caja resultant.
|
||||||
|
struct DirectionPools {
|
||||||
|
std::vector<Direction> perp;
|
||||||
|
std::vector<Direction> uturn;
|
||||||
|
};
|
||||||
|
[[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools;
|
||||||
|
|
||||||
|
// Canvi de direcció + aplicació de l'animació i flip corresponents.
|
||||||
|
// Tots els camins que trien nova direcció (ctor amb `initial_direction`,
|
||||||
|
// gir després d'un bloqueig) passen per ací per mantenir anim_ i
|
||||||
|
// flip_ en sincronia amb (dir_x_, dir_y_).
|
||||||
|
void changeDirection(float dx, float dy);
|
||||||
|
|
||||||
|
// Aplica la direcció triada (wrapper del switch en una sola crida).
|
||||||
|
void applyChoice(Direction dir);
|
||||||
|
|
||||||
|
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
|
||||||
|
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
||||||
|
AnimationComponent anim_walk_down_;
|
||||||
|
AnimationComponent anim_walk_up_;
|
||||||
|
AnimationComponent anim_walk_lateral_;
|
||||||
};
|
};
|
||||||
@@ -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/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -42,6 +43,23 @@ RollingRock::RollingRock(const fkyaml::node& yaml) {
|
|||||||
applyInitialDirection(initial, *this);
|
applyInitialDirection(initial, *this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RollingRock::step(float delta_time) {
|
||||||
|
const float DX = dir_x_ * speed_ * delta_time;
|
||||||
|
const float DY = dir_y_ * speed_ * delta_time;
|
||||||
|
if (DX == 0.0F && DY == 0.0F) { return; }
|
||||||
|
|
||||||
|
auto candidate = getCollisionRect();
|
||||||
|
candidate.x += DX;
|
||||||
|
candidate.y += DY;
|
||||||
|
if (Map::insidePlayfield(candidate) && !Map::collides(candidate)) {
|
||||||
|
pixel_rect_.x += DX;
|
||||||
|
pixel_rect_.y += DY;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Bloqueig: aturada definitiva (la roca no rebota).
|
||||||
|
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("player_hit", getDamage()); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,12 @@ class RollingRock : public DynamicHazard {
|
|||||||
public:
|
public:
|
||||||
explicit RollingRock(const fkyaml::node& yaml);
|
explicit RollingRock(const fkyaml::node& yaml);
|
||||||
|
|
||||||
|
// Avança en línia recta fins a la primera col·lisió: si la caixa
|
||||||
|
// candidata sortiria del playfield o trepitjaria un sarc, s'atura
|
||||||
|
// (direcció → 0,0) i l'actualització es manté en aquella posició fins
|
||||||
|
// que un agent extern la torni a empènyer. No rebota per decisió
|
||||||
|
// de disseny: la roca és trampa estàtica disfressada de dinàmica.
|
||||||
|
void step(float delta_time) override;
|
||||||
|
|
||||||
void onPlayerContact(Player* player) override;
|
void onPlayerContact(Player* player) override;
|
||||||
};
|
};
|
||||||
@@ -26,3 +26,18 @@ void Entity::onPlayerContact(Player* /*player*/) {
|
|||||||
void Entity::notifyTileEntered(int /*col*/, int /*row*/) {
|
void Entity::notifyTileEntered(int /*col*/, int /*row*/) {
|
||||||
// No-op per defecte; només `Sarcophagus` l'usa.
|
// No-op per defecte; només `Sarcophagus` l'usa.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Entity::getCollisionRect() const -> SDL_FRect {
|
||||||
|
if (!collision_box_.has_value()) { return pixel_rect_; }
|
||||||
|
return {
|
||||||
|
.x = pixel_rect_.x + static_cast<float>(collision_box_->offset_x),
|
||||||
|
.y = pixel_rect_.y + static_cast<float>(collision_box_->offset_y),
|
||||||
|
.w = static_cast<float>(collision_box_->width),
|
||||||
|
.h = static_cast<float>(collision_box_->height),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Entity::getFeetY() const -> float {
|
||||||
|
const auto RECT = getCollisionRect();
|
||||||
|
return RECT.y + RECT.h;
|
||||||
|
}
|
||||||
@@ -37,6 +37,18 @@ class Entity {
|
|||||||
int row{0};
|
int row{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sub-rectangle de col·lisió dins de `pixel_rect_`. Pensat per a
|
||||||
|
// personatges amb falsa perspectiva (sprite alt, caixa AABB petita
|
||||||
|
// encastada a baix) — mateixa convenció que el Player. `offset_x/y`
|
||||||
|
// són relatius a la cantonada superior-esquerra del sprite. Si l'entitat
|
||||||
|
// no té `CollisionBox`, `getCollisionRect()` retorna tot el `pixel_rect_`.
|
||||||
|
struct CollisionBox {
|
||||||
|
int offset_x{0};
|
||||||
|
int offset_y{0};
|
||||||
|
int width{0};
|
||||||
|
int height{0};
|
||||||
|
};
|
||||||
|
|
||||||
Entity() = default;
|
Entity() = default;
|
||||||
Entity(std::string name, TilePos tile, SDL_FRect pixel_rect);
|
Entity(std::string name, TilePos tile, SDL_FRect pixel_rect);
|
||||||
virtual ~Entity() = default;
|
virtual ~Entity() = default;
|
||||||
@@ -100,13 +112,43 @@ 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_; }
|
||||||
|
|
||||||
|
// Caixa de col·lisió: per defecte és el `pixel_rect_` sencer (entitats
|
||||||
|
// AABB-aligned). Si s'ha configurat un `CollisionBox`, retorna el
|
||||||
|
// sub-rect dins del sprite — vida útil: validar col·lisions amb el
|
||||||
|
// jugador o el mapa sense que la caixa inclogui "l'alçada" del sprite.
|
||||||
|
[[nodiscard]] auto getCollisionRect() const -> SDL_FRect;
|
||||||
|
|
||||||
|
// Y dels peus: la vora inferior de la caixa de col·lisió. La usarà el
|
||||||
|
// Z-order per ordenar entitats i jugador de menys a més profunditat
|
||||||
|
// (REND-01, quan s'implemente).
|
||||||
|
[[nodiscard]] auto getFeetY() const -> float;
|
||||||
|
|
||||||
|
// El loader li assigna una caixa explícita llegida del YAML; sense
|
||||||
|
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
|
||||||
|
// col·lisió coincideix amb el sprite.
|
||||||
|
void setCollisionBox(CollisionBox box) { collision_box_ = box; }
|
||||||
|
|
||||||
|
// Accés a la caixa crua (offsets + mida) — el loader la consulta per
|
||||||
|
// a col·locar el sprite quan `anchor: feet` ha d'alinear la caja (no
|
||||||
|
// el sprite) amb el tile. Nullopt si l'entitat no en té.
|
||||||
|
[[nodiscard]] auto getCollisionBox() const -> const std::optional<CollisionBox>& { return collision_box_; }
|
||||||
|
|
||||||
|
// Flip del sprite. Les entitats estàtiques mai no el toquen (queda a
|
||||||
|
// SDL_FLIP_NONE); les dinàmiques que miren cap als costats (p. ex.
|
||||||
|
// Mummy en direcció horitzontal) el canvien quan gira. El manager
|
||||||
|
// el passa a Surface::render.
|
||||||
|
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
|
||||||
|
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::string name_;
|
std::string name_;
|
||||||
TilePos tile_{};
|
TilePos tile_{};
|
||||||
SDL_FRect pixel_rect_{};
|
SDL_FRect pixel_rect_{};
|
||||||
std::optional<AnimationComponent> anim_;
|
std::optional<AnimationComponent> anim_;
|
||||||
|
std::optional<CollisionBox> collision_box_;
|
||||||
std::shared_ptr<Surface> surface_{nullptr};
|
std::shared_ptr<Surface> surface_{nullptr};
|
||||||
bool collides_{false};
|
bool collides_{false};
|
||||||
bool interactable_{false};
|
bool interactable_{false};
|
||||||
|
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
};
|
};
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "game/entities/entity.hpp"
|
#include "game/entities/entity.hpp"
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
|
||||||
@@ -30,10 +31,10 @@ 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 PATH = templatePath(name);
|
const std::string FULL = Resource::Helper::resolve(templatePath(name));
|
||||||
std::ifstream file(PATH);
|
std::ifstream file(FULL);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + PATH);
|
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + FULL);
|
||||||
}
|
}
|
||||||
fkyaml::node node = fkyaml::node::deserialize(file);
|
fkyaml::node node = fkyaml::node::deserialize(file);
|
||||||
cache.emplace(name, node);
|
cache.emplace(name, node);
|
||||||
@@ -41,9 +42,10 @@ namespace EntityLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> {
|
auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> {
|
||||||
std::ifstream file(level_yaml_path);
|
const auto FULL = Resource::Helper::resolve(level_yaml_path);
|
||||||
|
std::ifstream file(FULL);
|
||||||
if (!file.is_open()) {
|
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);
|
fkyaml::node root = fkyaml::node::deserialize(file);
|
||||||
std::vector<fkyaml::node> spawns;
|
std::vector<fkyaml::node> spawns;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iterator>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "game/entities/concrete/door.hpp"
|
#include "game/entities/concrete/door.hpp"
|
||||||
@@ -68,6 +70,13 @@ void EntityManager::notifyPlayerEnteredTile(int col, int row) {
|
|||||||
for (auto& e : entities_) { e->notifyTileEntered(col, row); }
|
for (auto& e : entities_) { e->notifyTileEntered(col, row); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
||||||
|
std::vector<Entity*> out;
|
||||||
|
out.reserve(entities_.size());
|
||||||
|
std::ranges::transform(entities_, std::back_inserter(out), [](const auto& e) { return e.get(); });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// NOTA: `renderAll` es defineix a `entity_manager_loadlevel.cpp`. Conté
|
// NOTA: `renderAll` es defineix a `entity_manager_loadlevel.cpp`. Conté
|
||||||
// blits via `Surface::render`, que dragaria el pipeline complet de render
|
// blits via `Surface::render`, que dragaria el pipeline complet de render
|
||||||
// (GIF, Canvas, Screen, àudio...) al smoke test. Separar-lo manté el smoke
|
// (GIF, Canvas, Screen, àudio...) al smoke test. Separar-lo manté el smoke
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
class Entity;
|
class Entity;
|
||||||
class Sarcophagus;
|
class Sarcophagus;
|
||||||
class Door;
|
class Door;
|
||||||
|
class Player;
|
||||||
|
|
||||||
// Contenidor i coordinador d'entitats d'un nivell. Responsabilitats:
|
// Contenidor i coordinador d'entitats d'un nivell. Responsabilitats:
|
||||||
// 1. `add` registra una entitat, la connecta al bus i manté propietat.
|
// 1. `add` registra una entitat, la connecta al bus i manté propietat.
|
||||||
@@ -49,10 +50,30 @@ class EntityManager {
|
|||||||
void update(float delta_time);
|
void update(float delta_time);
|
||||||
|
|
||||||
// Dibuix de totes les entitats. Sense ordenació per z: la crida dura
|
// Dibuix de totes les entitats. Sense ordenació per z: la crida dura
|
||||||
// l'ordre d'inserció. El z-order fi (interleaving amb el Player per
|
// l'ordre d'inserció. GameScene fa el z-order interleaving amb els
|
||||||
// feetY) queda per a una tasca posterior.
|
// players directament via `renderEntity`; `renderAll` queda com a
|
||||||
|
// dreçera quan no hi ha players al render (editor, tests).
|
||||||
void renderAll();
|
void renderAll();
|
||||||
|
|
||||||
|
// Render d'una única entitat (mateixa lògica de clipping per frame
|
||||||
|
// que renderAll). Exposat perquè GameScene pugui interleavar entitats
|
||||||
|
// i players per `getFeetY()` (REND-01). Estàtic perquè la lògica de
|
||||||
|
// render no depèn de cap estat del manager — ara com ara, l'entitat
|
||||||
|
// porta la seua surface, animació i pixel_rect.
|
||||||
|
static void renderEntity(const Entity& entity);
|
||||||
|
|
||||||
|
// Accés als punters observacionals a les entitats per a consumidors
|
||||||
|
// externs que necessiten construir una llista ordenada (z-order).
|
||||||
|
[[nodiscard]] auto getEntities() const -> std::vector<Entity*>;
|
||||||
|
|
||||||
|
// Comprova contacte del(s) jugador(s) amb entitats `collides()=true`.
|
||||||
|
// Si la caixa base del jugador solapa amb la caixa de col·lisió de
|
||||||
|
// l'entitat, crida `entity->onPlayerContact(player)`. No fa res si
|
||||||
|
// la llista està buida. Es crida des de GameScene cada frame, després
|
||||||
|
// de `update`, perquè la resposta (p.ex. `player_hit`, `level_complete`)
|
||||||
|
// reflecteixi l'estat del frame actual.
|
||||||
|
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 moviment de jugador: per a cada sarcòfag,
|
||||||
// cridar notifyTileEntered. Les entitats no-sarcòfag ho ignoren per
|
// cridar notifyTileEntered. Les entitats no-sarcòfag ho ignoren per
|
||||||
// defecte (veure Entity::notifyTileEntered).
|
// defecte (veure Entity::notifyTileEntered).
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/entity_loader.hpp"
|
#include "game/entities/entity_loader.hpp"
|
||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
// Render centralitzat al manager per confinar la dependència cap a
|
// Render centralitzat al manager per confinar la dependència cap a
|
||||||
@@ -32,12 +33,11 @@
|
|||||||
// surface per l'amplada del frame — així un GIF de 4 frames pot venir com
|
// surface per l'amplada del frame — així un GIF de 4 frames pot venir com
|
||||||
// a 1×4, 4×1 o 2×2 indistintament. `renderAll` clipa la cel·la del frame
|
// a 1×4, 4×1 o 2×2 indistintament. `renderAll` clipa la cel·la del frame
|
||||||
// actual amb un `src_rect`. Entitats sense animació es dibuixen al frame 0.
|
// actual amb un `src_rect`. Entitats sense animació es dibuixen al frame 0.
|
||||||
void EntityManager::renderAll() {
|
void EntityManager::renderEntity(const Entity& e) {
|
||||||
for (const auto& e : entities_) {
|
const auto& surface = e.getSurface();
|
||||||
const auto& surface = e->getSurface();
|
if (surface == nullptr) { return; }
|
||||||
if (surface == nullptr) { continue; }
|
const auto RECT = e.getPixelRect();
|
||||||
const auto RECT = e->getPixelRect();
|
const auto& anim = e.getAnimation();
|
||||||
const auto& anim = e->getAnimation();
|
|
||||||
const int FRAME = anim.has_value() ? anim->currentFrame() : 0;
|
const int FRAME = anim.has_value() ? anim->currentFrame() : 0;
|
||||||
const int FRAME_W = static_cast<int>(RECT.w);
|
const int FRAME_W = static_cast<int>(RECT.w);
|
||||||
const int FRAME_H = static_cast<int>(RECT.h);
|
const int FRAME_H = static_cast<int>(RECT.h);
|
||||||
@@ -51,7 +51,24 @@ void EntityManager::renderAll() {
|
|||||||
.w = RECT.w,
|
.w = RECT.w,
|
||||||
.h = RECT.h,
|
.h = RECT.h,
|
||||||
};
|
};
|
||||||
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y), &SRC);
|
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y), &SRC, e.getFlip());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityManager::renderAll() {
|
||||||
|
for (const auto& e : entities_) { renderEntity(*e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
||||||
|
if (players.empty()) { return; }
|
||||||
|
for (const auto& entity : entities_) {
|
||||||
|
if (!entity->collides()) { continue; }
|
||||||
|
const auto BOX = entity->getCollisionRect();
|
||||||
|
for (auto* p : players) {
|
||||||
|
if (p == nullptr) { continue; }
|
||||||
|
const auto PBOX = p->getBaseBox();
|
||||||
|
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); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +105,58 @@ namespace {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
||||||
|
// de la plantilla d'entitat.
|
||||||
|
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
|
||||||
|
// inferior del sprite i centrada horitzontalment (convenció antiga).
|
||||||
|
// - Amb `base_size: [bw, bh]`: la caja queda concèntrica amb el
|
||||||
|
// rectangle inferior bw×bh del sprite (els "peus" visuals), igual
|
||||||
|
// que fa el Player. Pot sobresortir per sobre/sota/costats del
|
||||||
|
// rectangle-base simètricament — així la caja representa el volum
|
||||||
|
// sòlid al voltant dels peus, no la silueta de tot el sprite.
|
||||||
|
// - `offset: [x, y]`: si apareix, sobreescriu el càlcul anterior.
|
||||||
|
// Retorna nullopt si no hi ha bloc, i l'entitat es queda sense caixa
|
||||||
|
// explícita (getCollisionRect torna tot el sprite).
|
||||||
|
auto readCollisionBox(const fkyaml::node& tmpl, float sprite_w, float sprite_h) -> std::optional<Entity::CollisionBox> {
|
||||||
|
if (!tmpl.contains("collision")) { return std::nullopt; }
|
||||||
|
const auto& c = tmpl["collision"];
|
||||||
|
if (!c.contains("size")) { return std::nullopt; }
|
||||||
|
const auto& sz = c["size"];
|
||||||
|
if (!sz.is_sequence() || sz.size() != 2) { return std::nullopt; }
|
||||||
|
const int W = sz[0].get_value<int>();
|
||||||
|
const int H = sz[1].get_value<int>();
|
||||||
|
int off_x = static_cast<int>((sprite_w - static_cast<float>(W)) / 2.0F);
|
||||||
|
int off_y = static_cast<int>(sprite_h - static_cast<float>(H));
|
||||||
|
if (c.contains("base_size")) {
|
||||||
|
const auto& bs = c["base_size"];
|
||||||
|
if (bs.is_sequence() && bs.size() == 2) {
|
||||||
|
const int BH = bs[1].get_value<int>();
|
||||||
|
// Centre vertical del rectangle-base (alineat a la vora
|
||||||
|
// inferior del sprite): sprite_h − bh/2. Caja centrada hi:
|
||||||
|
// off_y = sprite_h − bh/2 − H/2 = sprite_h − (bh + H)/2.
|
||||||
|
off_y = static_cast<int>(sprite_h) - ((BH + H) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.contains("offset")) {
|
||||||
|
const auto& o = c["offset"];
|
||||||
|
if (o.is_sequence() && o.size() == 2) {
|
||||||
|
off_x = o[0].get_value<int>();
|
||||||
|
off_y = o[1].get_value<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Entity::CollisionBox{.offset_x = off_x, .offset_y = off_y, .width = W, .height = H};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ancoratge d'spawn. "feet" vol dir que el `position: [col, row]` del
|
||||||
|
// map és el tile dels peus: el sprite es centra horitzontalment dins
|
||||||
|
// del tile i s'alinea la seua vora inferior amb la vora inferior del
|
||||||
|
// tile. "top_left" (per defecte) posa el sprite amb el cantonal
|
||||||
|
// superior-esquerre a la cantonada superior-esquerra del tile.
|
||||||
|
auto readAnchor(const fkyaml::node& tmpl) -> std::string {
|
||||||
|
if (!tmpl.contains("anchor")) { return "top_left"; }
|
||||||
|
return tmpl["anchor"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
// Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El
|
// Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El
|
||||||
// grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que
|
// grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que
|
||||||
// pot ser negatiu per overscan CPC.
|
// pot ser negatiu per overscan CPC.
|
||||||
@@ -115,7 +184,10 @@ namespace {
|
|||||||
// Crea l'entitat per factory i li adjunta la surface si n'hi ha. Retorna
|
// Crea l'entitat per factory i li adjunta la surface si n'hi ha. Retorna
|
||||||
// nullptr si la plantilla no es pot carregar o si el tipus no està
|
// nullptr si la plantilla no es pot carregar o si el tipus no està
|
||||||
// registrat al factory — el loop principal ja imprimeix el missatge.
|
// registrat al factory — el loop principal ja imprimeix el missatge.
|
||||||
auto buildEntity(const std::string& type, SpriteInfo& sprite_out) -> std::unique_ptr<Entity> {
|
// També llegeix el bloc `collision:` de la plantilla (si existeix) i
|
||||||
|
// el bota a l'entitat, i retorna l'`anchor` per a que el loader sàpiga
|
||||||
|
// com col·locar el sprite a l'spawn ("feet" o "top_left").
|
||||||
|
auto buildEntity(const std::string& type, SpriteInfo& sprite_out, std::string& anchor_out) -> std::unique_ptr<Entity> {
|
||||||
fkyaml::node tmpl;
|
fkyaml::node tmpl;
|
||||||
try {
|
try {
|
||||||
tmpl = EntityLoader::loadTemplate(type);
|
tmpl = EntityLoader::loadTemplate(type);
|
||||||
@@ -133,6 +205,10 @@ namespace {
|
|||||||
Resource::Cache::get().loadSurface(sprite_out.key, sprite_out.full_path);
|
Resource::Cache::get().loadSurface(sprite_out.key, sprite_out.full_path);
|
||||||
entity->setSurface(Resource::Cache::get().getSurface(sprite_out.key));
|
entity->setSurface(Resource::Cache::get().getSurface(sprite_out.key));
|
||||||
}
|
}
|
||||||
|
if (const auto BOX = readCollisionBox(tmpl, sprite_out.width, sprite_out.height); BOX.has_value()) {
|
||||||
|
entity->setCollisionBox(*BOX);
|
||||||
|
}
|
||||||
|
anchor_out = readAnchor(tmpl);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +243,17 @@ namespace {
|
|||||||
};
|
};
|
||||||
entity->setPixelRect(RENDER);
|
entity->setPixelRect(RENDER);
|
||||||
|
|
||||||
|
// La caixa de col·lisió del sarc és la lògica (48×16 inferior), no el
|
||||||
|
// rect del sprite sencer: si un jugador passa just per damunt (pels
|
||||||
|
// 16 px superiors del sprite, que són "alçada" sense cos), no s'ha
|
||||||
|
// de disparar cap contacte. Offset relatiu al render rect.
|
||||||
|
entity->setCollisionBox({
|
||||||
|
.offset_x = 0,
|
||||||
|
.offset_y = static_cast<int>(LOGICAL.y - RENDER.y),
|
||||||
|
.width = static_cast<int>(LOGICAL.w),
|
||||||
|
.height = static_cast<int>(LOGICAL.h),
|
||||||
|
});
|
||||||
|
|
||||||
// Downcast segur: la factory ens garanteix que "sarcophagus" crea
|
// Downcast segur: la factory ens garanteix que "sarcophagus" crea
|
||||||
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
|
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
|
||||||
// `unique_ptr` evitem el lint `bugprone-unused-return-value`.
|
// `unique_ptr` evitem el lint `bugprone-unused-return-value`.
|
||||||
@@ -180,14 +267,36 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, 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";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto& p = spawn["position"];
|
const auto& p = spawn["position"];
|
||||||
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
|
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
|
||||||
entity->setPixelRect({.x = PX.x, .y = PX.y, .w = sprite.width, .h = sprite.height});
|
|
||||||
|
// Anchor "feet": el tile [col,row] és el tile dels peus de l'actor.
|
||||||
|
// Si l'entitat té caixa de col·lisió explícita, la caixa s'alinea
|
||||||
|
// exactament amb el tile (el sprite queda col·locat per als offsets
|
||||||
|
// que la caja defineix — típicament sobresurt cap amunt per falsa
|
||||||
|
// perspectiva). Això és coherent amb el jugador, que també centra
|
||||||
|
// la seua 16×16 sobre el rectangle-base; la caja del tile i la del
|
||||||
|
// mapeig són la mateixa. Sense caixa, fallback històric: sprite
|
||||||
|
// centrat horitzontalment i amb la vora inferior a la vora inferior
|
||||||
|
// del tile.
|
||||||
|
float rect_x = PX.x;
|
||||||
|
float rect_y = PX.y;
|
||||||
|
if (anchor == "feet") {
|
||||||
|
const auto& box_opt = entity->getCollisionBox();
|
||||||
|
if (box_opt.has_value()) {
|
||||||
|
rect_x = PX.x - static_cast<float>(box_opt->offset_x);
|
||||||
|
rect_y = PX.y - static_cast<float>(box_opt->offset_y);
|
||||||
|
} else {
|
||||||
|
rect_x = PX.x + (static_cast<float>(Map::tile_size) - sprite.width) / 2.0F;
|
||||||
|
rect_y = PX.y + static_cast<float>(Map::tile_size) - sprite.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});
|
||||||
|
|
||||||
if (type == "door") {
|
if (type == "door") {
|
||||||
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
||||||
@@ -209,12 +318,13 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
|||||||
const auto TYPE = spawn["type"].get_value<std::string>();
|
const auto TYPE = spawn["type"].get_value<std::string>();
|
||||||
|
|
||||||
SpriteInfo sprite;
|
SpriteInfo sprite;
|
||||||
auto entity = buildEntity(TYPE, sprite);
|
std::string anchor;
|
||||||
|
auto entity = buildEntity(TYPE, sprite, anchor);
|
||||||
if (entity == nullptr) { continue; }
|
if (entity == nullptr) { continue; }
|
||||||
|
|
||||||
const bool OK = (TYPE == "sarcophagus")
|
const bool OK = (TYPE == "sarcophagus")
|
||||||
? placeSarcophagus(*this, std::move(entity), sprite, spawn)
|
? placeSarcophagus(*this, std::move(entity), sprite, spawn)
|
||||||
: placeGeneric(*this, std::move(entity), TYPE, sprite, spawn);
|
: placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn);
|
||||||
if (OK) { ++loaded; }
|
if (OK) { ++loaded; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/map/map.hpp"
|
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -22,9 +22,10 @@ namespace {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void Player::loadFrom(const std::string& yaml_path) {
|
void Player::loadFrom(const std::string& yaml_path) {
|
||||||
std::ifstream file(yaml_path);
|
const auto FULL = Resource::Helper::resolve(yaml_path);
|
||||||
|
std::ifstream file(FULL);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path);
|
throw std::runtime_error("Player: no s'ha pogut obrir " + FULL);
|
||||||
}
|
}
|
||||||
auto yaml = fkyaml::node::deserialize(file);
|
auto yaml = fkyaml::node::deserialize(file);
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ void Player::loadFrom(const std::string& yaml_path) {
|
|||||||
sprite_h = yaml["frameHeight"].get_value<int>();
|
sprite_h = yaml["frameHeight"].get_value<int>();
|
||||||
|
|
||||||
if (!yaml.contains("collision")) {
|
if (!yaml.contains("collision")) {
|
||||||
throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path);
|
throw std::runtime_error("Player: manca bloc `collision:` a " + FULL);
|
||||||
}
|
}
|
||||||
const auto& c = yaml["collision"];
|
const auto& c = yaml["collision"];
|
||||||
collision_size = c["size"].get_value<int>();
|
collision_size = c["size"].get_value<int>();
|
||||||
@@ -47,9 +48,10 @@ void Player::loadFrom(const std::string& yaml_path) {
|
|||||||
collision_y_offset = (sprite_h - base_size) - collision_padding;
|
collision_y_offset = (sprite_h - base_size) - collision_padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
Player::Player(const std::string& anim_key, float x, float y, const Controls& controls)
|
Player::Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info)
|
||||||
: AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)),
|
: AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)),
|
||||||
controls_(controls) {
|
controls_(controls),
|
||||||
|
map_(map_info) {
|
||||||
setPos(x, y);
|
setPos(x, y);
|
||||||
setCurrentAnimation("stand");
|
setCurrentAnimation("stand");
|
||||||
}
|
}
|
||||||
@@ -66,49 +68,60 @@ void Player::update(float delta_time) {
|
|||||||
setVelX(vx);
|
setVelX(vx);
|
||||||
setVelY(vy);
|
setVelY(vy);
|
||||||
|
|
||||||
// El sprite original mira a la dreta; flipem només si es mou a l'esquerra
|
// Tria de facing amb prevalença lateral: si hi ha component horitzontal
|
||||||
// i restaurem si torna a la dreta. Moviments purament verticals conserven
|
// (inclòs el diagonal), l'animació és lateral i el flip segueix el signe
|
||||||
// l'últim flip perquè el jugador no "gire" sense motiu.
|
// de `vx`. Només en moviment purament vertical passem a UP/DOWN, i en
|
||||||
if (vx < 0.0F) {
|
// aquest cas el flip es força a NONE perquè els sprites frontals no
|
||||||
setFlip(SDL_FLIP_HORIZONTAL);
|
// s'han d'espillar segons l'últim lateral. En aturar-se conservem
|
||||||
} else if (vx > 0.0F) {
|
// `facing_` per a l'animació stand corresponent.
|
||||||
|
if (vx != 0.0F) {
|
||||||
|
facing_ = Facing::SIDE;
|
||||||
|
setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
|
||||||
|
} else if (vy < 0.0F) {
|
||||||
|
facing_ = Facing::UP;
|
||||||
|
setFlip(SDL_FLIP_NONE);
|
||||||
|
} else if (vy > 0.0F) {
|
||||||
|
facing_ = Facing::DOWN;
|
||||||
setFlip(SDL_FLIP_NONE);
|
setFlip(SDL_FLIP_NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool MOVING = vx != 0.0F || vy != 0.0F;
|
|
||||||
setCurrentAnimation(MOVING ? "walk" : "stand");
|
|
||||||
|
|
||||||
// Snap al passadís: si només hi ha moviment horitzontal (o vertical) i
|
// Snap al passadís: si només hi ha moviment horitzontal (o vertical) i
|
||||||
// la caixa està prou prop d'una lane perpendicular (≤ mitja caixa), la
|
// la caixa està prou prop d'una lane perpendicular (≤ mitja caixa), la
|
||||||
// Y (o X) es corregeix gradualment per enfilar el passadís. Amb
|
// Y (o X) es corregeix gradualment per enfilar el passadís. Amb
|
||||||
// moviment diagonal no es fa snap — l'usuari pilota manualment.
|
// moviment diagonal no es fa snap — l'usuari pilota manualment.
|
||||||
const bool ONLY_H = (vx != 0.0F) && (vy == 0.0F);
|
const bool ONLY_H = (vx != 0.0F) && (vy == 0.0F);
|
||||||
const bool ONLY_V = (vy != 0.0F) && (vx == 0.0F);
|
const bool ONLY_V = (vy != 0.0F) && (vx == 0.0F);
|
||||||
|
|
||||||
|
// Pos d'inici de frame (abans de snap i integració): si en acabar el
|
||||||
|
// frame coincideix amb la posició final, el jugador no s'ha mogut
|
||||||
|
// realment (ni per input, ni per snap, ni ha superat la col·lisió) i cal
|
||||||
|
// forçar l'animació `stand` corresponent al facing — així s'atura el
|
||||||
|
// caminat quan xoca contra un mur o un sarcòfag.
|
||||||
|
const float FRAME_START_X = getPosX();
|
||||||
|
const float FRAME_START_Y = getPosY();
|
||||||
|
|
||||||
if (ONLY_H) {
|
if (ONLY_H) {
|
||||||
snapToHorizontalLane(delta_time);
|
snapToHorizontalLane(delta_time);
|
||||||
} else if (ONLY_V) {
|
} else if (ONLY_V) {
|
||||||
snapToVerticalLane(delta_time);
|
snapToVerticalLane(delta_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardem la posició prèvia i deixem que AnimatedSprite::update aplique
|
// Animació del frame + moviment col·lisió-aware. `tryMove` aplica els
|
||||||
// la velocitat com sempre; així no dupliquem la integració temporal ni
|
// deltes per eix i reverteix l'eix que col·lisiona, així el jugador
|
||||||
// l'animació. Després, axis-separated collision: revertim i re-apliquem
|
// llisca contra sarcòfags i murs en compte de quedar-se bloquejat en
|
||||||
// cada eix comprovant el mapa de colisions, de manera que el jugador
|
// sec. La caixa que s'envia al callback és `collisionBox()` (override
|
||||||
// llisca contra els sarcòfags en compte de quedar-se bloquejat en sec.
|
// a Player retornant `getBaseBox()`), no el sprite sencer.
|
||||||
const float OLD_X = getPosX();
|
animate(delta_time);
|
||||||
const float OLD_Y = getPosY();
|
tryMove(vx * delta_time, vy * delta_time, [this](const SDL_FRect& box) { return map_.collides(box); });
|
||||||
AnimatedSprite::update(delta_time);
|
|
||||||
const float NEW_X = getPosX();
|
|
||||||
const float NEW_Y = getPosY();
|
|
||||||
setPos(OLD_X, OLD_Y);
|
|
||||||
|
|
||||||
setPosX(NEW_X);
|
|
||||||
if (Map::collides(baseBox())) { setPosX(OLD_X); }
|
|
||||||
setPosY(NEW_Y);
|
|
||||||
if (Map::collides(baseBox())) { setPosY(OLD_Y); }
|
|
||||||
|
|
||||||
clampToPlayfield();
|
clampToPlayfield();
|
||||||
|
|
||||||
|
// Animació basada en moviment real: si la col·lisió (o l'absència
|
||||||
|
// d'input) ha deixat el jugador al mateix lloc que al principi del
|
||||||
|
// frame, queda en `stand` del facing actual.
|
||||||
|
const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y);
|
||||||
|
setCurrentAnimation(animationName(MOVED));
|
||||||
|
|
||||||
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
||||||
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
||||||
// així els sarcòfags no reben falsos positius mentre el Player rellisca
|
// així els sarcòfags no reben falsos positius mentre el Player rellisca
|
||||||
@@ -124,34 +137,46 @@ void Player::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Player::animationName(bool moving) const -> const char* {
|
||||||
|
switch (facing_) {
|
||||||
|
case Facing::UP:
|
||||||
|
return moving ? "walk_up" : "stand_up";
|
||||||
|
case Facing::DOWN:
|
||||||
|
return moving ? "walk_down" : "stand_down";
|
||||||
|
case Facing::SIDE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return moving ? "walk" : "stand";
|
||||||
|
}
|
||||||
|
|
||||||
auto Player::getCurrentTile() const -> std::pair<int, int> {
|
auto Player::getCurrentTile() const -> std::pair<int, int> {
|
||||||
// El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat
|
// El grid del tilemap és de `tile_size` píxels i està 16-alineat
|
||||||
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
|
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
|
||||||
// centre de la caixa de col·lisió ens dóna el tile on "està" el Player.
|
// centre de la caixa de col·lisió ens dóna el tile on "està" el Player.
|
||||||
const auto BOX = baseBox();
|
const auto BOX = baseBox();
|
||||||
const float CX = BOX.x + (BOX.w * 0.5F);
|
const float CX = BOX.x + (BOX.w * 0.5F);
|
||||||
const float CY = BOX.y + (BOX.h * 0.5F);
|
const float CY = BOX.y + (BOX.h * 0.5F);
|
||||||
const int COL = static_cast<int>(std::floor(
|
const int COL = static_cast<int>(std::floor(
|
||||||
(CX - static_cast<float>(Map::tilemap_origin_x)) / static_cast<float>(Map::tile_size)));
|
(CX - static_cast<float>(map_.tilemap_origin_x)) / static_cast<float>(map_.tile_size)));
|
||||||
const int ROW = static_cast<int>(std::floor(
|
const int ROW = static_cast<int>(std::floor(
|
||||||
(CY - static_cast<float>(Map::tilemap_origin_y)) / static_cast<float>(Map::tile_size)));
|
(CY - static_cast<float>(map_.tilemap_origin_y)) / static_cast<float>(map_.tile_size)));
|
||||||
return {COL, ROW};
|
return {COL, ROW};
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::clampToPlayfield() {
|
void Player::clampToPlayfield() {
|
||||||
// Límits de la zona jugable: la caixa de col·lisió no pot eixir del
|
// Límits de la zona jugable: la caixa de col·lisió no pot eixir del
|
||||||
// playfield (muros). El cap del sprite sí pot sobresortir per damunt.
|
// playfield (muros). El cap del sprite sí pot sobresortir per damunt.
|
||||||
const auto MIN_X = static_cast<float>(Map::playfield_x - collision_x_offset);
|
const auto MIN_X = static_cast<float>(map_.playfield_x - collision_x_offset);
|
||||||
const auto MAX_X = static_cast<float>(
|
const auto MAX_X = static_cast<float>(
|
||||||
Map::playfield_x + Map::playfield_w - collision_x_offset - collision_size);
|
map_.playfield_x + map_.playfield_w - collision_x_offset - collision_size);
|
||||||
const auto MIN_Y = static_cast<float>(Map::playfield_y - collision_y_offset);
|
const auto MIN_Y = static_cast<float>(map_.playfield_y - collision_y_offset);
|
||||||
const auto MAX_Y = static_cast<float>(
|
const auto MAX_Y = static_cast<float>(
|
||||||
Map::playfield_y + Map::playfield_h - collision_y_offset - collision_size);
|
map_.playfield_y + map_.playfield_h - collision_y_offset - collision_size);
|
||||||
setPosX(std::clamp(getPosX(), MIN_X, MAX_X));
|
setPosX(std::clamp(getPosX(), MIN_X, MAX_X));
|
||||||
setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y));
|
setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Player::baseBox() const -> SDL_FRect {
|
auto Player::getBaseBox() const -> SDL_FRect {
|
||||||
return {
|
return {
|
||||||
.x = getPosX() + static_cast<float>(collision_x_offset),
|
.x = getPosX() + static_cast<float>(collision_x_offset),
|
||||||
.y = getPosY() + static_cast<float>(collision_y_offset),
|
.y = getPosY() + static_cast<float>(collision_y_offset),
|
||||||
@@ -182,24 +207,24 @@ void Player::snapToLane(
|
|||||||
const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time);
|
const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time);
|
||||||
const float SAVED = (this->*get_pos)();
|
const float SAVED = (this->*get_pos)();
|
||||||
(this->*set_pos)(SAVED + std::copysign(STEP, DELTA));
|
(this->*set_pos)(SAVED + std::copysign(STEP, DELTA));
|
||||||
if (Map::collides(baseBox())) { (this->*set_pos)(SAVED); }
|
if (map_.collides(baseBox())) { (this->*set_pos)(SAVED); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::snapToHorizontalLane(float delta_time) {
|
void Player::snapToHorizontalLane(float delta_time) {
|
||||||
snapToLane(delta_time,
|
snapToLane(delta_time,
|
||||||
static_cast<float>(Map::playfield_y - collision_y_offset),
|
static_cast<float>(map_.playfield_y - collision_y_offset),
|
||||||
static_cast<float>(Map::pitch_y),
|
static_cast<float>(map_.pitch_y),
|
||||||
Map::grid_rows,
|
map_.grid_rows,
|
||||||
&Player::getPosY,
|
&Player::getPosY,
|
||||||
&Player::setPosY);
|
&Player::setPosY);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::snapToVerticalLane(float delta_time) {
|
void Player::snapToVerticalLane(float delta_time) {
|
||||||
snapToLane(delta_time,
|
snapToLane(delta_time,
|
||||||
static_cast<float>(Map::playfield_x - collision_x_offset),
|
static_cast<float>(map_.playfield_x - collision_x_offset),
|
||||||
static_cast<float>(Map::pitch_x),
|
static_cast<float>(map_.pitch_x),
|
||||||
Map::grid_cols,
|
map_.grid_cols,
|
||||||
&Player::getPosX,
|
&Player::getPosX,
|
||||||
&Player::setPosX);
|
&Player::setPosX);
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||||
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
class EventBus;
|
class EventBus;
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ class Player : public AnimatedSprite {
|
|||||||
// 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);
|
Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info);
|
||||||
|
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
|
|
||||||
@@ -60,10 +62,21 @@ class Player : public AnimatedSprite {
|
|||||||
// Map::tilemap_origin_*). Retorna (col, row).
|
// Map::tilemap_origin_*). Retorna (col, row).
|
||||||
[[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
|
||||||
|
// pel mapa de col·lisions i per les entitats que comproven contacte
|
||||||
|
// amb el jugador (momies, trampes, porta).
|
||||||
|
[[nodiscard]] auto getBaseBox() const -> SDL_FRect;
|
||||||
|
|
||||||
|
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
|
||||||
|
// 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
|
||||||
|
// bloquejar el moviment).
|
||||||
|
[[nodiscard]] auto collisionBox() const -> SDL_FRect override { return getBaseBox(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Rectangle de col·lisió del jugador, usat pel mapa de colisions i com
|
// Alias privat històric. Mantingut perquè viu dins de l'implementació
|
||||||
// a referència per al Z-order.
|
// (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`.
|
||||||
[[nodiscard]] auto baseBox() const -> SDL_FRect;
|
[[nodiscard]] auto baseBox() const -> SDL_FRect { return getBaseBox(); }
|
||||||
|
|
||||||
// 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).
|
||||||
@@ -76,6 +89,10 @@ class Player : public AnimatedSprite {
|
|||||||
// wrappers deleguen a `snapToLane` només canviant l'eix actiu.
|
// wrappers deleguen a `snapToLane` només canviant l'eix actiu.
|
||||||
void snapToHorizontalLane(float delta_time);
|
void snapToHorizontalLane(float delta_time);
|
||||||
void snapToVerticalLane(float delta_time);
|
void snapToVerticalLane(float delta_time);
|
||||||
|
|
||||||
|
// Nom d'animació per al `facing_` actual. Si `moving` és cert s'usa
|
||||||
|
// la variant `walk*`, altrament la variant `stand*`.
|
||||||
|
[[nodiscard]] auto animationName(bool moving) const -> const char*;
|
||||||
void snapToLane(
|
void snapToLane(
|
||||||
float delta_time,
|
float delta_time,
|
||||||
float base,
|
float base,
|
||||||
@@ -85,6 +102,22 @@ class Player : public AnimatedSprite {
|
|||||||
void (Player::*set_pos)(float));
|
void (Player::*set_pos)(float));
|
||||||
|
|
||||||
Controls controls_;
|
Controls controls_;
|
||||||
|
// Còpia plana de la geometria del mapa + punters a `collides`/
|
||||||
|
// `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
|
||||||
|
// a un mapa concret — si el mapa canvia, la Scene ha de
|
||||||
|
// reinstanciar-lo.
|
||||||
|
Map::Info map_;
|
||||||
|
|
||||||
|
// Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions
|
||||||
|
// s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els
|
||||||
|
// moviments purament horitzontals o diagonals prenen `SIDE` (prevalença
|
||||||
|
// lateral), els purament verticals fan `UP`/`DOWN`. En aturar-se es
|
||||||
|
// preserva l'última orientació perquè la versió stand coincideixi.
|
||||||
|
enum class Facing : std::uint8_t { SIDE,
|
||||||
|
UP,
|
||||||
|
DOWN };
|
||||||
|
Facing facing_{Facing::SIDE};
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -21,13 +22,14 @@ namespace {
|
|||||||
namespace Map {
|
namespace Map {
|
||||||
|
|
||||||
void loadFrom(const std::string& yaml_path) {
|
void loadFrom(const std::string& yaml_path) {
|
||||||
std::ifstream file(yaml_path);
|
const auto FULL = Resource::Helper::resolve(yaml_path);
|
||||||
|
std::ifstream file(FULL);
|
||||||
if (!file.is_open()) {
|
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);
|
auto yaml = fkyaml::node::deserialize(file);
|
||||||
if (!yaml.contains("geometry")) {
|
if (!yaml.contains("geometry")) {
|
||||||
throw std::runtime_error("Map: manca bloc `geometry:` a " + yaml_path);
|
throw std::runtime_error("Map: manca bloc `geometry:` a " + FULL);
|
||||||
}
|
}
|
||||||
const auto& g = yaml["geometry"];
|
const auto& g = yaml["geometry"];
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ namespace Map {
|
|||||||
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 " << yaml_path << '\n';
|
<< " des de " << FULL << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
auto getSarcRect(int col, int row) -> SDL_FRect {
|
auto getSarcRect(int col, int row) -> SDL_FRect {
|
||||||
@@ -100,4 +102,26 @@ namespace Map {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto insidePlayfield(const SDL_FRect& box) -> bool {
|
||||||
|
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 makeInfo() -> Info {
|
||||||
|
return {
|
||||||
|
.playfield_x = playfield_x,
|
||||||
|
.playfield_y = playfield_y,
|
||||||
|
.playfield_w = playfield_w,
|
||||||
|
.playfield_h = playfield_h,
|
||||||
|
.pitch_x = pitch_x,
|
||||||
|
.pitch_y = pitch_y,
|
||||||
|
.grid_cols = grid_cols,
|
||||||
|
.grid_rows = grid_rows,
|
||||||
|
.tile_size = tile_size,
|
||||||
|
.tilemap_origin_x = tilemap_origin_x,
|
||||||
|
.tilemap_origin_y = tilemap_origin_y,
|
||||||
|
.collides = &collides,
|
||||||
|
.inside_playfield = &insidePlayfield,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Map
|
} // namespace Map
|
||||||
@@ -14,6 +14,14 @@
|
|||||||
// els membres viuen com a globals del namespace i els callers hi accedeixen
|
// els membres viuen com a globals del namespace i els callers hi accedeixen
|
||||||
// amb `Map::grid_cols`, `Map::collides(...)`, etc. El canvi respecte a
|
// amb `Map::grid_cols`, `Map::collides(...)`, etc. El canvi respecte a
|
||||||
// l'anterior `class Map { static ... };` és purament sintàctic.
|
// l'anterior `class Map { static ... };` és purament sintàctic.
|
||||||
|
//
|
||||||
|
// `MapInfo` (POD trivialment copiable) empaqueta la geometria + dos
|
||||||
|
// punters a funció de col·lisió per injectar-la a entitats que han
|
||||||
|
// d'evitar conèixer el namespace `Map::` directament. Construïda via
|
||||||
|
// `Map::makeInfo()` després de `loadFrom`. Avui només l'usa el Player
|
||||||
|
// (COUP-02 a docs/audit-game.md); momies, roques i `loadFromLevel`
|
||||||
|
// continuen llegint `Map::*` directament — la nota original justifica
|
||||||
|
// que mentre hi hagi un Map global actiu, l'acoblament és acceptable.
|
||||||
namespace Map {
|
namespace Map {
|
||||||
|
|
||||||
// Geometria de la graella (carregada de pyramid.yaml > geometry).
|
// Geometria de la graella (carregada de pyramid.yaml > geometry).
|
||||||
@@ -67,4 +75,33 @@ namespace Map {
|
|||||||
// amb cap sarcòfag.
|
// amb cap sarcòfag.
|
||||||
[[nodiscard]] auto collides(const SDL_FRect& base_box) -> bool;
|
[[nodiscard]] auto collides(const SDL_FRect& base_box) -> bool;
|
||||||
|
|
||||||
|
// true si la caixa `box` queda completament dins del playfield (zona
|
||||||
|
// interior, sense tocar els murs exteriors). El Player ja té clamp
|
||||||
|
// propi que aplica el mateix constrenyiment; això serveix a les
|
||||||
|
// entitats dinàmiques per validar posicions candidates del seu step.
|
||||||
|
[[nodiscard]] auto insidePlayfield(const SDL_FRect& box) -> bool;
|
||||||
|
|
||||||
|
// Snapshot plà de la geometria del mapa + punters a `collides` /
|
||||||
|
// `insidePlayfield`. Pensat per ser passat per valor (POD petit,
|
||||||
|
// trivialment copiable). Un cop construïda, llegeix els valors
|
||||||
|
// congelats — si `Map::loadFrom` es torna a cridar amb un mapa
|
||||||
|
// diferent, cal demanar un MapInfo nou.
|
||||||
|
struct Info {
|
||||||
|
int playfield_x{0};
|
||||||
|
int playfield_y{0};
|
||||||
|
int playfield_w{0};
|
||||||
|
int playfield_h{0};
|
||||||
|
int pitch_x{0};
|
||||||
|
int pitch_y{0};
|
||||||
|
int grid_cols{0};
|
||||||
|
int grid_rows{0};
|
||||||
|
int tile_size{0};
|
||||||
|
int tilemap_origin_x{0};
|
||||||
|
int tilemap_origin_y{0};
|
||||||
|
bool (*collides)(const SDL_FRect&){nullptr};
|
||||||
|
bool (*inside_playfield)(const SDL_FRect&){nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] auto makeInfo() -> Info;
|
||||||
|
|
||||||
} // namespace Map
|
} // namespace Map
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -67,9 +68,10 @@ namespace {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Tilemap::Tilemap(const std::string& yaml_path) {
|
Tilemap::Tilemap(const std::string& yaml_path) {
|
||||||
std::ifstream file(yaml_path);
|
const auto FULL = Resource::Helper::resolve(yaml_path);
|
||||||
|
std::ifstream file(FULL);
|
||||||
if (!file.is_open()) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto yaml = fkyaml::node::deserialize(file);
|
auto yaml = fkyaml::node::deserialize(file);
|
||||||
@@ -111,7 +113,7 @@ Tilemap::Tilemap(const std::string& yaml_path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Tilemap: " << COLS << "x" << ROWS << " tiles ("
|
std::cout << "Tilemap: " << COLS << "x" << ROWS << " tiles ("
|
||||||
<< width_px_ << "x" << height_px_ << " px) des de " << yaml_path << '\n';
|
<< width_px_ << "x" << height_px_ << " px) des de " << FULL << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tilemap::render() const {
|
void Tilemap::render() const {
|
||||||
|
|||||||
@@ -14,6 +14,21 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// --- Estat mutable (privat al TU) ---
|
||||||
|
// Encapsulat ací per impedir mutacions directes des de fora del namespace
|
||||||
|
// Options:: (decisió COUP-03 / docs/audit-game.md). Els callers llegeixen
|
||||||
|
// via getters const-ref i muten via setters específics que persisteixen.
|
||||||
|
std::string g_config_version;
|
||||||
|
std::string g_config_file_path;
|
||||||
|
std::string g_postfx_file_path;
|
||||||
|
std::string g_crtpi_file_path;
|
||||||
|
std::string g_locale{Defaults::I18n::LOCALE};
|
||||||
|
Options::Window g_window{};
|
||||||
|
Options::Video g_video{};
|
||||||
|
Options::Audio g_audio{};
|
||||||
|
std::vector<Options::PostFXPreset> g_postfx_presets{};
|
||||||
|
std::vector<Options::CrtPiPreset> g_crtpi_presets{};
|
||||||
|
|
||||||
// Taules enum ↔ string per a la serialització a YAML. La primera entrada
|
// Taules enum ↔ string per a la serialització a YAML. La primera entrada
|
||||||
// és el fallback quan l'enum no té nom conegut o el text no casa amb
|
// és el fallback quan l'enum no té nom conegut o el text no casa amb
|
||||||
// cap entrada coneguda — per això `LETTERBOX`/`NEAREST`/`OFF` encapçalen.
|
// cap entrada coneguda — per això `LETTERBOX`/`NEAREST`/`OFF` encapçalen.
|
||||||
@@ -93,40 +108,40 @@ namespace {
|
|||||||
void parseWindow(const fkyaml::node& yaml) {
|
void parseWindow(const fkyaml::node& yaml) {
|
||||||
if (!yaml.contains("window")) { return; }
|
if (!yaml.contains("window")) { return; }
|
||||||
const auto& node = yaml["window"];
|
const auto& node = yaml["window"];
|
||||||
readField(node, "zoom", Options::window.zoom);
|
readField(node, "zoom", g_window.zoom);
|
||||||
readField(node, "fullscreen", Options::window.fullscreen);
|
readField(node, "fullscreen", g_window.fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseGpu(const fkyaml::node& video_node) {
|
void parseGpu(const fkyaml::node& video_node) {
|
||||||
if (!video_node.contains("gpu")) { return; }
|
if (!video_node.contains("gpu")) { return; }
|
||||||
const auto& node = video_node["gpu"];
|
const auto& node = video_node["gpu"];
|
||||||
readField(node, "acceleration", Options::video.gpu.acceleration);
|
readField(node, "acceleration", g_video.gpu.acceleration);
|
||||||
readField(node, "preferred_driver", Options::video.gpu.preferred_driver);
|
readField(node, "preferred_driver", g_video.gpu.preferred_driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseShader(const fkyaml::node& video_node) {
|
void parseShader(const fkyaml::node& video_node) {
|
||||||
if (!video_node.contains("shader")) { return; }
|
if (!video_node.contains("shader")) { return; }
|
||||||
const auto& node = video_node["shader"];
|
const auto& node = video_node["shader"];
|
||||||
readField(node, "enabled", Options::video.shader.enabled);
|
readField(node, "enabled", g_video.shader.enabled);
|
||||||
readField(node, "current_shader", Options::video.shader.current_shader);
|
readField(node, "current_shader", g_video.shader.current_shader);
|
||||||
readField(node, "current_postfx_preset", Options::video.shader.current_postfx_preset);
|
readField(node, "current_postfx_preset", g_video.shader.current_postfx_preset);
|
||||||
readField(node, "current_crtpi_preset", Options::video.shader.current_crtpi_preset);
|
readField(node, "current_crtpi_preset", g_video.shader.current_crtpi_preset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseVideo(const fkyaml::node& yaml) {
|
void parseVideo(const fkyaml::node& yaml) {
|
||||||
if (!yaml.contains("video")) { return; }
|
if (!yaml.contains("video")) { return; }
|
||||||
const auto& node = yaml["video"];
|
const auto& node = yaml["video"];
|
||||||
readField(node, "vsync", Options::video.vsync);
|
readField(node, "vsync", g_video.vsync);
|
||||||
if (node.contains("scaling_mode")) {
|
if (node.contains("scaling_mode")) {
|
||||||
Options::video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value<std::string>());
|
g_video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value<std::string>());
|
||||||
}
|
}
|
||||||
if (node.contains("texture_filter")) {
|
if (node.contains("texture_filter")) {
|
||||||
Options::video.texture_filter = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value<std::string>());
|
g_video.texture_filter = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value<std::string>());
|
||||||
}
|
}
|
||||||
if (node.contains("render_info_position")) {
|
if (node.contains("render_info_position")) {
|
||||||
Options::video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value<std::string>());
|
g_video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value<std::string>());
|
||||||
}
|
}
|
||||||
readField(node, "render_info_show_time", Options::video.render_info_show_time);
|
readField(node, "render_info_show_time", g_video.render_info_show_time);
|
||||||
parseGpu(node);
|
parseGpu(node);
|
||||||
parseShader(node);
|
parseShader(node);
|
||||||
}
|
}
|
||||||
@@ -134,22 +149,22 @@ namespace {
|
|||||||
void parseMusic(const fkyaml::node& audio_node) {
|
void parseMusic(const fkyaml::node& audio_node) {
|
||||||
if (!audio_node.contains("music")) { return; }
|
if (!audio_node.contains("music")) { return; }
|
||||||
const auto& node = audio_node["music"];
|
const auto& node = audio_node["music"];
|
||||||
readField(node, "enabled", Options::audio.music.enabled);
|
readField(node, "enabled", g_audio.music.enabled);
|
||||||
readField(node, "volume", Options::audio.music.volume);
|
readField(node, "volume", g_audio.music.volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseSound(const fkyaml::node& audio_node) {
|
void parseSound(const fkyaml::node& audio_node) {
|
||||||
if (!audio_node.contains("sound")) { return; }
|
if (!audio_node.contains("sound")) { return; }
|
||||||
const auto& node = audio_node["sound"];
|
const auto& node = audio_node["sound"];
|
||||||
readField(node, "enabled", Options::audio.sound.enabled);
|
readField(node, "enabled", g_audio.sound.enabled);
|
||||||
readField(node, "volume", Options::audio.sound.volume);
|
readField(node, "volume", g_audio.sound.volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
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"];
|
||||||
readField(node, "enabled", Options::audio.enabled);
|
readField(node, "enabled", g_audio.enabled);
|
||||||
readField(node, "volume", Options::audio.volume);
|
readField(node, "volume", g_audio.volume);
|
||||||
parseMusic(node);
|
parseMusic(node);
|
||||||
parseSound(node);
|
parseSound(node);
|
||||||
}
|
}
|
||||||
@@ -423,14 +438,65 @@ namespace {
|
|||||||
|
|
||||||
namespace Options {
|
namespace Options {
|
||||||
|
|
||||||
|
// --- Getters de l'estat ---
|
||||||
|
auto window() -> const Window& { return g_window; }
|
||||||
|
auto video() -> const Video& { return g_video; }
|
||||||
|
auto audio() -> const Audio& { return g_audio; }
|
||||||
|
auto locale() -> const std::string& { return g_locale; }
|
||||||
|
auto configVersion() -> const std::string& { return g_config_version; }
|
||||||
|
auto postfxPresets() -> const std::vector<PostFXPreset>& { return g_postfx_presets; }
|
||||||
|
auto crtpiPresets() -> const std::vector<CrtPiPreset>& { return g_crtpi_presets; }
|
||||||
|
|
||||||
|
// --- Setters: muten + persisteixen ---
|
||||||
|
void setWindowZoom(int value) {
|
||||||
|
g_window.zoom = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setWindowFullscreen(bool value) {
|
||||||
|
g_window.fullscreen = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setVideoVSync(bool value) {
|
||||||
|
g_video.vsync = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setVideoScalingMode(Defaults::ScalingMode value) {
|
||||||
|
g_video.scaling_mode = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setVideoRenderInfoPosition(Defaults::RenderInfoPosition value) {
|
||||||
|
g_video.render_info_position = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setVideoRenderInfoShowTime(bool value) {
|
||||||
|
g_video.render_info_show_time = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setShaderEnabled(bool value) {
|
||||||
|
g_video.shader.enabled = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setActiveShaderName(const std::string& value) {
|
||||||
|
g_video.shader.current_shader = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setPostFXPreset(const std::string& value) {
|
||||||
|
g_video.shader.current_postfx_preset = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
void setCrtPiPreset(const std::string& value) {
|
||||||
|
g_video.shader.current_crtpi_preset = value;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
void setConfigFile(const std::string& path) {
|
void setConfigFile(const std::string& path) {
|
||||||
config_file_path = path;
|
g_config_file_path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto loadFromFile() -> bool {
|
auto loadFromFile() -> bool {
|
||||||
if (config_file_path.empty()) { return false; }
|
if (g_config_file_path.empty()) { return false; }
|
||||||
|
|
||||||
const std::string CONTENT = readFileOrEmpty(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ó: creem el fitxer amb els valors per defecte.
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
@@ -449,12 +515,12 @@ namespace Options {
|
|||||||
if (file_version != Texts::VERSION) {
|
if (file_version != Texts::VERSION) {
|
||||||
std::cout << "Options: config.yaml versió \"" << file_version
|
std::cout << "Options: config.yaml versió \"" << file_version
|
||||||
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
|
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
|
||||||
backupFile(config_file_path);
|
backupFile(g_config_file_path);
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
config_version = file_version;
|
g_config_version = file_version;
|
||||||
|
|
||||||
if (yaml.contains("locale")) { Options::locale = yaml["locale"].get_value<std::string>(); }
|
if (yaml.contains("locale")) { g_locale = yaml["locale"].get_value<std::string>(); }
|
||||||
parseWindow(yaml);
|
parseWindow(yaml);
|
||||||
parseVideo(yaml);
|
parseVideo(yaml);
|
||||||
parseAudio(yaml);
|
parseAudio(yaml);
|
||||||
@@ -462,84 +528,84 @@ namespace Options {
|
|||||||
} 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';
|
||||||
std::cout << "Options: reescrivint el fitxer amb defaults.\n";
|
std::cout << "Options: reescrivint el fitxer amb defaults.\n";
|
||||||
backupFile(config_file_path);
|
backupFile(g_config_file_path);
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto saveToFile() -> bool {
|
auto saveToFile() -> bool {
|
||||||
if (config_file_path.empty()) { return false; }
|
if (g_config_file_path.empty()) { return false; }
|
||||||
|
|
||||||
config_version = Texts::VERSION;
|
g_config_version = Texts::VERSION;
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n";
|
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n";
|
||||||
out << "version: \"" << config_version << "\"\n";
|
out << "version: \"" << g_config_version << "\"\n";
|
||||||
out << "locale: \"" << locale << "\"\n\n";
|
out << "locale: \"" << g_locale << "\"\n\n";
|
||||||
|
|
||||||
out << "window:\n";
|
out << "window:\n";
|
||||||
out << " zoom: " << window.zoom << "\n";
|
out << " zoom: " << g_window.zoom << "\n";
|
||||||
out << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n\n";
|
out << " fullscreen: " << (g_window.fullscreen ? "true" : "false") << "\n\n";
|
||||||
|
|
||||||
out << "video:\n";
|
out << "video:\n";
|
||||||
out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
out << " vsync: " << (g_video.vsync ? "true" : "false") << "\n";
|
||||||
out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, video.scaling_mode) << "\"\n";
|
out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, g_video.scaling_mode) << "\"\n";
|
||||||
out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, video.texture_filter) << "\"\n";
|
out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, g_video.texture_filter) << "\"\n";
|
||||||
out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, video.render_info_position) << "\"\n";
|
out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, g_video.render_info_position) << "\"\n";
|
||||||
out << " render_info_show_time: " << (video.render_info_show_time ? "true" : "false") << "\n";
|
out << " render_info_show_time: " << (g_video.render_info_show_time ? "true" : "false") << "\n";
|
||||||
out << " gpu:\n";
|
out << " gpu:\n";
|
||||||
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
|
out << " acceleration: " << (g_video.gpu.acceleration ? "true" : "false") << "\n";
|
||||||
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
|
out << " preferred_driver: \"" << g_video.gpu.preferred_driver << "\"\n";
|
||||||
out << " shader:\n";
|
out << " shader:\n";
|
||||||
out << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
|
out << " enabled: " << (g_video.shader.enabled ? "true" : "false") << "\n";
|
||||||
out << " current_shader: \"" << video.shader.current_shader << "\"\n";
|
out << " current_shader: \"" << g_video.shader.current_shader << "\"\n";
|
||||||
out << " current_postfx_preset: \"" << video.shader.current_postfx_preset << "\"\n";
|
out << " current_postfx_preset: \"" << g_video.shader.current_postfx_preset << "\"\n";
|
||||||
out << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset << "\"\n\n";
|
out << " current_crtpi_preset: \"" << g_video.shader.current_crtpi_preset << "\"\n\n";
|
||||||
|
|
||||||
out << "audio:\n";
|
out << "audio:\n";
|
||||||
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
|
out << " enabled: " << (g_audio.enabled ? "true" : "false") << "\n";
|
||||||
out << " volume: " << audio.volume << "\n";
|
out << " volume: " << g_audio.volume << "\n";
|
||||||
out << " music:\n";
|
out << " music:\n";
|
||||||
out << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n";
|
out << " enabled: " << (g_audio.music.enabled ? "true" : "false") << "\n";
|
||||||
out << " volume: " << audio.music.volume << "\n";
|
out << " volume: " << g_audio.music.volume << "\n";
|
||||||
out << " sound:\n";
|
out << " sound:\n";
|
||||||
out << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n";
|
out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\n";
|
||||||
out << " volume: " << audio.sound.volume << "\n";
|
out << " volume: " << g_audio.sound.volume << "\n";
|
||||||
|
|
||||||
return writeFile(config_file_path, out.str());
|
return writeFile(g_config_file_path, out.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto buildScreenConfig() -> Screen::Config {
|
auto buildScreenConfig() -> Screen::Config {
|
||||||
return {
|
return {
|
||||||
.zoom = window.zoom,
|
.zoom = g_window.zoom,
|
||||||
.fullscreen = window.fullscreen,
|
.fullscreen = g_window.fullscreen,
|
||||||
.vsync = video.vsync,
|
.vsync = g_video.vsync,
|
||||||
.scaling_mode = video.scaling_mode,
|
.scaling_mode = g_video.scaling_mode,
|
||||||
.texture_filter = video.texture_filter,
|
.texture_filter = g_video.texture_filter,
|
||||||
.gpu_acceleration = video.gpu.acceleration,
|
.gpu_acceleration = g_video.gpu.acceleration,
|
||||||
.preferred_driver = video.gpu.preferred_driver,
|
.preferred_driver = g_video.gpu.preferred_driver,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto buildAudioConfig() -> ::Audio::Config {
|
auto buildAudioConfig() -> ::Audio::Config {
|
||||||
return {
|
return {
|
||||||
.enabled = audio.enabled,
|
.enabled = g_audio.enabled,
|
||||||
.volume = audio.volume,
|
.volume = g_audio.volume,
|
||||||
.music_enabled = audio.music.enabled,
|
.music_enabled = g_audio.music.enabled,
|
||||||
.music_volume = audio.music.volume,
|
.music_volume = g_audio.music.volume,
|
||||||
.sound_enabled = audio.sound.enabled,
|
.sound_enabled = g_audio.sound.enabled,
|
||||||
.sound_volume = audio.sound.volume,
|
.sound_volume = g_audio.sound.volume,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPostFXFile(const std::string& path) {
|
void setPostFXFile(const std::string& path) {
|
||||||
postfx_file_path = path;
|
g_postfx_file_path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto loadPostFXFromFile() -> bool {
|
auto loadPostFXFromFile() -> bool {
|
||||||
return loadPresetsFromFile<PostFXPreset>(
|
return loadPresetsFromFile<PostFXPreset>(
|
||||||
postfx_file_path,
|
g_postfx_file_path,
|
||||||
postfx_presets,
|
g_postfx_presets,
|
||||||
&defaultPostFXPresets,
|
&defaultPostFXPresets,
|
||||||
&parsePostFXPreset,
|
&parsePostFXPreset,
|
||||||
&savePostFXToFile,
|
&savePostFXToFile,
|
||||||
@@ -548,21 +614,21 @@ namespace Options {
|
|||||||
|
|
||||||
auto savePostFXToFile() -> bool {
|
auto savePostFXToFile() -> bool {
|
||||||
return savePresetsToFile<PostFXPreset>(
|
return savePresetsToFile<PostFXPreset>(
|
||||||
postfx_file_path,
|
g_postfx_file_path,
|
||||||
postfx_presets,
|
g_postfx_presets,
|
||||||
&writePostFXPreset,
|
&writePostFXPreset,
|
||||||
"Presets PostFX per a AEE 2026. Editable a mà.",
|
"Presets PostFX per a AEE 2026. Editable a mà.",
|
||||||
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
|
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCrtPiFile(const std::string& path) {
|
void setCrtPiFile(const std::string& path) {
|
||||||
crtpi_file_path = path;
|
g_crtpi_file_path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto loadCrtPiFromFile() -> bool {
|
auto loadCrtPiFromFile() -> bool {
|
||||||
return loadPresetsFromFile<CrtPiPreset>(
|
return loadPresetsFromFile<CrtPiPreset>(
|
||||||
crtpi_file_path,
|
g_crtpi_file_path,
|
||||||
crtpi_presets,
|
g_crtpi_presets,
|
||||||
&defaultCrtPiPresets,
|
&defaultCrtPiPresets,
|
||||||
&parseCrtPiPreset,
|
&parseCrtPiPreset,
|
||||||
&saveCrtPiToFile,
|
&saveCrtPiToFile,
|
||||||
@@ -571,29 +637,29 @@ namespace Options {
|
|||||||
|
|
||||||
auto saveCrtPiToFile() -> bool {
|
auto saveCrtPiToFile() -> bool {
|
||||||
return savePresetsToFile<CrtPiPreset>(
|
return savePresetsToFile<CrtPiPreset>(
|
||||||
crtpi_file_path,
|
g_crtpi_file_path,
|
||||||
crtpi_presets,
|
g_crtpi_presets,
|
||||||
&writeCrtPiPreset,
|
&writeCrtPiPreset,
|
||||||
"Presets CRT-Pi per a AEE 2026. Editable a mà.",
|
"Presets CRT-Pi per a AEE 2026. Editable a mà.",
|
||||||
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
|
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto buildActiveShader() -> Rendering::ShaderType {
|
auto buildActiveShader() -> Rendering::ShaderType {
|
||||||
if (video.shader.current_shader == "crtpi") {
|
if (g_video.shader.current_shader == "crtpi") {
|
||||||
return Rendering::ShaderType::CRTPI;
|
return Rendering::ShaderType::CRTPI;
|
||||||
}
|
}
|
||||||
return Rendering::ShaderType::POSTFX;
|
return Rendering::ShaderType::POSTFX;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame {
|
auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame {
|
||||||
const std::string& active_preset = (video.shader.current_shader == "crtpi")
|
const std::string& active_preset = (g_video.shader.current_shader == "crtpi")
|
||||||
? video.shader.current_crtpi_preset
|
? g_video.shader.current_crtpi_preset
|
||||||
: video.shader.current_postfx_preset;
|
: g_video.shader.current_postfx_preset;
|
||||||
return {
|
return {
|
||||||
.target_position = video.render_info_position,
|
.target_position = g_video.render_info_position,
|
||||||
.show_time = video.render_info_show_time,
|
.show_time = g_video.render_info_show_time,
|
||||||
.shader_enabled = video.shader.enabled,
|
.shader_enabled = g_video.shader.enabled,
|
||||||
.shader_name = video.shader.current_shader,
|
.shader_name = g_video.shader.current_shader,
|
||||||
.shader_preset = active_preset,
|
.shader_preset = active_preset,
|
||||||
.text_color = text_color,
|
.text_color = text_color,
|
||||||
.shadow_color = shadow_color,
|
.shadow_color = shadow_color,
|
||||||
@@ -603,12 +669,12 @@ namespace Options {
|
|||||||
auto buildCrtPiParams() -> Rendering::CrtPiParams {
|
auto buildCrtPiParams() -> Rendering::CrtPiParams {
|
||||||
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
|
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
|
||||||
// PostFX passthrough, però sent nets per si algú consulta aquests params).
|
// PostFX passthrough, però sent nets per si algú consulta aquests params).
|
||||||
if (!video.shader.enabled) { return {}; }
|
if (!g_video.shader.enabled) { return {}; }
|
||||||
|
|
||||||
const auto IT = std::ranges::find_if(crtpi_presets,
|
const auto IT = std::ranges::find_if(g_crtpi_presets,
|
||||||
[](const CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; });
|
[](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
|
||||||
if (IT == crtpi_presets.end()) {
|
if (IT == g_crtpi_presets.end()) {
|
||||||
std::cout << "Options: preset CrtPi \"" << video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
|
std::cout << "Options: preset CrtPi \"" << g_video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -631,12 +697,12 @@ namespace Options {
|
|||||||
|
|
||||||
auto buildPostFXParams() -> Rendering::PostFXParams {
|
auto buildPostFXParams() -> Rendering::PostFXParams {
|
||||||
// Si el shader està deshabilitat, retornem params a zero → passthrough.
|
// Si el shader està deshabilitat, retornem params a zero → passthrough.
|
||||||
if (!video.shader.enabled) { return {}; }
|
if (!g_video.shader.enabled) { return {}; }
|
||||||
|
|
||||||
const auto IT = std::ranges::find_if(postfx_presets,
|
const auto IT = std::ranges::find_if(g_postfx_presets,
|
||||||
[](const PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; });
|
[](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
|
||||||
if (IT == postfx_presets.end()) {
|
if (IT == g_postfx_presets.end()) {
|
||||||
std::cout << "Options: preset PostFX \"" << video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
|
std::cout << "Options: preset PostFX \"" << g_video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -13,6 +13,15 @@
|
|||||||
// del config.yaml. Els subsistemes (Screen, Audio, ...) reben structs tipats
|
// del config.yaml. Els subsistemes (Screen, Audio, ...) reben structs tipats
|
||||||
// a través dels build*Config(), de manera que no queden acoblats a aquest
|
// a través dels build*Config(), de manera que no queden acoblats a aquest
|
||||||
// namespace ni al format concret del fitxer.
|
// namespace ni al format concret del fitxer.
|
||||||
|
//
|
||||||
|
// L'estat mutable viu en un anonymous namespace dins d'`options.cpp`: els
|
||||||
|
// callers el llegeixen via getters const-ref (`window()`, `video()`, ...) i
|
||||||
|
// el muten via setters específics que persisteixen a config.yaml. Els
|
||||||
|
// setters NO notifiquen els subsistemes (Screen/Audio): el caller (típic.
|
||||||
|
// `core/input/global_inputs.cpp`) crida després el setter del subsistema.
|
||||||
|
// Aquesta asimetria preserva la direcció `Director → Screen/Audio` i evita
|
||||||
|
// que Options aprenga del layout dels subsistemes (decisió WONTFIX d'aquest
|
||||||
|
// refactor: veure docs/audit-game.md → COUP-03).
|
||||||
namespace Options {
|
namespace Options {
|
||||||
|
|
||||||
struct Window {
|
struct Window {
|
||||||
@@ -95,20 +104,30 @@ namespace Options {
|
|||||||
Sound sound{};
|
Sound sound{};
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Estat global (inline vars per a que una sola definició viva a tot
|
// --- Getters de l'estat (read-only) ---
|
||||||
// el programa sense necessitar un .cpp extra per a cada una) ---
|
[[nodiscard]] auto window() -> const Window&;
|
||||||
inline std::string config_version{};
|
[[nodiscard]] auto video() -> const Video&;
|
||||||
inline std::string config_file_path{};
|
[[nodiscard]] auto audio() -> const Audio&;
|
||||||
inline std::string postfx_file_path{};
|
[[nodiscard]] auto locale() -> const std::string&;
|
||||||
inline std::string crtpi_file_path{};
|
[[nodiscard]] auto configVersion() -> const std::string&;
|
||||||
inline std::string locale{Defaults::I18n::LOCALE};
|
[[nodiscard]] auto postfxPresets() -> const std::vector<PostFXPreset>&;
|
||||||
inline Window window{};
|
[[nodiscard]] auto crtpiPresets() -> const std::vector<CrtPiPreset>&;
|
||||||
inline Video video{};
|
|
||||||
inline Audio audio{};
|
|
||||||
inline std::vector<PostFXPreset> postfx_presets{};
|
|
||||||
inline std::vector<CrtPiPreset> crtpi_presets{};
|
|
||||||
|
|
||||||
// --- API pública ---
|
// --- Setters: muten + persisteixen a config.yaml ---
|
||||||
|
// No notifiquen els subsistemes; el caller ha de propagar el canvi al
|
||||||
|
// subsistema corresponent (p.ex. Screen::setWindowZoom) per separat.
|
||||||
|
void setWindowZoom(int value);
|
||||||
|
void setWindowFullscreen(bool value);
|
||||||
|
void setVideoVSync(bool value);
|
||||||
|
void setVideoScalingMode(Defaults::ScalingMode value);
|
||||||
|
void setVideoRenderInfoPosition(Defaults::RenderInfoPosition value);
|
||||||
|
void setVideoRenderInfoShowTime(bool value);
|
||||||
|
void setShaderEnabled(bool value);
|
||||||
|
void setActiveShaderName(const std::string& value); // "postfx" | "crtpi"
|
||||||
|
void setPostFXPreset(const std::string& value);
|
||||||
|
void setCrtPiPreset(const std::string& value);
|
||||||
|
|
||||||
|
// --- API pública: càrrega/desament de fitxers ---
|
||||||
void setConfigFile(const std::string& path);
|
void setConfigFile(const std::string& path);
|
||||||
auto loadFromFile() -> bool;
|
auto loadFromFile() -> bool;
|
||||||
auto saveToFile() -> bool;
|
auto saveToFile() -> bool;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "core/rendering/render_info.hpp"
|
#include "core/rendering/render_info.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "game/entities/entity.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
@@ -97,7 +98,7 @@ GameScene::GameScene(Config config)
|
|||||||
static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
||||||
const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
||||||
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
||||||
players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1));
|
players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo()));
|
||||||
|
|
||||||
// Connectem el Player al bus perquè emeti `on_tile_entered` i els
|
// Connectem el Player al bus perquè emeti `on_tile_entered` i els
|
||||||
// sarcòfags rebin les notificacions de pas per tile.
|
// sarcòfags rebin les notificacions de pas per tile.
|
||||||
@@ -123,6 +124,17 @@ void GameScene::update(int delta_ms) {
|
|||||||
// als sarcòfags via la subscripció del bus, i aquest `update` pot
|
// als sarcòfags via la subscripció del bus, i aquest `update` pot
|
||||||
// aplicar la transició OPENING → OPEN en el mateix frame.
|
// aplicar la transició OPENING → OPEN en el mateix frame.
|
||||||
entity_manager_.update(DT);
|
entity_manager_.update(DT);
|
||||||
|
|
||||||
|
// Detecció de contacte player↔entitat després que totes les entitats
|
||||||
|
// hagin mogut aquest frame. La porta (si està oberta) emet
|
||||||
|
// `level_complete`; la momia/trampa emet `player_hit`. Els sarcs, encara
|
||||||
|
// que tinguen collides=true, tenen la caixa lògica fora del camí del
|
||||||
|
// jugador (gràcies a Map::collides al Player::update), així que aquí
|
||||||
|
// no generen falsos contactes.
|
||||||
|
std::vector<Player*> player_ptrs;
|
||||||
|
player_ptrs.reserve(players_.size());
|
||||||
|
std::ranges::transform(players_, std::back_inserter(player_ptrs), [](const auto& p) { return p.get(); });
|
||||||
|
entity_manager_.checkPlayerCollisions(player_ptrs);
|
||||||
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
}
|
}
|
||||||
@@ -137,25 +149,33 @@ void GameScene::render() {
|
|||||||
// col·lisions del jugador.
|
// col·lisions del jugador.
|
||||||
tilemap_.render();
|
tilemap_.render();
|
||||||
|
|
||||||
// Entitats (sarcòfags, porta, momia, antorxa...) dibuixades per sobre
|
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
||||||
// dels tiles però sota dels players. Z-order fi (interleaving per feetY
|
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
||||||
// amb els sarcs) queda per a una tasca posterior; "sin pulido".
|
// els més avall al davant. Així un sarc situat per darrere d'un player
|
||||||
entity_manager_.renderAll();
|
// queda tapat per ell, i un sarc a davant el tapa a ell.
|
||||||
|
struct Renderable {
|
||||||
// Amb un sol jugador, l'ordre és irrellevant i ens estalviem el vector
|
float feet_y;
|
||||||
// temporal. Quan n'hi haja més d'un (o quan els sarcs siguen sprites)
|
Entity* entity; // nullable; si és nullptr, s'usa `player`.
|
||||||
// caldrà ordenar per `getFeetY()` ascendent i interleaveing amb les
|
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
||||||
// files de sarcs — l'ordre ascendent manté els més al fons darrere.
|
};
|
||||||
if (players_.size() <= 1) {
|
std::vector<Renderable> order;
|
||||||
for (auto& player : players_) { player->render(); }
|
order.reserve(entity_manager_.entityCount() + players_.size());
|
||||||
} else {
|
const auto ENTITIES = entity_manager_.getEntities();
|
||||||
std::vector<Player*> sorted;
|
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||||||
sorted.reserve(players_.size());
|
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||||||
std::ranges::transform(players_, std::back_inserter(sorted), [](const auto& p) { return p.get(); });
|
|
||||||
std::ranges::sort(sorted, [](const Player* a, const Player* b) {
|
|
||||||
return a->getFeetY() < b->getFeetY();
|
|
||||||
});
|
});
|
||||||
for (auto* player : sorted) { player->render(); }
|
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||||||
|
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
||||||
|
});
|
||||||
|
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
||||||
|
return a.feet_y < b.feet_y;
|
||||||
|
});
|
||||||
|
for (const auto& r : order) {
|
||||||
|
if (r.entity != nullptr) {
|
||||||
|
EntityManager::renderEntity(*r.entity);
|
||||||
|
} else if (r.player != nullptr) {
|
||||||
|
r.player->render();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifier::get().render();
|
Notifier::get().render();
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "game/scenes/intro_scene.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/input/global_inputs.hpp"
|
||||||
|
#include "core/rendering/notifier.hpp"
|
||||||
|
#include "core/rendering/render_info.hpp"
|
||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/text.hpp"
|
||||||
|
#include "core/system/event_buffer.hpp"
|
||||||
|
#include "game/options.hpp"
|
||||||
|
#include "game/scenes/placeholder_input.hpp"
|
||||||
|
#include "game/ui/ui_palette.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int NAME_Y_OFFSET = -8;
|
||||||
|
constexpr int TIMER_Y_OFFSET = 8;
|
||||||
|
// TODO: migrar a un logger unificat quan es decideixi (veure INC-03 a
|
||||||
|
// docs/audit-core-rendering.md).
|
||||||
|
const char* const SCENE_NAME = "INTRO";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
IntroScene::IntroScene(std::shared_ptr<Text> font, SceneFactory next_factory)
|
||||||
|
: font_(std::move(font)),
|
||||||
|
next_factory_(std::move(next_factory)) {
|
||||||
|
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
IntroScene::~IntroScene() = default;
|
||||||
|
|
||||||
|
void IntroScene::update(int delta_ms) {
|
||||||
|
GlobalInputs::update(delta_ms);
|
||||||
|
if (GlobalInputs::wantsQuit()) {
|
||||||
|
wants_quit_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timer_ms_ -= delta_ms;
|
||||||
|
|
||||||
|
const bool SKIPPED = std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) {
|
||||||
|
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((timer_ms_ <= 0 || SKIPPED) && next_factory_) {
|
||||||
|
next_scene_ = next_factory_();
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifier::get().update(delta_ms);
|
||||||
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntroScene::render() {
|
||||||
|
constexpr std::uint32_t BLACK_ARGB = 0xFF000000U;
|
||||||
|
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB);
|
||||||
|
|
||||||
|
if (font_) {
|
||||||
|
const int SECS_LEFT = std::max(1, (timer_ms_ + 999) / 1000);
|
||||||
|
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
GameCanvas::CENTER_X,
|
||||||
|
GameCanvas::CENTER_Y + NAME_Y_OFFSET,
|
||||||
|
SCENE_NAME,
|
||||||
|
1,
|
||||||
|
Defaults::UI::NOTIFICATION_TEXT);
|
||||||
|
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
GameCanvas::CENTER_X,
|
||||||
|
GameCanvas::CENTER_Y + TIMER_Y_OFFSET,
|
||||||
|
std::to_string(SECS_LEFT),
|
||||||
|
1,
|
||||||
|
Defaults::UI::NOTIFICATION_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifier::get().render();
|
||||||
|
RenderInfo::get().render();
|
||||||
|
Screen::get().present();
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "core/scene/scene.hpp"
|
||||||
|
|
||||||
|
class Text;
|
||||||
|
|
||||||
|
// Escena placeholder de la introducció. Com la resta de placeholders, durada
|
||||||
|
// fixa i skip amb tecla no reservada; quan expira encadena a la factory
|
||||||
|
// injectada (en producció: GameScene).
|
||||||
|
class IntroScene : public Scene {
|
||||||
|
public:
|
||||||
|
using SceneFactory = std::function<std::unique_ptr<Scene>()>;
|
||||||
|
|
||||||
|
static constexpr int DURATION_MS = 5000;
|
||||||
|
|
||||||
|
IntroScene(std::shared_ptr<Text> font, SceneFactory next_factory);
|
||||||
|
~IntroScene() override;
|
||||||
|
|
||||||
|
void update(int delta_ms) override;
|
||||||
|
void render() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Text> font_;
|
||||||
|
SceneFactory next_factory_;
|
||||||
|
int timer_ms_{DURATION_MS};
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#include "game/scenes/logo_scene.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/input/global_inputs.hpp"
|
||||||
|
#include "core/rendering/notifier.hpp"
|
||||||
|
#include "core/rendering/render_info.hpp"
|
||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/text.hpp"
|
||||||
|
#include "core/system/event_buffer.hpp"
|
||||||
|
#include "game/options.hpp"
|
||||||
|
#include "game/scenes/placeholder_input.hpp"
|
||||||
|
#include "game/ui/ui_palette.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int NAME_Y_OFFSET = -8;
|
||||||
|
constexpr int TIMER_Y_OFFSET = 8;
|
||||||
|
// TODO: migrar a un logger unificat quan es decideixi (veure INC-03 a
|
||||||
|
// docs/audit-core-rendering.md). De moment std::cout per rastrejar el
|
||||||
|
// flux d'escenes als smoke runs.
|
||||||
|
const char* const SCENE_NAME = "LOGO";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
LogoScene::LogoScene(std::shared_ptr<Text> font, SceneFactory next_factory)
|
||||||
|
: font_(std::move(font)),
|
||||||
|
next_factory_(std::move(next_factory)) {
|
||||||
|
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
LogoScene::~LogoScene() = default;
|
||||||
|
|
||||||
|
void LogoScene::update(int delta_ms) {
|
||||||
|
GlobalInputs::update(delta_ms);
|
||||||
|
if (GlobalInputs::wantsQuit()) {
|
||||||
|
wants_quit_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timer_ms_ -= delta_ms;
|
||||||
|
|
||||||
|
const bool SKIPPED = std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) {
|
||||||
|
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((timer_ms_ <= 0 || SKIPPED) && next_factory_) {
|
||||||
|
next_scene_ = next_factory_();
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifier::get().update(delta_ms);
|
||||||
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogoScene::render() {
|
||||||
|
constexpr std::uint32_t BLACK_ARGB = 0xFF000000U;
|
||||||
|
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB);
|
||||||
|
|
||||||
|
if (font_) {
|
||||||
|
const int SECS_LEFT = std::max(1, (timer_ms_ + 999) / 1000);
|
||||||
|
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
GameCanvas::CENTER_X,
|
||||||
|
GameCanvas::CENTER_Y + NAME_Y_OFFSET,
|
||||||
|
SCENE_NAME,
|
||||||
|
1,
|
||||||
|
Defaults::UI::NOTIFICATION_TEXT);
|
||||||
|
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
GameCanvas::CENTER_X,
|
||||||
|
GameCanvas::CENTER_Y + TIMER_Y_OFFSET,
|
||||||
|
std::to_string(SECS_LEFT),
|
||||||
|
1,
|
||||||
|
Defaults::UI::NOTIFICATION_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifier::get().render();
|
||||||
|
RenderInfo::get().render();
|
||||||
|
Screen::get().present();
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "core/scene/scene.hpp"
|
||||||
|
|
||||||
|
class Text;
|
||||||
|
|
||||||
|
// Escena placeholder del logotip. Durada fixa (5 s) o skip amb qualsevol tecla
|
||||||
|
// no reservada. Quan expira, encadena amb la `SceneFactory` que se li injecta
|
||||||
|
// al ctor — així l'escena no coneix el tipus concret de la següent, i es pot
|
||||||
|
// exercir en tests sense arrossegar GameScene.
|
||||||
|
class LogoScene : public Scene {
|
||||||
|
public:
|
||||||
|
using SceneFactory = std::function<std::unique_ptr<Scene>()>;
|
||||||
|
|
||||||
|
static constexpr int DURATION_MS = 5000;
|
||||||
|
|
||||||
|
LogoScene(std::shared_ptr<Text> font, SceneFactory next_factory);
|
||||||
|
~LogoScene() override;
|
||||||
|
|
||||||
|
void update(int delta_ms) override;
|
||||||
|
void render() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Text> font_;
|
||||||
|
SceneFactory next_factory_;
|
||||||
|
int timer_ms_{DURATION_MS};
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#include "game/scenes/placeholder_input.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "core/input/key_config.hpp"
|
||||||
|
|
||||||
|
namespace Placeholder {
|
||||||
|
|
||||||
|
auto isReservedScancode(SDL_Scancode sc) -> bool {
|
||||||
|
if (sc == SDL_SCANCODE_UNKNOWN) { return false; }
|
||||||
|
const auto& bindings = KeyConfig::get().bindings();
|
||||||
|
return std::ranges::any_of(bindings, [sc](const auto& kv) { return kv.second == sc; });
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Placeholder
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
// Helper per als placeholders del flux LOGO → TITLE → INTRO → GAME. Les escenes
|
||||||
|
// placeholder avancen amb qualsevol tecla excepte les registrades a KeyConfig
|
||||||
|
// (F1–F12, Escape), que mantenen el seu comportament habitual via GlobalInputs.
|
||||||
|
namespace Placeholder {
|
||||||
|
|
||||||
|
// Retorna true si el scancode coincideix amb alguna tecla declarada al
|
||||||
|
// KeyConfig::get() (les carregades de data/input/keys.yaml). Si KeyConfig
|
||||||
|
// no s'ha carregat, totes les consultes retornaran SDL_SCANCODE_UNKNOWN i
|
||||||
|
// aquesta funció tornarà false, de manera que el test/smoke no depèn de
|
||||||
|
// l'estat global de la configuració.
|
||||||
|
[[nodiscard]] auto isReservedScancode(SDL_Scancode sc) -> bool;
|
||||||
|
|
||||||
|
} // namespace Placeholder
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "game/scenes/title_scene.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/input/global_inputs.hpp"
|
||||||
|
#include "core/rendering/notifier.hpp"
|
||||||
|
#include "core/rendering/render_info.hpp"
|
||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/text.hpp"
|
||||||
|
#include "core/system/event_buffer.hpp"
|
||||||
|
#include "game/options.hpp"
|
||||||
|
#include "game/scenes/placeholder_input.hpp"
|
||||||
|
#include "game/ui/ui_palette.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int NAME_Y_OFFSET = -8;
|
||||||
|
constexpr int TIMER_Y_OFFSET = 8;
|
||||||
|
// TODO: migrar a un logger unificat quan es decideixi (veure INC-03 a
|
||||||
|
// docs/audit-core-rendering.md).
|
||||||
|
const char* const SCENE_NAME = "TITLE";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TitleScene::TitleScene(std::shared_ptr<Text> font, SceneFactory next_factory)
|
||||||
|
: font_(std::move(font)),
|
||||||
|
next_factory_(std::move(next_factory)) {
|
||||||
|
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
TitleScene::~TitleScene() = default;
|
||||||
|
|
||||||
|
void TitleScene::update(int delta_ms) {
|
||||||
|
GlobalInputs::update(delta_ms);
|
||||||
|
if (GlobalInputs::wantsQuit()) {
|
||||||
|
wants_quit_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timer_ms_ -= delta_ms;
|
||||||
|
|
||||||
|
const bool SKIPPED = std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) {
|
||||||
|
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((timer_ms_ <= 0 || SKIPPED) && next_factory_) {
|
||||||
|
next_scene_ = next_factory_();
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifier::get().update(delta_ms);
|
||||||
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleScene::render() {
|
||||||
|
constexpr std::uint32_t BLACK_ARGB = 0xFF000000U;
|
||||||
|
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB);
|
||||||
|
|
||||||
|
if (font_) {
|
||||||
|
const int SECS_LEFT = std::max(1, (timer_ms_ + 999) / 1000);
|
||||||
|
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
GameCanvas::CENTER_X,
|
||||||
|
GameCanvas::CENTER_Y + NAME_Y_OFFSET,
|
||||||
|
SCENE_NAME,
|
||||||
|
1,
|
||||||
|
Defaults::UI::NOTIFICATION_TEXT);
|
||||||
|
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||||
|
GameCanvas::CENTER_X,
|
||||||
|
GameCanvas::CENTER_Y + TIMER_Y_OFFSET,
|
||||||
|
std::to_string(SECS_LEFT),
|
||||||
|
1,
|
||||||
|
Defaults::UI::NOTIFICATION_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifier::get().render();
|
||||||
|
RenderInfo::get().render();
|
||||||
|
Screen::get().present();
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "core/scene/scene.hpp"
|
||||||
|
|
||||||
|
class Text;
|
||||||
|
|
||||||
|
// Escena placeholder del títol. Mateix comportament que LogoScene — duració
|
||||||
|
// fixa de 5 s i skip amb tecla no reservada —, però classe separada perquè
|
||||||
|
// quan divergeixi en contingut real (menú, seleccions, animacions) creixi en
|
||||||
|
// el seu propi fitxer sense haver de refactoritzar cap base comuna.
|
||||||
|
class TitleScene : public Scene {
|
||||||
|
public:
|
||||||
|
using SceneFactory = std::function<std::unique_ptr<Scene>()>;
|
||||||
|
|
||||||
|
static constexpr int DURATION_MS = 5000;
|
||||||
|
|
||||||
|
TitleScene(std::shared_ptr<Text> font, SceneFactory next_factory);
|
||||||
|
~TitleScene() override;
|
||||||
|
|
||||||
|
void update(int delta_ms) override;
|
||||||
|
void render() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Text> font_;
|
||||||
|
SceneFactory next_factory_;
|
||||||
|
int timer_ms_{DURATION_MS};
|
||||||
|
};
|
||||||
@@ -90,6 +90,24 @@ namespace Defaults {
|
|||||||
// pel codi. Si algun dia es volen temes múltiples, es pot moure a YAML
|
// pel codi. Si algun dia es volen temes múltiples, es pot moure a YAML
|
||||||
// sense més impacte que dos accessors extra.
|
// sense més impacte que dos accessors extra.
|
||||||
namespace UI {
|
namespace UI {
|
||||||
|
// Fonts d'UI centralitzades: un únic lloc per conèixer la carpeta
|
||||||
|
// (data/fonts/), el nom-clau al Resource::Cache i els fitxers .gif /
|
||||||
|
// .fnt de cada font. Afegir-ne una nova és una constant ací i ja.
|
||||||
|
namespace Font {
|
||||||
|
constexpr const char* DIR = "data/fonts/";
|
||||||
|
|
||||||
|
// HUD/Notifier/RenderInfo: font principal de UI del joc.
|
||||||
|
constexpr const char* HUD_NAME = "8bithud";
|
||||||
|
constexpr const char* HUD_GIF = "data/fonts/8bithud.gif";
|
||||||
|
constexpr const char* HUD_FNT = "data/fonts/8bithud.fnt";
|
||||||
|
|
||||||
|
// Placeholders (LOGO / TITLE / INTRO): font temporal fins que els
|
||||||
|
// placeholders es substitueixin per les seues escenes finals.
|
||||||
|
constexpr const char* PLACEHOLDER_NAME = "ari-w9500";
|
||||||
|
constexpr const char* PLACEHOLDER_GIF = "data/fonts/ari-w9500.gif";
|
||||||
|
constexpr const char* PLACEHOLDER_FNT = "data/fonts/ari-w9500.fnt";
|
||||||
|
} // namespace Font
|
||||||
|
|
||||||
// Overlay de RenderInfo (F12).
|
// Overlay de RenderInfo (F12).
|
||||||
constexpr Uint8 RENDER_INFO_TEXT = 47;
|
constexpr Uint8 RENDER_INFO_TEXT = 47;
|
||||||
constexpr Uint8 RENDER_INFO_SHADOW = 34;
|
constexpr Uint8 RENDER_INFO_SHADOW = 34;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.15.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.19.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
@@ -14,6 +14,12 @@
|
|||||||
// T10 torch anim — Torch llegeix 4 frames del YAML i el tick avança frame.
|
// T10 torch anim — Torch llegeix 4 frames del YAML i el tick avança frame.
|
||||||
// T11 sarcofag — tile_size [3,1] llegit; perímetre 8; falsa perspectiva:
|
// T11 sarcofag — tile_size [3,1] llegit; perímetre 8; falsa perspectiva:
|
||||||
// el sprite.height no canvia la caixa lògica 48×16.
|
// el sprite.height no canvia la caixa lògica 48×16.
|
||||||
|
// T12 collision — `Entity::CollisionBox` encastada al sprite: la caixa
|
||||||
|
// retornada coincideix amb les dimensions del bloc
|
||||||
|
// `collision:` del YAML; sense caixa, coincideix amb el
|
||||||
|
// sprite sencer (pixel_rect).
|
||||||
|
// T13 mummy step — camina en línia recta pel playfield i gira en topar
|
||||||
|
// un mur (insidePlayfield=false) o sarc (Map::collides).
|
||||||
//
|
//
|
||||||
// Execució (des de la ruta del repo):
|
// Execució (des de la ruta del repo):
|
||||||
// make build && cd build && ctest --output-on-failure
|
// make build && cd build && ctest --output-on-failure
|
||||||
@@ -342,6 +348,83 @@ namespace {
|
|||||||
REQUIRE(sarc->getPerimeterSize() == 8);
|
REQUIRE(sarc->getPerimeterSize() == 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testCollisionBox() {
|
||||||
|
std::cout << "[T12] collision box encastada al sprite\n";
|
||||||
|
// Torch sense bloc `collision:` → getCollisionRect == pixel_rect.
|
||||||
|
fkyaml::node torch_tmpl = EntityLoader::loadTemplate("torch");
|
||||||
|
Torch torch(torch_tmpl);
|
||||||
|
torch.setPixelRect({.x = 100.0F, .y = 200.0F, .w = 16.0F, .h = 16.0F});
|
||||||
|
const auto TRECT = torch.getCollisionRect();
|
||||||
|
REQUIRE(static_cast<int>(TRECT.w) == 16);
|
||||||
|
REQUIRE(static_cast<int>(TRECT.h) == 16);
|
||||||
|
REQUIRE(static_cast<int>(TRECT.x) == 100);
|
||||||
|
REQUIRE(static_cast<int>(TRECT.y) == 200);
|
||||||
|
|
||||||
|
// Mummy amb `collision: { size: [12, 12] }` i sprite 12×24 → caixa
|
||||||
|
// auto-centrada bottom dels peus: offset_x = (12-12)/2 = 0,
|
||||||
|
// offset_y = 24 - 12 = 12. Amb pixel_rect a (100, 200, 12, 24),
|
||||||
|
// la caixa ha de sortir a (100, 212, 12, 12) i feet_y = 224.
|
||||||
|
fkyaml::node mummy_tmpl = EntityLoader::loadTemplate("mummy");
|
||||||
|
Mummy mummy(mummy_tmpl);
|
||||||
|
// Apliquem el CollisionBox com fa el loader (readCollisionBox)
|
||||||
|
// amb sprite 12×24, offset calculat per defecte.
|
||||||
|
mummy.setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||||
|
mummy.setPixelRect({.x = 100.0F, .y = 200.0F, .w = 12.0F, .h = 24.0F});
|
||||||
|
const auto MRECT = mummy.getCollisionRect();
|
||||||
|
REQUIRE(static_cast<int>(MRECT.x) == 100);
|
||||||
|
REQUIRE(static_cast<int>(MRECT.y) == 212);
|
||||||
|
REQUIRE(static_cast<int>(MRECT.w) == 12);
|
||||||
|
REQUIRE(static_cast<int>(MRECT.h) == 12);
|
||||||
|
REQUIRE(static_cast<int>(mummy.getFeetY()) == 224);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testMummyStep() {
|
||||||
|
std::cout << "[T13] momia step amb col·lisió i gir\n";
|
||||||
|
// Cal Map::loadFrom (T9 ja el carrega, però el rerrumigo ací per
|
||||||
|
// aïllar la intenció del test).
|
||||||
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
|
// Momia al corredor exterior superior: playfield_y=48, alçada del
|
||||||
|
// corredor exterior = outer_corridor (16). Així el base box 12×12
|
||||||
|
// queda a y∈[52, 64] aprox. Collision box: offset_y=12 (sprite
|
||||||
|
// 12×24 amb peus 12×12 a baix). pixel_rect.x/y triat perquè els
|
||||||
|
// peus quedin al tile centrat (4, 3) del tilemap → peus a
|
||||||
|
// (col*16-8, row*16+0) i alineats bottom del tile.
|
||||||
|
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
|
||||||
|
Mummy mummy(tmpl);
|
||||||
|
mummy.setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||||
|
// Posem els peus ben dins d'un corredor horitzontal lliure. El
|
||||||
|
// corredor superior exterior va de y=48 a y=64 (alçada 16). Amb
|
||||||
|
// collision_y=12, sprite 12×24 → pixel_rect.y perquè feetY=64.
|
||||||
|
// feetY = pixel_rect.y + offset_y + h = pixel_rect.y + 24 → pixel_rect.y = 40.
|
||||||
|
// X: fora d'un sarc; (x=60) és al corredor extern abans dels sarcs.
|
||||||
|
mummy.setPixelRect({.x = 60.0F, .y = 40.0F, .w = 12.0F, .h = 24.0F});
|
||||||
|
const auto BEFORE = mummy.getPixelRect();
|
||||||
|
|
||||||
|
// Velocitat llegida del YAML és 40 px/s. A 0.1s hauria d'avançar
|
||||||
|
// 4 px en X (direcció inicial right=+1,0). Com que està en un
|
||||||
|
// corredor lliure, el step s'aplica.
|
||||||
|
mummy.step(0.1F);
|
||||||
|
const auto AFTER = mummy.getPixelRect();
|
||||||
|
REQUIRE(AFTER.x > BEFORE.x);
|
||||||
|
REQUIRE(AFTER.y == BEFORE.y);
|
||||||
|
|
||||||
|
// Posem la momia contra la paret dreta del playfield: avançar un
|
||||||
|
// frame la trauria fora → el step ha de girar. No assumim cap nova
|
||||||
|
// direcció concreta (perpendiculars lliures poden ser vàlides); el
|
||||||
|
// contracte és "ja no continua cap a la dreta".
|
||||||
|
const float RIGHT_WALL = static_cast<float>(Map::playfield_x + Map::playfield_w);
|
||||||
|
mummy.setPixelRect({.x = RIGHT_WALL - 12.0F, .y = 40.0F, .w = 12.0F, .h = 24.0F});
|
||||||
|
const auto POS_BEFORE = mummy.getPixelRect();
|
||||||
|
mummy.step(0.1F);
|
||||||
|
const auto POS_AFTER = mummy.getPixelRect();
|
||||||
|
// No ha avançat cap a la dreta en el frame de gir.
|
||||||
|
REQUIRE(POS_AFTER.x == POS_BEFORE.x);
|
||||||
|
REQUIRE(POS_AFTER.y == POS_BEFORE.y);
|
||||||
|
// I ja no apunta cap a la dreta (ha triat perpendicular o U-turn).
|
||||||
|
REQUIRE(mummy.getDirX() <= 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
void testTorchAnimation() {
|
void testTorchAnimation() {
|
||||||
std::cout << "[T10] torch animation wired from yaml\n";
|
std::cout << "[T10] torch animation wired from yaml\n";
|
||||||
// Regressió: el torch.yaml declarava `frames: [0]` i el render no
|
// Regressió: el torch.yaml declarava `frames: [0]` i el render no
|
||||||
@@ -379,6 +462,8 @@ auto main() -> int {
|
|||||||
testDecorationFlags();
|
testDecorationFlags();
|
||||||
testLoadFromLevel();
|
testLoadFromLevel();
|
||||||
testSarcophagusYaml();
|
testSarcophagusYaml();
|
||||||
|
testCollisionBox();
|
||||||
|
testMummyStep();
|
||||||
testTorchAnimation();
|
testTorchAnimation();
|
||||||
|
|
||||||
if (g_failures == 0) {
|
if (g_failures == 0) {
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Smoke tests dels helpers purs del flux d'escenes: parser de debug.yaml i
|
||||||
|
// classificació de tecles reservades. Stand-alone, sense frameworks externs.
|
||||||
|
//
|
||||||
|
// Tests coberts:
|
||||||
|
// T1 parseStartScene — "LOGO"/"TITLE"/"INTRO"/"GAME" donen l'enum correcte.
|
||||||
|
// T2 parseStartScene — valor desconegut o fitxer buit → GAME (fallback).
|
||||||
|
// T3 parseStartScene — YAML invàlid no llança, cau a GAME.
|
||||||
|
// T4 resolveStartScene — fitxer inexistent → GAME sense crash.
|
||||||
|
// T5 resolveStartScene — fitxer real amb valor vàlid és respectat.
|
||||||
|
// T6 resolveStartScene — fitxer real amb valor invàlid → GAME.
|
||||||
|
// T7 isReservedScancode — F1/F12/Escape reservats després de KeyConfig::load.
|
||||||
|
// T8 isReservedScancode — SPACE i fletxes no són reservades.
|
||||||
|
// T9 isReservedScancode — SDL_SCANCODE_UNKNOWN sempre fals.
|
||||||
|
//
|
||||||
|
// La lògica d'update/render dels placeholders (transició per timeout o
|
||||||
|
// keypress) no es testeja ací perquè `update()` crida GlobalInputs, que
|
||||||
|
// arrossega Screen/Notifier/RenderInfo i tot l'stack SDL. Aquesta integració
|
||||||
|
// la cobreix el smoke run del binari sota xvfb-run.
|
||||||
|
//
|
||||||
|
// Execució:
|
||||||
|
// make build && cd build && ctest --output-on-failure
|
||||||
|
// O bé:
|
||||||
|
// ./build/test_scene_flow
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/input/key_config.hpp"
|
||||||
|
#include "game/debug_config.hpp"
|
||||||
|
#include "game/scenes/placeholder_input.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int g_failures = 0;
|
||||||
|
|
||||||
|
void check(bool cond, const char* msg, const char* file, int line) {
|
||||||
|
if (!cond) {
|
||||||
|
std::cerr << " FAIL: " << msg << " (" << file << ":" << line << ")\n";
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CHECK(cond) check((cond), #cond, __FILE__, __LINE__)
|
||||||
|
|
||||||
|
void writeTemp(const std::string& path, const std::string& content) {
|
||||||
|
std::ofstream file(path);
|
||||||
|
file << content;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseStartSceneValid() {
|
||||||
|
std::cout << "T1 parseStartScene valid\n";
|
||||||
|
CHECK(DebugConfig::parseStartScene("start_scene: LOGO") == DebugConfig::StartScene::LOGO);
|
||||||
|
CHECK(DebugConfig::parseStartScene("start_scene: TITLE") == DebugConfig::StartScene::TITLE);
|
||||||
|
CHECK(DebugConfig::parseStartScene("start_scene: INTRO") == DebugConfig::StartScene::INTRO);
|
||||||
|
CHECK(DebugConfig::parseStartScene("start_scene: GAME") == DebugConfig::StartScene::GAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseStartSceneFallback() {
|
||||||
|
std::cout << "T2 parseStartScene fallback\n";
|
||||||
|
CHECK(DebugConfig::parseStartScene("") == DebugConfig::StartScene::GAME);
|
||||||
|
CHECK(DebugConfig::parseStartScene("start_scene: FOO") == DebugConfig::StartScene::GAME);
|
||||||
|
CHECK(DebugConfig::parseStartScene("other_field: 42") == DebugConfig::StartScene::GAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseStartSceneInvalidYaml() {
|
||||||
|
std::cout << "T3 parseStartScene YAML invàlid\n";
|
||||||
|
// Text amb caràcters que fan petar el parser: indentació incoherent +
|
||||||
|
// claus desbalancejades. Si el nostre try/catch no està bé, això
|
||||||
|
// propagaria una excepció i trencaria el test.
|
||||||
|
CHECK(DebugConfig::parseStartScene("{{{unclosed: [") == DebugConfig::StartScene::GAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testResolveStartSceneMissing() {
|
||||||
|
std::cout << "T4 resolveStartScene fitxer inexistent\n";
|
||||||
|
CHECK(DebugConfig::resolveStartScene("/tmp/aee_2026_nonexistent_XYZ.yaml") == DebugConfig::StartScene::GAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testResolveStartSceneValid() {
|
||||||
|
std::cout << "T5 resolveStartScene fitxer vàlid\n";
|
||||||
|
const auto PATH = std::filesystem::temp_directory_path() / "aee_2026_test_debug_valid.yaml";
|
||||||
|
writeTemp(PATH.string(), "start_scene: TITLE\n");
|
||||||
|
CHECK(DebugConfig::resolveStartScene(PATH.string()) == DebugConfig::StartScene::TITLE);
|
||||||
|
std::filesystem::remove(PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testResolveStartSceneInvalid() {
|
||||||
|
std::cout << "T6 resolveStartScene valor invàlid\n";
|
||||||
|
const auto PATH = std::filesystem::temp_directory_path() / "aee_2026_test_debug_invalid.yaml";
|
||||||
|
writeTemp(PATH.string(), "start_scene: WUT\n");
|
||||||
|
CHECK(DebugConfig::resolveStartScene(PATH.string()) == DebugConfig::StartScene::GAME);
|
||||||
|
std::filesystem::remove(PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testCreateDefaultIfMissing() {
|
||||||
|
std::cout << "T7 createDefaultIfMissing\n";
|
||||||
|
const auto PATH = std::filesystem::temp_directory_path() / "aee_2026_test_debug_create.yaml";
|
||||||
|
std::filesystem::remove(PATH);
|
||||||
|
CHECK(!std::filesystem::exists(PATH));
|
||||||
|
DebugConfig::createDefaultIfMissing(PATH.string());
|
||||||
|
CHECK(std::filesystem::exists(PATH));
|
||||||
|
CHECK(DebugConfig::resolveStartScene(PATH.string()) == DebugConfig::StartScene::GAME);
|
||||||
|
|
||||||
|
// Segona invocació: ha de ser idempotent (no reescriu si ja existeix).
|
||||||
|
writeTemp(PATH.string(), "start_scene: LOGO\n");
|
||||||
|
DebugConfig::createDefaultIfMissing(PATH.string());
|
||||||
|
CHECK(DebugConfig::resolveStartScene(PATH.string()) == DebugConfig::StartScene::LOGO);
|
||||||
|
std::filesystem::remove(PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testIsReservedScancodeAfterLoad() {
|
||||||
|
std::cout << "T8 isReservedScancode després de KeyConfig::load\n";
|
||||||
|
KeyConfig::get().load("data/input/keys.yaml");
|
||||||
|
CHECK(Placeholder::isReservedScancode(SDL_SCANCODE_F1));
|
||||||
|
CHECK(Placeholder::isReservedScancode(SDL_SCANCODE_F12));
|
||||||
|
CHECK(Placeholder::isReservedScancode(SDL_SCANCODE_ESCAPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testIsReservedScancodeNonReserved() {
|
||||||
|
std::cout << "T9 isReservedScancode per tecles no reservades\n";
|
||||||
|
CHECK(!Placeholder::isReservedScancode(SDL_SCANCODE_SPACE));
|
||||||
|
CHECK(!Placeholder::isReservedScancode(SDL_SCANCODE_RETURN));
|
||||||
|
CHECK(!Placeholder::isReservedScancode(SDL_SCANCODE_UP));
|
||||||
|
CHECK(!Placeholder::isReservedScancode(SDL_SCANCODE_DOWN));
|
||||||
|
CHECK(!Placeholder::isReservedScancode(SDL_SCANCODE_LEFT));
|
||||||
|
CHECK(!Placeholder::isReservedScancode(SDL_SCANCODE_RIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testIsReservedScancodeUnknown() {
|
||||||
|
std::cout << "T10 isReservedScancode amb UNKNOWN\n";
|
||||||
|
CHECK(!Placeholder::isReservedScancode(SDL_SCANCODE_UNKNOWN));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto main() -> int {
|
||||||
|
testParseStartSceneValid();
|
||||||
|
testParseStartSceneFallback();
|
||||||
|
testParseStartSceneInvalidYaml();
|
||||||
|
testResolveStartSceneMissing();
|
||||||
|
testResolveStartSceneValid();
|
||||||
|
testResolveStartSceneInvalid();
|
||||||
|
testCreateDefaultIfMissing();
|
||||||
|
testIsReservedScancodeAfterLoad();
|
||||||
|
testIsReservedScancodeNonReserved();
|
||||||
|
testIsReservedScancodeUnknown();
|
||||||
|
|
||||||
|
if (g_failures == 0) {
|
||||||
|
std::cout << "OK: tots els tests de scene flow passen\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::cerr << g_failures << " tests han fallat\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||