Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb07e7e564 | ||
|
|
03f1c5f926 | ||
|
|
6800786194 | ||
|
|
9c9b6028e7 | ||
|
|
2cb21211ef | ||
|
|
87a9f098b5 | ||
|
|
d31483124e | ||
|
|
2883df57da | ||
|
|
510ab55a33 | ||
|
|
a7e681a347 | ||
|
|
b19a6e25da | ||
|
|
d9f6fb45f8 | ||
|
|
64e0178ee9 | ||
|
|
a3b03b87be | ||
|
|
32dcf264c4 |
No files matched your search
@@ -76,7 +76,12 @@ set(APP_SOURCES
|
||||
source/core/resources/palette_loader.cpp
|
||||
source/utils/easing.cpp
|
||||
source/utils/utils.cpp
|
||||
source/game/debug_config.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/entities/player.cpp
|
||||
source/game/map/map.cpp
|
||||
@@ -150,14 +155,19 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
||||
|
||||
# Copia recursius de la carpeta data/ al costat del binari per que les rutes
|
||||
# relatives ("data/player/player.gif") funcionen sense desplegament.
|
||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
# Còpia recursiva de data/ al costat del binari perquè les rutes relatives
|
||||
# ("data/player/player.gif") funcionin sense desplegament. Es defineix com
|
||||
# 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
|
||||
"${CMAKE_SOURCE_DIR}/data"
|
||||
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/data"
|
||||
"${CMAKE_BINARY_DIR}/data"
|
||||
COMMENT "Copying data/ next to the binary"
|
||||
)
|
||||
add_dependencies(${PROJECT_NAME} sync_data)
|
||||
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||
@@ -255,6 +265,7 @@ option(BUILD_TESTS "Build smoke tests" ON)
|
||||
if(BUILD_TESTS)
|
||||
add_executable(test_entities_smoke
|
||||
tests/test_entities_smoke.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/game/map/map.cpp
|
||||
${ENTITY_SOURCES}
|
||||
)
|
||||
@@ -268,4 +279,25 @@ if(BUILD_TESTS)
|
||||
add_test(NAME entities_smoke
|
||||
COMMAND test_entities_smoke
|
||||
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()
|
||||
@@ -14,47 +14,66 @@ endif
|
||||
|
||||
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:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||
|
||||
build: configure
|
||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||
|
||||
run: build
|
||||
@./$(BUILDDIR)/$(PROJECT)
|
||||
|
||||
release:
|
||||
all:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@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:
|
||||
@echo "$(PROJECT) $(VERSION) ($(GIT_HASH))"
|
||||
|
||||
clean:
|
||||
@rm -rf $(BUILDDIR)
|
||||
|
||||
rebuild: clean build
|
||||
rebuild: clean all
|
||||
|
||||
# ==============================================================================
|
||||
# CODE QUALITY (delegados a cmake)
|
||||
# ==============================================================================
|
||||
format: configure
|
||||
format:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@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
|
||||
|
||||
tidy: configure
|
||||
tidy:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@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
|
||||
|
||||
cppcheck: configure
|
||||
cppcheck:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target cppcheck
|
||||
|
||||
# ==============================================================================
|
||||
@@ -63,3 +82,35 @@ cppcheck: configure
|
||||
hooks-install:
|
||||
@git config core.hooksPath .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,23 +4,32 @@ sprite:
|
||||
path: mummy.gif
|
||||
size: [12, 24]
|
||||
tile_size: [1, 1]
|
||||
# La momia usa falsa perspectiva: el sprite fa 24 px d'alt però la caixa
|
||||
# AABB només ocupa els 12×12 de baix (els "peus"). L'spawn `position` del
|
||||
# map és el tile dels peus: el sprite es centra horitzontalment i la
|
||||
# seua vora inferior s'alinea amb la vora inferior del tile.
|
||||
# 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: [12, 12]
|
||||
size: [16, 16]
|
||||
base_size: [12, 12]
|
||||
flags:
|
||||
collides: true
|
||||
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:
|
||||
idle: { frames: [0], fps: 1, loop: true }
|
||||
walk: { frames: [0], fps: 8, loop: true }
|
||||
walk_down: { frames: [0, 1, 2, 3], 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:
|
||||
type: corridor_wanderer
|
||||
direction_choice: random
|
||||
initial_direction: right
|
||||
stats:
|
||||
speed: 40
|
||||
speed: 30
|
||||
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]
|
||||
- type: mummy
|
||||
position: [10, 7]
|
||||
- type: mummy
|
||||
position: [7, 9]
|
||||
@@ -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`.
|
||||
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:
|
||||
- `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-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 |
|
||||
|
||||
## 5. Inconsistències internes
|
||||
|
||||
| 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-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 |
|
||||
@@ -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 |
|
||||
|----|--------------|----------|-----------|-------|-------|
|
||||
| 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-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.
|
||||
@@ -248,9 +248,37 @@ Tanca els hooks d'IA i col·lisió que quedaven apuntalats a les fases 1–3.
|
||||
- **Smoke tests**: T12 (CollisionBox), T13 (Mummy::step), T11 (sarc YAML),
|
||||
T10 (torch anim). Total 14/14.
|
||||
|
||||
## 7. Estat global
|
||||
## 7. Fase 5 — Convenció de caixa "peus" com el jugador + animacions direccionals (DONE)
|
||||
|
||||
- **Andamiatge, integració, render animat, falsa perspectiva, moviment**: DONE. Els 26 actors del `pyramid.yaml` (4 torches, 20 sarcs, 1 door, 1 mummy) es carreguen, animen, es col·loquen amb l'anchor correcte, col·lisionen segons caixa lògica i el z-order interleava per peus.
|
||||
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.
|
||||
|
||||
|
||||
@@ -52,12 +52,17 @@ impedeix avançar, l'animació `walk*` s'atura i es força la variant
|
||||
|
||||
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. `AnimatedSprite::update` integra la velocitat; la col·lisió
|
||||
eix-separada reverteix cada eix si `Map::collides(baseBox())`.
|
||||
4. Després de `clampToPlayfield`, es compara la posició final amb el
|
||||
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_`.
|
||||
5. Si hi ha hagut moviment real (incloent-hi snap), s'aplica la variant
|
||||
`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
|
||||
@@ -70,7 +75,9 @@ mirant amunt.
|
||||
|
||||
- DONE: sis animacions al YAML, `facing_` persistent, flip condicionat,
|
||||
`stand` automàtic a la col·lisió.
|
||||
- Build i smoke tests (14/14) verdes.
|
||||
- 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
|
||||
|
||||
@@ -42,83 +42,86 @@ namespace GlobalInputs {
|
||||
}
|
||||
|
||||
void onDecZoom() {
|
||||
if (Options::window.fullscreen) { return; }
|
||||
if (Options::window.zoom <= MIN_ZOOM) { return; }
|
||||
Options::window.zoom--;
|
||||
Screen::get().setWindowZoom(Options::window.zoom);
|
||||
Options::saveToFile();
|
||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom));
|
||||
if (Options::window().fullscreen) { return; }
|
||||
if (Options::window().zoom <= MIN_ZOOM) { return; }
|
||||
const int NEW_ZOOM = Options::window().zoom - 1;
|
||||
Options::setWindowZoom(NEW_ZOOM);
|
||||
Screen::get().setWindowZoom(NEW_ZOOM);
|
||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(NEW_ZOOM));
|
||||
}
|
||||
|
||||
void onIncZoom() {
|
||||
if (Options::window.fullscreen) { return; }
|
||||
if (Options::window.zoom >= MAX_ZOOM) { return; }
|
||||
Options::window.zoom++;
|
||||
Screen::get().setWindowZoom(Options::window.zoom);
|
||||
Options::saveToFile();
|
||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom));
|
||||
if (Options::window().fullscreen) { return; }
|
||||
if (Options::window().zoom >= MAX_ZOOM) { return; }
|
||||
const int NEW_ZOOM = Options::window().zoom + 1;
|
||||
Options::setWindowZoom(NEW_ZOOM);
|
||||
Screen::get().setWindowZoom(NEW_ZOOM);
|
||||
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(NEW_ZOOM));
|
||||
}
|
||||
|
||||
void onFullscreen() {
|
||||
Options::window.fullscreen = !Options::window.fullscreen;
|
||||
Screen::get().setFullscreen(Options::window.fullscreen);
|
||||
Options::saveToFile();
|
||||
notifyLocale(Options::window.fullscreen ? "input.fullscreen_on" : "input.fullscreen_off");
|
||||
const bool NEW_FS = !Options::window().fullscreen;
|
||||
Options::setWindowFullscreen(NEW_FS);
|
||||
Screen::get().setFullscreen(NEW_FS);
|
||||
notifyLocale(NEW_FS ? "input.fullscreen_on" : "input.fullscreen_off");
|
||||
}
|
||||
|
||||
void onToggleVSync() {
|
||||
Options::video.vsync = !Options::video.vsync;
|
||||
Screen::get().setVSync(Options::video.vsync);
|
||||
Options::saveToFile();
|
||||
notifyLocale(Options::video.vsync ? "input.vsync_on" : "input.vsync_off");
|
||||
const bool NEW_VSYNC = !Options::video().vsync;
|
||||
Options::setVideoVSync(NEW_VSYNC);
|
||||
Screen::get().setVSync(NEW_VSYNC);
|
||||
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
|
||||
}
|
||||
|
||||
void onToggleShader() {
|
||||
Options::video.shader.enabled = !Options::video.shader.enabled;
|
||||
const bool NEW_ENABLED = !Options::video().shader.enabled;
|
||||
Options::setShaderEnabled(NEW_ENABLED);
|
||||
reapplyShader();
|
||||
Options::saveToFile();
|
||||
notifyLocale(Options::video.shader.enabled ? "input.shader_on" : "input.shader_off");
|
||||
notifyLocale(NEW_ENABLED ? "input.shader_on" : "input.shader_off");
|
||||
}
|
||||
|
||||
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();
|
||||
Options::saveToFile();
|
||||
Notification::notify(Locale::get().text("input.shader") + ": " + Options::video.shader.current_shader);
|
||||
Notification::notify(Locale::get().text("input.shader") + ": " + NEW_NAME);
|
||||
}
|
||||
|
||||
// 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.
|
||||
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 (Options::crtpi_presets.empty()) { return; }
|
||||
auto it = std::ranges::find_if(Options::crtpi_presets,
|
||||
[](const auto& preset) { return preset.name == Options::video.shader.current_crtpi_preset; });
|
||||
std::size_t idx = (it == Options::crtpi_presets.end()) ? 0 : std::distance(Options::crtpi_presets.begin(), it);
|
||||
idx = (idx + 1) % Options::crtpi_presets.size();
|
||||
Options::video.shader.current_crtpi_preset = Options::crtpi_presets[idx].name;
|
||||
const auto& presets = Options::crtpiPresets();
|
||||
if (presets.empty()) { return; }
|
||||
auto it = std::ranges::find_if(presets,
|
||||
[](const auto& preset) { return preset.name == Options::video().shader.current_crtpi_preset; });
|
||||
std::size_t idx = (it == presets.end()) ? 0 : std::distance(presets.begin(), it);
|
||||
idx = (idx + 1) % presets.size();
|
||||
new_preset = presets[idx].name;
|
||||
Options::setCrtPiPreset(new_preset);
|
||||
} else {
|
||||
if (Options::postfx_presets.empty()) { return; }
|
||||
auto it = std::ranges::find_if(Options::postfx_presets,
|
||||
[](const auto& preset) { return preset.name == Options::video.shader.current_postfx_preset; });
|
||||
std::size_t idx = (it == Options::postfx_presets.end()) ? 0 : std::distance(Options::postfx_presets.begin(), it);
|
||||
idx = (idx + 1) % Options::postfx_presets.size();
|
||||
Options::video.shader.current_postfx_preset = Options::postfx_presets[idx].name;
|
||||
const auto& presets = Options::postfxPresets();
|
||||
if (presets.empty()) { return; }
|
||||
auto it = std::ranges::find_if(presets,
|
||||
[](const auto& preset) { return preset.name == Options::video().shader.current_postfx_preset; });
|
||||
std::size_t idx = (it == presets.end()) ? 0 : std::distance(presets.begin(), it);
|
||||
idx = (idx + 1) % presets.size();
|
||||
new_preset = presets[idx].name;
|
||||
Options::setPostFXPreset(new_preset);
|
||||
}
|
||||
reapplyShader();
|
||||
Options::saveToFile();
|
||||
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);
|
||||
Notification::notify(Locale::get().text("input.preset") + ": " + new_preset);
|
||||
}
|
||||
|
||||
void onNextScalingMode() {
|
||||
using Mode = Defaults::ScalingMode;
|
||||
Options::video.scaling_mode = static_cast<Mode>((static_cast<int>(Options::video.scaling_mode) + 1) % 5);
|
||||
Screen::get().setScalingMode(Options::video.scaling_mode);
|
||||
Options::saveToFile();
|
||||
const auto NEW_MODE = static_cast<Mode>((static_cast<int>(Options::video().scaling_mode) + 1) % 5);
|
||||
Options::setVideoScalingMode(NEW_MODE);
|
||||
Screen::get().setScalingMode(NEW_MODE);
|
||||
const char* mode_key = nullptr;
|
||||
switch (Options::video.scaling_mode) {
|
||||
switch (NEW_MODE) {
|
||||
case Mode::DISABLED:
|
||||
mode_key = "input.scaling_disabled";
|
||||
break;
|
||||
@@ -139,14 +142,12 @@ namespace GlobalInputs {
|
||||
}
|
||||
|
||||
void onCycleRenderInfo() {
|
||||
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::saveToFile();
|
||||
const int NEXT = (static_cast<int>(Options::video().render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT;
|
||||
Options::setVideoRenderInfoPosition(static_cast<Defaults::RenderInfoPosition>(NEXT));
|
||||
}
|
||||
|
||||
void onToggleRenderTime() {
|
||||
Options::video.render_info_show_time = !Options::video.render_info_show_time;
|
||||
Options::saveToFile();
|
||||
Options::setVideoRenderInfoShowTime(!Options::video().render_info_show_time);
|
||||
}
|
||||
|
||||
void onExit() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
auto KeyConfig::get() -> KeyConfig& {
|
||||
@@ -12,16 +13,17 @@ auto KeyConfig::get() -> KeyConfig& {
|
||||
}
|
||||
|
||||
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()) {
|
||||
std::cerr << "KeyConfig: no s'ha pogut obrir " << file_path << '\n';
|
||||
std::cerr << "KeyConfig: no s'ha pogut obrir " << FULL << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(file);
|
||||
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;
|
||||
}
|
||||
bindings_.clear();
|
||||
@@ -36,9 +38,9 @@ void KeyConfig::load(const std::string& file_path) {
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
|
||||
[[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:
|
||||
KeyConfig() = default;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -33,9 +34,10 @@ auto Locale::get() -> Locale& {
|
||||
}
|
||||
|
||||
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()) {
|
||||
std::cerr << "Locale: no s'ha pogut obrir " << file_path << '\n';
|
||||
std::cerr << "Locale: no s'ha pogut obrir " << FULL << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,9 +45,9 @@ void Locale::load(const std::string& file_path) {
|
||||
auto yaml = fkyaml::node::deserialize(file);
|
||||
strings_.clear();
|
||||
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) {
|
||||
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));
|
||||
|
||||
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
|
||||
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
|
||||
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
|
||||
const int IMG_W = image_descriptor.image_width;
|
||||
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());
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,3 +72,12 @@ void MovingSprite::setPosY(float value) {
|
||||
y_ = value;
|
||||
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 <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp"
|
||||
@@ -40,6 +41,15 @@ class MovingSprite : public Sprite {
|
||||
auto getFlip() -> SDL_FlipMode { return flip_; }
|
||||
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:
|
||||
void move(float delta_time);
|
||||
|
||||
|
||||
@@ -19,6 +19,15 @@ class Sprite {
|
||||
// Hook d'actualització per frame. Base buida (no hi ha Sprite pur a
|
||||
// actualitzar); les subclasses mòbils/animades fan override.
|
||||
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();
|
||||
// Renderitza substituint l'índex source_color per target_color (ús típic:
|
||||
// la classe Text, que recolora glifs monocroms per paràmetre).
|
||||
|
||||
@@ -2,20 +2,41 @@
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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> {
|
||||
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) {
|
||||
std::cerr << "Resource::Helper: cannot open " << filepath << '\n';
|
||||
std::cerr << "Resource::Helper: cannot open " << FULL << '\n';
|
||||
return {};
|
||||
}
|
||||
std::streamsize size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<std::uint8_t> data(static_cast<std::size_t>(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 data;
|
||||
|
||||
@@ -6,7 +6,23 @@
|
||||
|
||||
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).
|
||||
// Les rutes relatives es resolen via `resolve()`; les absolutes s'obrin
|
||||
// directament.
|
||||
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t>;
|
||||
|
||||
} // namespace Resource::Helper
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
@@ -19,10 +20,15 @@
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "core/scene/scene.hpp"
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "game/debug_config.hpp"
|
||||
#include "game/options.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 "utils/defaults.hpp"
|
||||
|
||||
@@ -31,6 +37,15 @@ Director::Director() {
|
||||
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
|
||||
// les opcions; si no existeix config.yaml el crea amb els defaults.
|
||||
createSystemFolder();
|
||||
@@ -61,16 +76,21 @@ Director::Director() {
|
||||
|
||||
// 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.
|
||||
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.
|
||||
KeyConfig::get().load("data/input/keys.yaml");
|
||||
|
||||
// Recursos compartits d'UI (paleta + font). Es carreguen abans que els
|
||||
// overlays perquè tots els reaprofiten sense duplicar la lògica.
|
||||
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
|
||||
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
|
||||
// noms de font viuen a Defaults::UI::Font (un únic lloc per canviar-los).
|
||||
Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal");
|
||||
Resource::Cache::get().loadSurface("8bithud.gif", "data/font/8bithud.gif");
|
||||
Resource::Cache::get().getSurface("8bithud.gif")->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||
const std::string HUD_FONT_GIF_KEY = std::string(Defaults::UI::Font::HUD_NAME) + ".gif";
|
||||
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í
|
||||
// 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
|
||||
// i la injectem als consumidors (Notifier, RenderInfo) per a que cap
|
||||
// d'ells hagi de conèixer els noms dels recursos concrets.
|
||||
auto hud_font_surface = Resource::Cache::get().getSurface("8bithud.gif");
|
||||
auto hud_text = std::make_shared<Text>(hud_font_surface, "data/font/8bithud.fnt");
|
||||
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||||
auto hud_text = std::make_shared<Text>(hud_font_surface, Defaults::UI::Font::HUD_FNT);
|
||||
const auto& ui_palette = Resource::Cache::get().getPalette("ui");
|
||||
|
||||
RenderInfo::get().init(Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW), hud_text);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -143,6 +207,8 @@ void Director::processEvents() {
|
||||
void Director::switchSceneIfRequested() {
|
||||
if (!current_scene_) { return; }
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
#include "game/entities/concrete/mummy.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <random>
|
||||
#include <string>
|
||||
@@ -20,9 +21,36 @@ namespace {
|
||||
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
|
||||
|
||||
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");
|
||||
collides_ = true;
|
||||
interactable_ = false;
|
||||
@@ -43,16 +71,36 @@ Mummy::Mummy(const fkyaml::node& yaml) {
|
||||
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
|
||||
}
|
||||
if (initial == "right") {
|
||||
setDirection(1.0F, 0.0F);
|
||||
changeDirection(1.0F, 0.0F);
|
||||
} else if (initial == "left") {
|
||||
setDirection(-1.0F, 0.0F);
|
||||
changeDirection(-1.0F, 0.0F);
|
||||
} else if (initial == "up") {
|
||||
setDirection(0.0F, -1.0F);
|
||||
changeDirection(0.0F, -1.0F);
|
||||
} else if (initial == "down") {
|
||||
setDirection(0.0F, 1.0F);
|
||||
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 {
|
||||
if (allowed.empty()) { return Direction::NONE; }
|
||||
std::uniform_int_distribution<std::size_t> dist(0, allowed.size() - 1);
|
||||
@@ -66,6 +114,31 @@ auto Mummy::candidateBox(float dx, float dy) const -> SDL_FRect {
|
||||
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;
|
||||
@@ -84,60 +157,63 @@ void Mummy::step(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloqueig: cal girar. Recollim les direccions perpendiculars a l'actual
|
||||
// que tinguin un pròxim tram lliure. Evitem U-turn llevat que no hi hagi
|
||||
// cap alternativa (comportament típic d'Oh Mummy!: no es recula sense
|
||||
// motiu). Si la direcció perpendicular també està bloquejada, no s'hi
|
||||
// inclou; si totes les perpendiculars estan bloquejades, afegim l'opcio
|
||||
// oposada com a últim recurs (cul-de-sac).
|
||||
const float STEP = speed_ * delta_time;
|
||||
// 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},
|
||||
{.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},
|
||||
}};
|
||||
|
||||
std::vector<Direction> perp_allowed;
|
||||
std::vector<Direction> uturn_allowed;
|
||||
DirectionPools pools;
|
||||
for (const auto& c : ALL) {
|
||||
const bool IS_CURRENT = (c.dx > 0.0F && dir_x_ > 0.0F) || (c.dx < 0.0F && dir_x_ < 0.0F) || (c.dy > 0.0F && dir_y_ > 0.0F) || (c.dy < 0.0F && dir_y_ < 0.0F);
|
||||
const bool IS_UTURN = (c.dx > 0.0F && dir_x_ < 0.0F) || (c.dx < 0.0F && dir_x_ > 0.0F) || (c.dy > 0.0F && dir_y_ < 0.0F) || (c.dy < 0.0F && dir_y_ > 0.0F);
|
||||
if (IS_CURRENT) { continue; } // ja hem vist que està bloquejada
|
||||
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) {
|
||||
uturn_allowed.push_back(c.dir);
|
||||
pools.uturn.push_back(c.dir);
|
||||
} else {
|
||||
perp_allowed.push_back(c.dir);
|
||||
pools.perp.push_back(c.dir);
|
||||
}
|
||||
}
|
||||
return pools;
|
||||
}
|
||||
|
||||
const auto& pool = !perp_allowed.empty() ? perp_allowed : uturn_allowed;
|
||||
const auto CHOICE = chooseDirection(pool);
|
||||
switch (CHOICE) {
|
||||
void Mummy::applyChoice(Direction dir) {
|
||||
switch (dir) {
|
||||
case Direction::UP:
|
||||
setDirection(0.0F, -1.0F);
|
||||
changeDirection(0.0F, -1.0F);
|
||||
break;
|
||||
case Direction::DOWN:
|
||||
setDirection(0.0F, 1.0F);
|
||||
changeDirection(0.0F, 1.0F);
|
||||
break;
|
||||
case Direction::LEFT:
|
||||
setDirection(-1.0F, 0.0F);
|
||||
changeDirection(-1.0F, 0.0F);
|
||||
break;
|
||||
case Direction::RIGHT:
|
||||
setDirection(1.0F, 0.0F);
|
||||
changeDirection(1.0F, 0.0F);
|
||||
break;
|
||||
case Direction::NONE:
|
||||
setDirection(0.0F, 0.0F);
|
||||
changeDirection(0.0F, 0.0F);
|
||||
break;
|
||||
}
|
||||
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
|
||||
}
|
||||
|
||||
void Mummy::onPlayerContact(Player* /*player*/) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/animation_component.hpp"
|
||||
#include "game/entities/hazard.hpp"
|
||||
|
||||
// Mòmia: perill dinàmic que deambula pels passadís. A cada intersecció
|
||||
@@ -44,4 +45,36 @@ class Mummy : public DynamicHazard {
|
||||
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_;
|
||||
};
|
||||
@@ -128,6 +128,18 @@ class Entity {
|
||||
// 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:
|
||||
std::string name_;
|
||||
TilePos tile_{};
|
||||
@@ -137,5 +149,6 @@ class Entity {
|
||||
std::shared_ptr<Surface> surface_{nullptr};
|
||||
bool collides_{false};
|
||||
bool interactable_{false};
|
||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||
EventBus* bus_{nullptr};
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "game/entities/entity.hpp"
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
|
||||
@@ -30,10 +31,10 @@ namespace EntityLoader {
|
||||
auto it = cache.find(name);
|
||||
if (it != cache.end()) { return it->second; }
|
||||
|
||||
const std::string PATH = templatePath(name);
|
||||
std::ifstream file(PATH);
|
||||
const std::string FULL = Resource::Helper::resolve(templatePath(name));
|
||||
std::ifstream file(FULL);
|
||||
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);
|
||||
cache.emplace(name, node);
|
||||
@@ -41,9 +42,10 @@ namespace EntityLoader {
|
||||
}
|
||||
|
||||
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()) {
|
||||
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);
|
||||
std::vector<fkyaml::node> spawns;
|
||||
|
||||
@@ -51,7 +51,7 @@ void EntityManager::renderEntity(const Entity& e) {
|
||||
.w = RECT.w,
|
||||
.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() {
|
||||
@@ -105,9 +105,16 @@ namespace {
|
||||
return info;
|
||||
}
|
||||
|
||||
// Bloc `collision: { size: [w, h], offset: [x, y] }` de la plantilla
|
||||
// d'entitat. `offset` és opcional: si no apareix, s'entén "la caixa
|
||||
// queda bottom-center del sprite" (convenció de falsa perspectiva).
|
||||
// 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> {
|
||||
@@ -120,6 +127,16 @@ namespace {
|
||||
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) {
|
||||
@@ -258,16 +275,26 @@ namespace {
|
||||
const auto& p = spawn["position"];
|
||||
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
|
||||
|
||||
// Anchor "feet": el tile [col,row] és el tile dels peus. El sprite
|
||||
// es centra horitzontalment sobre el tile i la seua vora inferior
|
||||
// s'alinea amb la vora inferior del tile. Els píxels que sobresurten
|
||||
// (alçada del sprite per damunt del tile dels peus) queden "per
|
||||
// sobre" sense afectar cap col·lisió — falsa perspectiva.
|
||||
// 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") {
|
||||
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;
|
||||
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});
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
#include <utility>
|
||||
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -22,9 +22,10 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
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()) {
|
||||
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);
|
||||
|
||||
@@ -32,7 +33,7 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
sprite_h = yaml["frameHeight"].get_value<int>();
|
||||
|
||||
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"];
|
||||
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;
|
||||
}
|
||||
|
||||
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)),
|
||||
controls_(controls) {
|
||||
controls_(controls),
|
||||
map_(map_info) {
|
||||
setPos(x, y);
|
||||
setCurrentAnimation("stand");
|
||||
}
|
||||
@@ -104,22 +106,13 @@ void Player::update(float delta_time) {
|
||||
snapToVerticalLane(delta_time);
|
||||
}
|
||||
|
||||
// Guardem la posició prèvia i deixem que AnimatedSprite::update aplique
|
||||
// la velocitat com sempre; així no dupliquem la integració temporal ni
|
||||
// l'animació. Després, axis-separated collision: revertim i re-apliquem
|
||||
// cada eix comprovant el mapa de colisions, de manera que el jugador
|
||||
// llisca contra els sarcòfags en compte de quedar-se bloquejat en sec.
|
||||
const float OLD_X = getPosX();
|
||||
const float OLD_Y = getPosY();
|
||||
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); }
|
||||
// Animació del frame + moviment col·lisió-aware. `tryMove` aplica els
|
||||
// deltes per eix i reverteix l'eix que col·lisiona, així el jugador
|
||||
// llisca contra sarcòfags i murs en compte de quedar-se bloquejat en
|
||||
// sec. La caixa que s'envia al callback és `collisionBox()` (override
|
||||
// a Player retornant `getBaseBox()`), no el sprite sencer.
|
||||
animate(delta_time);
|
||||
tryMove(vx * delta_time, vy * delta_time, [this](const SDL_FRect& box) { return map_.collides(box); });
|
||||
|
||||
clampToPlayfield();
|
||||
|
||||
@@ -157,28 +150,28 @@ auto Player::animationName(bool moving) const -> const char* {
|
||||
}
|
||||
|
||||
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
|
||||
// centre de la caixa de col·lisió ens dóna el tile on "està" el Player.
|
||||
const auto BOX = baseBox();
|
||||
const float CX = BOX.x + (BOX.w * 0.5F);
|
||||
const float CY = BOX.y + (BOX.h * 0.5F);
|
||||
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(
|
||||
(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};
|
||||
}
|
||||
|
||||
void Player::clampToPlayfield() {
|
||||
// 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.
|
||||
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>(
|
||||
Map::playfield_x + Map::playfield_w - collision_x_offset - collision_size);
|
||||
const auto MIN_Y = static_cast<float>(Map::playfield_y - collision_y_offset);
|
||||
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 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));
|
||||
setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y));
|
||||
}
|
||||
@@ -214,24 +207,24 @@ void Player::snapToLane(
|
||||
const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time);
|
||||
const float SAVED = (this->*get_pos)();
|
||||
(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) {
|
||||
snapToLane(delta_time,
|
||||
static_cast<float>(Map::playfield_y - collision_y_offset),
|
||||
static_cast<float>(Map::pitch_y),
|
||||
Map::grid_rows,
|
||||
static_cast<float>(map_.playfield_y - collision_y_offset),
|
||||
static_cast<float>(map_.pitch_y),
|
||||
map_.grid_rows,
|
||||
&Player::getPosY,
|
||||
&Player::setPosY);
|
||||
}
|
||||
|
||||
void Player::snapToVerticalLane(float delta_time) {
|
||||
snapToLane(delta_time,
|
||||
static_cast<float>(Map::playfield_x - collision_x_offset),
|
||||
static_cast<float>(Map::pitch_x),
|
||||
Map::grid_cols,
|
||||
static_cast<float>(map_.playfield_x - collision_x_offset),
|
||||
static_cast<float>(map_.pitch_x),
|
||||
map_.grid_cols,
|
||||
&Player::getPosX,
|
||||
&Player::setPosX);
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
|
||||
class EventBus;
|
||||
|
||||
@@ -40,7 +41,7 @@ class Player : public AnimatedSprite {
|
||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||
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;
|
||||
|
||||
@@ -66,6 +67,12 @@ class Player : public AnimatedSprite {
|
||||
// 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:
|
||||
// Alias privat històric. Mantingut perquè viu dins de l'implementació
|
||||
// (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`.
|
||||
@@ -95,6 +102,12 @@ class Player : public AnimatedSprite {
|
||||
void (Player::*set_pos)(float));
|
||||
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -21,13 +22,14 @@ namespace {
|
||||
namespace Map {
|
||||
|
||||
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()) {
|
||||
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);
|
||||
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"];
|
||||
|
||||
@@ -76,7 +78,7 @@ namespace Map {
|
||||
std::cout << "Map: geometria " << grid_cols << "x" << grid_rows
|
||||
<< " sarcs " << sarc_w << "x" << sarc_h
|
||||
<< " pitch " << pitch_x << "x" << pitch_y
|
||||
<< " des de " << yaml_path << '\n';
|
||||
<< " des de " << FULL << '\n';
|
||||
}
|
||||
|
||||
auto getSarcRect(int col, int row) -> SDL_FRect {
|
||||
@@ -104,4 +106,22 @@ namespace Map {
|
||||
return box.x >= static_cast<float>(playfield_x) && box.y >= static_cast<float>(playfield_y) && box.x + box.w <= static_cast<float>(playfield_x + playfield_w) && box.y + box.h <= static_cast<float>(playfield_y + playfield_h);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -14,6 +14,14 @@
|
||||
// els membres viuen com a globals del namespace i els callers hi accedeixen
|
||||
// amb `Map::grid_cols`, `Map::collides(...)`, etc. El canvi respecte a
|
||||
// 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 {
|
||||
|
||||
// Geometria de la graella (carregada de pyramid.yaml > geometry).
|
||||
@@ -73,4 +81,27 @@ namespace Map {
|
||||
// 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
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -67,9 +68,10 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
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()) {
|
||||
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);
|
||||
@@ -111,7 +113,7 @@ Tilemap::Tilemap(const std::string& yaml_path) {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -14,6 +14,21 @@
|
||||
|
||||
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
|
||||
// é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.
|
||||
@@ -93,40 +108,40 @@ namespace {
|
||||
void parseWindow(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("window")) { return; }
|
||||
const auto& node = yaml["window"];
|
||||
readField(node, "zoom", Options::window.zoom);
|
||||
readField(node, "fullscreen", Options::window.fullscreen);
|
||||
readField(node, "zoom", g_window.zoom);
|
||||
readField(node, "fullscreen", g_window.fullscreen);
|
||||
}
|
||||
|
||||
void parseGpu(const fkyaml::node& video_node) {
|
||||
if (!video_node.contains("gpu")) { return; }
|
||||
const auto& node = video_node["gpu"];
|
||||
readField(node, "acceleration", Options::video.gpu.acceleration);
|
||||
readField(node, "preferred_driver", Options::video.gpu.preferred_driver);
|
||||
readField(node, "acceleration", g_video.gpu.acceleration);
|
||||
readField(node, "preferred_driver", g_video.gpu.preferred_driver);
|
||||
}
|
||||
|
||||
void parseShader(const fkyaml::node& video_node) {
|
||||
if (!video_node.contains("shader")) { return; }
|
||||
const auto& node = video_node["shader"];
|
||||
readField(node, "enabled", Options::video.shader.enabled);
|
||||
readField(node, "current_shader", Options::video.shader.current_shader);
|
||||
readField(node, "current_postfx_preset", Options::video.shader.current_postfx_preset);
|
||||
readField(node, "current_crtpi_preset", Options::video.shader.current_crtpi_preset);
|
||||
readField(node, "enabled", g_video.shader.enabled);
|
||||
readField(node, "current_shader", g_video.shader.current_shader);
|
||||
readField(node, "current_postfx_preset", g_video.shader.current_postfx_preset);
|
||||
readField(node, "current_crtpi_preset", g_video.shader.current_crtpi_preset);
|
||||
}
|
||||
|
||||
void parseVideo(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("video")) { return; }
|
||||
const auto& node = yaml["video"];
|
||||
readField(node, "vsync", Options::video.vsync);
|
||||
readField(node, "vsync", g_video.vsync);
|
||||
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")) {
|
||||
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")) {
|
||||
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);
|
||||
parseShader(node);
|
||||
}
|
||||
@@ -134,22 +149,22 @@ namespace {
|
||||
void parseMusic(const fkyaml::node& audio_node) {
|
||||
if (!audio_node.contains("music")) { return; }
|
||||
const auto& node = audio_node["music"];
|
||||
readField(node, "enabled", Options::audio.music.enabled);
|
||||
readField(node, "volume", Options::audio.music.volume);
|
||||
readField(node, "enabled", g_audio.music.enabled);
|
||||
readField(node, "volume", g_audio.music.volume);
|
||||
}
|
||||
|
||||
void parseSound(const fkyaml::node& audio_node) {
|
||||
if (!audio_node.contains("sound")) { return; }
|
||||
const auto& node = audio_node["sound"];
|
||||
readField(node, "enabled", Options::audio.sound.enabled);
|
||||
readField(node, "volume", Options::audio.sound.volume);
|
||||
readField(node, "enabled", g_audio.sound.enabled);
|
||||
readField(node, "volume", g_audio.sound.volume);
|
||||
}
|
||||
|
||||
void parseAudio(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("audio")) { return; }
|
||||
const auto& node = yaml["audio"];
|
||||
readField(node, "enabled", Options::audio.enabled);
|
||||
readField(node, "volume", Options::audio.volume);
|
||||
readField(node, "enabled", g_audio.enabled);
|
||||
readField(node, "volume", g_audio.volume);
|
||||
parseMusic(node);
|
||||
parseSound(node);
|
||||
}
|
||||
@@ -423,14 +438,65 @@ namespace {
|
||||
|
||||
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) {
|
||||
config_file_path = path;
|
||||
g_config_file_path = path;
|
||||
}
|
||||
|
||||
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()) {
|
||||
// Primera execució: creem el fitxer amb els valors per defecte.
|
||||
return saveToFile();
|
||||
@@ -449,12 +515,12 @@ namespace Options {
|
||||
if (file_version != Texts::VERSION) {
|
||||
std::cout << "Options: config.yaml versió \"" << file_version
|
||||
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
|
||||
backupFile(config_file_path);
|
||||
backupFile(g_config_file_path);
|
||||
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);
|
||||
parseVideo(yaml);
|
||||
parseAudio(yaml);
|
||||
@@ -462,84 +528,84 @@ namespace Options {
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
|
||||
std::cout << "Options: reescrivint el fitxer amb defaults.\n";
|
||||
backupFile(config_file_path);
|
||||
backupFile(g_config_file_path);
|
||||
return saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n";
|
||||
out << "version: \"" << config_version << "\"\n";
|
||||
out << "locale: \"" << locale << "\"\n\n";
|
||||
out << "version: \"" << g_config_version << "\"\n";
|
||||
out << "locale: \"" << g_locale << "\"\n\n";
|
||||
|
||||
out << "window:\n";
|
||||
out << " zoom: " << window.zoom << "\n";
|
||||
out << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n\n";
|
||||
out << " zoom: " << g_window.zoom << "\n";
|
||||
out << " fullscreen: " << (g_window.fullscreen ? "true" : "false") << "\n\n";
|
||||
|
||||
out << "video:\n";
|
||||
out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
||||
out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, video.scaling_mode) << "\"\n";
|
||||
out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, video.texture_filter) << "\"\n";
|
||||
out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, video.render_info_position) << "\"\n";
|
||||
out << " render_info_show_time: " << (video.render_info_show_time ? "true" : "false") << "\n";
|
||||
out << " vsync: " << (g_video.vsync ? "true" : "false") << "\n";
|
||||
out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, g_video.scaling_mode) << "\"\n";
|
||||
out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, g_video.texture_filter) << "\"\n";
|
||||
out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, g_video.render_info_position) << "\"\n";
|
||||
out << " render_info_show_time: " << (g_video.render_info_show_time ? "true" : "false") << "\n";
|
||||
out << " gpu:\n";
|
||||
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
|
||||
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
|
||||
out << " acceleration: " << (g_video.gpu.acceleration ? "true" : "false") << "\n";
|
||||
out << " preferred_driver: \"" << g_video.gpu.preferred_driver << "\"\n";
|
||||
out << " shader:\n";
|
||||
out << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
|
||||
out << " current_shader: \"" << video.shader.current_shader << "\"\n";
|
||||
out << " current_postfx_preset: \"" << video.shader.current_postfx_preset << "\"\n";
|
||||
out << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset << "\"\n\n";
|
||||
out << " enabled: " << (g_video.shader.enabled ? "true" : "false") << "\n";
|
||||
out << " current_shader: \"" << g_video.shader.current_shader << "\"\n";
|
||||
out << " current_postfx_preset: \"" << g_video.shader.current_postfx_preset << "\"\n";
|
||||
out << " current_crtpi_preset: \"" << g_video.shader.current_crtpi_preset << "\"\n\n";
|
||||
|
||||
out << "audio:\n";
|
||||
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << audio.volume << "\n";
|
||||
out << " enabled: " << (g_audio.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << g_audio.volume << "\n";
|
||||
out << " music:\n";
|
||||
out << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << audio.music.volume << "\n";
|
||||
out << " enabled: " << (g_audio.music.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << g_audio.music.volume << "\n";
|
||||
out << " sound:\n";
|
||||
out << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n";
|
||||
out << " volume: " << audio.sound.volume << "\n";
|
||||
out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\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 {
|
||||
return {
|
||||
.zoom = window.zoom,
|
||||
.fullscreen = window.fullscreen,
|
||||
.vsync = video.vsync,
|
||||
.scaling_mode = video.scaling_mode,
|
||||
.texture_filter = video.texture_filter,
|
||||
.gpu_acceleration = video.gpu.acceleration,
|
||||
.preferred_driver = video.gpu.preferred_driver,
|
||||
.zoom = g_window.zoom,
|
||||
.fullscreen = g_window.fullscreen,
|
||||
.vsync = g_video.vsync,
|
||||
.scaling_mode = g_video.scaling_mode,
|
||||
.texture_filter = g_video.texture_filter,
|
||||
.gpu_acceleration = g_video.gpu.acceleration,
|
||||
.preferred_driver = g_video.gpu.preferred_driver,
|
||||
};
|
||||
}
|
||||
|
||||
auto buildAudioConfig() -> ::Audio::Config {
|
||||
return {
|
||||
.enabled = audio.enabled,
|
||||
.volume = audio.volume,
|
||||
.music_enabled = audio.music.enabled,
|
||||
.music_volume = audio.music.volume,
|
||||
.sound_enabled = audio.sound.enabled,
|
||||
.sound_volume = audio.sound.volume,
|
||||
.enabled = g_audio.enabled,
|
||||
.volume = g_audio.volume,
|
||||
.music_enabled = g_audio.music.enabled,
|
||||
.music_volume = g_audio.music.volume,
|
||||
.sound_enabled = g_audio.sound.enabled,
|
||||
.sound_volume = g_audio.sound.volume,
|
||||
};
|
||||
}
|
||||
|
||||
void setPostFXFile(const std::string& path) {
|
||||
postfx_file_path = path;
|
||||
g_postfx_file_path = path;
|
||||
}
|
||||
|
||||
auto loadPostFXFromFile() -> bool {
|
||||
return loadPresetsFromFile<PostFXPreset>(
|
||||
postfx_file_path,
|
||||
postfx_presets,
|
||||
g_postfx_file_path,
|
||||
g_postfx_presets,
|
||||
&defaultPostFXPresets,
|
||||
&parsePostFXPreset,
|
||||
&savePostFXToFile,
|
||||
@@ -548,21 +614,21 @@ namespace Options {
|
||||
|
||||
auto savePostFXToFile() -> bool {
|
||||
return savePresetsToFile<PostFXPreset>(
|
||||
postfx_file_path,
|
||||
postfx_presets,
|
||||
g_postfx_file_path,
|
||||
g_postfx_presets,
|
||||
&writePostFXPreset,
|
||||
"Presets PostFX per a AEE 2026. Editable a mà.",
|
||||
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
|
||||
}
|
||||
|
||||
void setCrtPiFile(const std::string& path) {
|
||||
crtpi_file_path = path;
|
||||
g_crtpi_file_path = path;
|
||||
}
|
||||
|
||||
auto loadCrtPiFromFile() -> bool {
|
||||
return loadPresetsFromFile<CrtPiPreset>(
|
||||
crtpi_file_path,
|
||||
crtpi_presets,
|
||||
g_crtpi_file_path,
|
||||
g_crtpi_presets,
|
||||
&defaultCrtPiPresets,
|
||||
&parseCrtPiPreset,
|
||||
&saveCrtPiToFile,
|
||||
@@ -571,29 +637,29 @@ namespace Options {
|
||||
|
||||
auto saveCrtPiToFile() -> bool {
|
||||
return savePresetsToFile<CrtPiPreset>(
|
||||
crtpi_file_path,
|
||||
crtpi_presets,
|
||||
g_crtpi_file_path,
|
||||
g_crtpi_presets,
|
||||
&writeCrtPiPreset,
|
||||
"Presets CRT-Pi per a AEE 2026. Editable a mà.",
|
||||
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
|
||||
}
|
||||
|
||||
auto buildActiveShader() -> Rendering::ShaderType {
|
||||
if (video.shader.current_shader == "crtpi") {
|
||||
if (g_video.shader.current_shader == "crtpi") {
|
||||
return Rendering::ShaderType::CRTPI;
|
||||
}
|
||||
return Rendering::ShaderType::POSTFX;
|
||||
}
|
||||
|
||||
auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame {
|
||||
const std::string& active_preset = (video.shader.current_shader == "crtpi")
|
||||
? video.shader.current_crtpi_preset
|
||||
: video.shader.current_postfx_preset;
|
||||
const std::string& active_preset = (g_video.shader.current_shader == "crtpi")
|
||||
? g_video.shader.current_crtpi_preset
|
||||
: g_video.shader.current_postfx_preset;
|
||||
return {
|
||||
.target_position = video.render_info_position,
|
||||
.show_time = video.render_info_show_time,
|
||||
.shader_enabled = video.shader.enabled,
|
||||
.shader_name = video.shader.current_shader,
|
||||
.target_position = g_video.render_info_position,
|
||||
.show_time = g_video.render_info_show_time,
|
||||
.shader_enabled = g_video.shader.enabled,
|
||||
.shader_name = g_video.shader.current_shader,
|
||||
.shader_preset = active_preset,
|
||||
.text_color = text_color,
|
||||
.shadow_color = shadow_color,
|
||||
@@ -603,12 +669,12 @@ namespace Options {
|
||||
auto buildCrtPiParams() -> Rendering::CrtPiParams {
|
||||
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
|
||||
// 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 CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; });
|
||||
if (IT == crtpi_presets.end()) {
|
||||
std::cout << "Options: preset CrtPi \"" << video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
|
||||
const auto IT = std::ranges::find_if(g_crtpi_presets,
|
||||
[](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
|
||||
if (IT == g_crtpi_presets.end()) {
|
||||
std::cout << "Options: preset CrtPi \"" << g_video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
@@ -631,12 +697,12 @@ namespace Options {
|
||||
|
||||
auto buildPostFXParams() -> Rendering::PostFXParams {
|
||||
// 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 PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; });
|
||||
if (IT == postfx_presets.end()) {
|
||||
std::cout << "Options: preset PostFX \"" << video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
|
||||
const auto IT = std::ranges::find_if(g_postfx_presets,
|
||||
[](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
|
||||
if (IT == g_postfx_presets.end()) {
|
||||
std::cout << "Options: preset PostFX \"" << g_video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
// del config.yaml. Els subsistemes (Screen, Audio, ...) reben structs tipats
|
||||
// a través dels build*Config(), de manera que no queden acoblats a aquest
|
||||
// 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 {
|
||||
|
||||
struct Window {
|
||||
@@ -95,20 +104,30 @@ namespace Options {
|
||||
Sound sound{};
|
||||
};
|
||||
|
||||
// --- Estat global (inline vars per a que una sola definició viva a tot
|
||||
// el programa sense necessitar un .cpp extra per a cada una) ---
|
||||
inline std::string config_version{};
|
||||
inline std::string config_file_path{};
|
||||
inline std::string postfx_file_path{};
|
||||
inline std::string crtpi_file_path{};
|
||||
inline std::string locale{Defaults::I18n::LOCALE};
|
||||
inline Window window{};
|
||||
inline Video video{};
|
||||
inline Audio audio{};
|
||||
inline std::vector<PostFXPreset> postfx_presets{};
|
||||
inline std::vector<CrtPiPreset> crtpi_presets{};
|
||||
// --- Getters de l'estat (read-only) ---
|
||||
[[nodiscard]] auto window() -> const Window&;
|
||||
[[nodiscard]] auto video() -> const Video&;
|
||||
[[nodiscard]] auto audio() -> const Audio&;
|
||||
[[nodiscard]] auto locale() -> const std::string&;
|
||||
[[nodiscard]] auto configVersion() -> const std::string&;
|
||||
[[nodiscard]] auto postfxPresets() -> const std::vector<PostFXPreset>&;
|
||||
[[nodiscard]] auto crtpiPresets() -> const std::vector<CrtPiPreset>&;
|
||||
|
||||
// --- 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);
|
||||
auto loadFromFile() -> bool;
|
||||
auto saveToFile() -> bool;
|
||||
|
||||
@@ -98,7 +98,7 @@ GameScene::GameScene(Config config)
|
||||
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_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
|
||||
// sarcòfags rebin les notificacions de pas per tile.
|
||||
|
||||
@@ -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
|
||||
// sense més impacte que dos accessors extra.
|
||||
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).
|
||||
constexpr Uint8 RENDER_INFO_TEXT = 47;
|
||||
constexpr Uint8 RENDER_INFO_SHADOW = 34;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Texts {
|
||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||
constexpr const char* VERSION = "0.18.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 GameCanvas {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||