Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa27308897 | ||
|
|
e58eb10d6d | ||
|
|
a29ea641ab | ||
|
|
28ff425c89 | ||
|
|
e8418f89e0 | ||
|
|
9ca09484a6 | ||
|
|
9291ef6572 | ||
|
|
6f8ab74d6b | ||
|
|
881e0ea1f6 | ||
|
|
6a6a1a52c7 | ||
|
|
62f4b857ef | ||
|
|
3b378e2b29 | ||
|
|
041447eaa4 | ||
|
|
a6b2b02930 | ||
|
|
b7811180ba | ||
|
|
2cbf1bb6e0 | ||
|
|
8890fa9e1f | ||
|
|
23f7cb03a6 | ||
|
|
486a440668 | ||
|
|
a717411edd | ||
|
|
5e862ceadc | ||
|
|
78e164fce4 | ||
|
|
c13b11c501 | ||
|
|
8921b9fd0c | ||
|
|
1d9fea28fe | ||
|
|
d0d5cb0e66 | ||
|
|
e25ccd02fd | ||
|
|
3635cebf25 | ||
|
|
cfbcdc236b | ||
|
|
7871d3930e | ||
|
|
932b457725 | ||
|
|
e9f99a0542 | ||
|
|
0d58da536c | ||
|
|
583acb9053 | ||
|
|
babb111b6f | ||
|
|
866e133aa9 | ||
|
|
170521aba6 | ||
|
|
c8882a3f6e | ||
|
|
5fce52e0ad | ||
|
|
0b708f1515 | ||
|
|
6e99fd0a8b | ||
|
|
1243b457be | ||
|
|
c910a16834 | ||
|
|
0ec1a2548a | ||
|
|
4ea8ab7b6d | ||
|
|
05465b791e | ||
|
|
9949096fba | ||
|
|
ea5762265e | ||
|
|
78732042cb | ||
|
|
f10671d9c0 | ||
|
|
7d5a4cee8e | ||
|
|
d560623e00 | ||
|
|
3f88dfaaee | ||
|
|
e1bd75e00d | ||
|
|
e32cff9dac | ||
|
|
243ced7498 | ||
|
|
fceeb11cdd | ||
|
|
233fcb8226 | ||
|
|
8bc45ccedc | ||
|
|
f7ab6a641a | ||
|
|
8eaa91f309 | ||
|
|
23fdf6e1bd | ||
|
|
1431f27cfa | ||
|
|
29cb4c2211 | ||
|
|
3f09858203 | ||
|
|
b6d6ee471c | ||
|
|
25ce524470 | ||
|
|
921e10bbb3 | ||
|
|
36fc496226 | ||
|
|
0618452224 | ||
|
|
ca64115bfe | ||
|
|
1fc3ab8e31 | ||
|
|
9233bd8d75 | ||
|
|
e7a024bcdb | ||
|
|
158968e2c2 | ||
|
|
dadbee9bad | ||
|
|
5710568af8 | ||
|
|
165499b9d4 |
No files matched your search
@@ -8,8 +8,8 @@ dist/
|
||||
*.dylib
|
||||
*.dll
|
||||
*.exe
|
||||
aee_2026
|
||||
_aee_2026
|
||||
aee_arcade
|
||||
_aee_arcade
|
||||
.vscode/
|
||||
.idea/
|
||||
.cache/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(aee_2026 VERSION 0.1.0 LANGUAGES CXX)
|
||||
project(aee_arcade VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||
@@ -92,6 +92,10 @@ set(APP_SOURCES
|
||||
source/core/rendering/sprite/moving_sprite.cpp
|
||||
source/core/rendering/sprite/animated_sprite.cpp
|
||||
source/core/rendering/sprite/animated_sprite_parser.cpp
|
||||
source/core/rendering/effects/heat_haze.cpp
|
||||
source/core/rendering/effects/scan_reveal.cpp
|
||||
source/core/rendering/transitions/mosaic.cpp
|
||||
source/core/rendering/transitions/fade_to_black.cpp
|
||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||
source/core/rendering/sdl3gpu/viewport.cpp
|
||||
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
||||
@@ -100,18 +104,22 @@ set(APP_SOURCES
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/core/resources/palette_loader.cpp
|
||||
source/utils/easing.cpp
|
||||
source/utils/log.cpp
|
||||
source/utils/utils.cpp
|
||||
source/game/debug_config.cpp
|
||||
source/game/gameplay_config.cpp
|
||||
source/game/options.cpp
|
||||
source/game/options_apply.cpp
|
||||
source/game/ui/service_menu.cpp
|
||||
source/game/ui/hud.cpp
|
||||
source/game/scenes/placeholder_input.cpp
|
||||
source/game/scenes/start_input.cpp
|
||||
source/game/scenes/logo_scene.cpp
|
||||
source/game/scenes/title_scene.cpp
|
||||
source/game/scenes/intro_scene.cpp
|
||||
source/game/scenes/game_scene.cpp
|
||||
source/game/scenes/room_fsm.cpp
|
||||
source/game/scenes/game_over_fsm.cpp
|
||||
source/game/entities/player.cpp
|
||||
source/game/map/map.cpp
|
||||
source/game/map/tilemap.cpp
|
||||
@@ -214,8 +222,10 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
||||
add_executable(pack_resources EXCLUDE_FROM_ALL
|
||||
tools/pack_resources/pack_resources.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/utils/log.cpp
|
||||
)
|
||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||
target_link_libraries(pack_resources PRIVATE SDL3::SDL3)
|
||||
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
||||
@@ -357,9 +367,11 @@ if(BUILD_TESTS)
|
||||
add_executable(test_entities_smoke
|
||||
tests/test_entities_smoke.cpp
|
||||
tests/hole_player_stubs.cpp
|
||||
tests/audio_cache_stubs.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/game/map/map.cpp
|
||||
source/utils/log.cpp
|
||||
${ENTITY_SOURCES_BASE}
|
||||
)
|
||||
target_include_directories(test_entities_smoke PRIVATE
|
||||
@@ -385,9 +397,12 @@ if(BUILD_TESTS)
|
||||
tests/test_whip_attack.cpp
|
||||
tests/whip_attack_player_stubs.cpp
|
||||
tests/hole_player_stubs.cpp
|
||||
tests/audio_cache_stubs.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/game/gameplay_config.cpp
|
||||
source/game/map/map.cpp
|
||||
source/utils/log.cpp
|
||||
${ENTITY_SOURCES_BASE}
|
||||
source/game/entities/concrete/whip_attack.cpp
|
||||
)
|
||||
@@ -412,6 +427,7 @@ if(BUILD_TESTS)
|
||||
source/core/input/key_config.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/utils/log.cpp
|
||||
)
|
||||
target_include_directories(test_scene_flow PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
@@ -451,6 +467,7 @@ if(BUILD_TESTS)
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/utils/easing.cpp
|
||||
source/utils/log.cpp
|
||||
)
|
||||
target_include_directories(test_game_fsm PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
PROJECT := aee_2026
|
||||
PROJECT := aee_arcade
|
||||
BUILDDIR := build
|
||||
|
||||
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# AEE 2026 — Tecles d'UI (Global Inputs)
|
||||
# AEE ARCADE — Tecles d'UI (Global Inputs)
|
||||
#
|
||||
# Mapeig id → codi SDL (el que reconeix SDL_GetScancodeFromName). Les tecles
|
||||
# de moviment del jugador viuen a part (Defaults::KeysGame); ací només hi ha
|
||||
@@ -58,6 +58,15 @@ fonts:
|
||||
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||
kerning: 0
|
||||
# Copyright de la TITLE: pixel-art estricte (Tiny5-Regular size 5,
|
||||
# caixa 3×6 px) generat amb font-gen. Diferenciada de la `placeholder`
|
||||
# que pinta el "press start" — així la jerarquia tipogràfica del peu
|
||||
# queda neta.
|
||||
copyright:
|
||||
name: "Tiny5-Regular"
|
||||
gif: "data/fonts/Tiny5-Regular.gif"
|
||||
fnt: "data/fonts/Tiny5-Regular.fnt"
|
||||
kerning: 1
|
||||
|
||||
hud:
|
||||
bg_enabled: false
|
||||
@@ -92,9 +101,15 @@ pause:
|
||||
enter_curve: outCubic
|
||||
exit_curve: inCubic
|
||||
|
||||
# Sons del menú de servei. Els fitxers viuen a data/sounds/. SELECT
|
||||
# Sons del menú de servei. Els fitxers viuen a data/audio/sounds/. SELECT
|
||||
# acompanya navegació (amunt/avall) i back; ACCEPT acompanya modificació
|
||||
# d'opció (esquerra/dreta/retorn) o entrada a un submenú.
|
||||
sounds:
|
||||
menu_select: "menu_select.wav"
|
||||
menu_accept: "menu_accept.wav"
|
||||
# Disparat quan un jugador prem START a la TITLE (primera pulsació o
|
||||
# la del segon jugador dins de la finestra de STARTING).
|
||||
player_start: "player_start.wav"
|
||||
# Soroll/scratch sincronitzat amb la fase SCAN de LogoScene (scanline
|
||||
# reveal). Es dispara al constructor de l'escena.
|
||||
logo_glitch: "glitch.wav"
|
||||
@@ -40,6 +40,14 @@ ai:
|
||||
type: corridor_wanderer
|
||||
direction_choice: random
|
||||
initial_direction: east
|
||||
|
||||
# Estat SPAWN: la mòmia s'instancia visible al pixel_rect_ inicial just
|
||||
# quan GameScene crida `spawnPendingActors()` (a la subfase text-exit
|
||||
# de ROOM_INTRO, fase 4). El `triggerSpawnExit` es dispara llavors
|
||||
# mateix; passats `duration_s` segons transita a ROAMING i empieza a
|
||||
# deambular. Sense aquest bloc, default 0 ⇒ ACTIVE immediat.
|
||||
spawn:
|
||||
duration_s: 1.0
|
||||
stats:
|
||||
speed: 30
|
||||
damage: 1
|
||||
@@ -58,3 +66,6 @@ hurt:
|
||||
duration_ms: 600
|
||||
speed: 60
|
||||
whippable: true
|
||||
# So opcional: basename del fitxer dins `data/audio/sounds/`. Es dispara
|
||||
# un sol cop per fuetada quan la mòmia entra a HURT. Si manca, no sona.
|
||||
sound: mummy_hurt.wav
|
||||
@@ -40,10 +40,10 @@ animations:
|
||||
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
|
||||
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
|
||||
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
|
||||
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
|
||||
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
|
||||
attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
|
||||
attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
|
||||
attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
|
||||
attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
|
||||
|
||||
recover_south: { frames: [23] }
|
||||
recover_north: { frames: [27] }
|
||||
@@ -119,7 +119,7 @@ hurt:
|
||||
# recuperació en què la hitbox ja no fa dany.
|
||||
whip:
|
||||
range_tiles: 3
|
||||
swing_ms: 200
|
||||
swing_ms: 320
|
||||
cooldown_ms: 200
|
||||
damage: 1
|
||||
sound: whip.wav
|
||||
@@ -136,7 +136,7 @@ death:
|
||||
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
||||
# caiguda no és dany convencional). Visualment es manifesta amb un
|
||||
# parpadeig que alterna la paleta normal del sprite amb la paleta
|
||||
# `palette` (basename de `data/player/<name>.pal`) cada
|
||||
# `palette` (basename de `data/entities/player/<name>.pal`) cada
|
||||
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
|
||||
# mecànica queda desactivada i el comportament és l'antic.
|
||||
immunity:
|
||||
@@ -40,10 +40,10 @@ animations:
|
||||
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
|
||||
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
|
||||
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
|
||||
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
|
||||
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
|
||||
attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
|
||||
attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
|
||||
attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
|
||||
attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
|
||||
|
||||
recover_south: { frames: [23] }
|
||||
recover_north: { frames: [27] }
|
||||
@@ -119,7 +119,7 @@ hurt:
|
||||
# recuperació en què la hitbox ja no fa dany.
|
||||
whip:
|
||||
range_tiles: 3
|
||||
swing_ms: 200
|
||||
swing_ms: 320
|
||||
cooldown_ms: 200
|
||||
damage: 1
|
||||
sound: whip.wav
|
||||
@@ -136,7 +136,7 @@ death:
|
||||
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
||||
# caiguda no és dany convencional). Visualment es manifesta amb un
|
||||
# parpadeig que alterna la paleta normal del sprite amb la paleta
|
||||
# `palette` (basename de `data/player/<name>.pal`) cada
|
||||
# `palette` (basename de `data/entities/player/<name>.pal`) cada
|
||||
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
|
||||
# mecànica queda desactivada i el comportament és l'antic.
|
||||
immunity:
|
||||
@@ -25,7 +25,7 @@ sprites:
|
||||
# Animació compartida entre les 4 direccions: la seqüència de frames i les
|
||||
# durades són idèntiques; només canvia quina surface/mida es clipa.
|
||||
# - `frames` : índex de tile dins l'spritesheet (graella deduïda).
|
||||
# - `frame_ms` : durada uniforme per frame; 50 ms × 4 ≈ 200 ms swing.
|
||||
# - `frame_ms` : durada uniforme per frame; 80 ms × 4 = 320 ms swing.
|
||||
# - `loop: false` : el látigo viu una animació i mor (`markDead`).
|
||||
# - `active_frames` : valors dins de `frames` durant els quals la
|
||||
# hitbox aplica dany. Wind-up (0) i recovery (3) no
|
||||
@@ -33,6 +33,6 @@ sprites:
|
||||
animations:
|
||||
swing:
|
||||
frames: [0, 1, 2, 3]
|
||||
frame_ms: 50
|
||||
frame_ms: 80
|
||||
loop: false
|
||||
active_frames: [1, 2]
|
||||
|
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 363 B |
|
Before Width: | Height: | Size: 369 B After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 327 B |
@@ -0,0 +1,133 @@
|
||||
# Font: Tiny5-Regular — generado desde Tiny5-Regular.ttf size 8
|
||||
# Generado con font_gen.py
|
||||
|
||||
box_width 6
|
||||
box_height 9
|
||||
columns 15
|
||||
|
||||
# codepoint_decimal ancho_visual
|
||||
32 2 # U+0020
|
||||
33 1 # !
|
||||
34 3 # "
|
||||
35 5 # #
|
||||
36 3 # $
|
||||
37 4 # %
|
||||
38 4 # &
|
||||
39 1 # '
|
||||
40 2 # (
|
||||
41 2 # )
|
||||
42 3 # *
|
||||
43 3 # +
|
||||
44 2 # ,
|
||||
45 3 # -
|
||||
46 1 # .
|
||||
47 2 # /
|
||||
48 3 # 0
|
||||
49 2 # 1
|
||||
50 3 # 2
|
||||
51 3 # 3
|
||||
52 3 # 4
|
||||
53 3 # 5
|
||||
54 3 # 6
|
||||
55 3 # 7
|
||||
56 3 # 8
|
||||
57 3 # 9
|
||||
58 1 # :
|
||||
59 2 # ;
|
||||
60 3 # <
|
||||
61 3 # =
|
||||
62 3 # >
|
||||
63 3 # ?
|
||||
64 4 # @
|
||||
65 4 # A
|
||||
66 4 # B
|
||||
67 4 # C
|
||||
68 4 # D
|
||||
69 4 # E
|
||||
70 4 # F
|
||||
71 4 # G
|
||||
72 4 # H
|
||||
73 1 # I
|
||||
74 3 # J
|
||||
75 4 # K
|
||||
76 3 # L
|
||||
77 5 # M
|
||||
78 4 # N
|
||||
79 4 # O
|
||||
80 4 # P
|
||||
81 4 # Q
|
||||
82 4 # R
|
||||
83 4 # S
|
||||
84 3 # T
|
||||
85 4 # U
|
||||
86 3 # V
|
||||
87 5 # W
|
||||
88 3 # X
|
||||
89 3 # Y
|
||||
90 3 # Z
|
||||
91 2 # [
|
||||
92 2 # \
|
||||
93 2 # ]
|
||||
94 3 # ^
|
||||
95 3 # _
|
||||
96 2 # `
|
||||
97 3 # a
|
||||
98 3 # b
|
||||
99 3 # c
|
||||
100 3 # d
|
||||
101 3 # e
|
||||
102 2 # f
|
||||
103 3 # g
|
||||
104 3 # h
|
||||
105 1 # i
|
||||
106 1 # j
|
||||
107 3 # k
|
||||
108 1 # l
|
||||
109 5 # m
|
||||
110 3 # n
|
||||
111 3 # o
|
||||
112 3 # p
|
||||
113 3 # q
|
||||
114 2 # r
|
||||
115 3 # s
|
||||
116 2 # t
|
||||
117 3 # u
|
||||
118 3 # v
|
||||
119 5 # w
|
||||
120 3 # x
|
||||
121 3 # y
|
||||
122 3 # z
|
||||
123 3 # {
|
||||
124 1 # |
|
||||
125 3 # }
|
||||
126 2 # ~
|
||||
192 4 # À
|
||||
193 4 # Á
|
||||
200 4 # È
|
||||
201 4 # É
|
||||
204 2 # Ì
|
||||
205 2 # Í
|
||||
210 4 # Ò
|
||||
211 4 # Ó
|
||||
217 4 # Ù
|
||||
218 4 # Ú
|
||||
209 4 # Ñ
|
||||
199 4 # Ç
|
||||
224 3 # à
|
||||
225 3 # á
|
||||
232 3 # è
|
||||
233 3 # é
|
||||
236 2 # ì
|
||||
237 2 # í
|
||||
242 3 # ò
|
||||
243 3 # ó
|
||||
249 3 # ù
|
||||
250 3 # ú
|
||||
241 3 # ñ
|
||||
231 3 # ç
|
||||
161 1 # ¡
|
||||
191 3 # ¿
|
||||
171 5 # «
|
||||
187 5 # »
|
||||
183 1 # ·
|
||||
169 3 # ©
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,8 +1,8 @@
|
||||
# Font: ari-w9500-condensed-display — generado desde ari-w9500-condensed-display.ttf size 11
|
||||
# Generado con tools/font_gen/font_gen.py
|
||||
# Generado con font_gen.py
|
||||
|
||||
box_width 12
|
||||
box_height 11
|
||||
box_height 14
|
||||
columns 15
|
||||
|
||||
# codepoint_decimal ancho_visual
|
||||
@@ -105,8 +105,8 @@ columns 15
|
||||
193 8 # Á
|
||||
200 7 # È
|
||||
201 7 # É
|
||||
204 3 # Ì
|
||||
205 3 # Í
|
||||
204 4 # Ì
|
||||
205 4 # Í
|
||||
210 9 # Ò
|
||||
211 9 # Ó
|
||||
217 7 # Ù
|
||||
@@ -118,7 +118,7 @@ columns 15
|
||||
232 7 # è
|
||||
233 7 # é
|
||||
236 4 # ì
|
||||
237 3 # í
|
||||
237 4 # í
|
||||
242 7 # ò
|
||||
243 7 # ó
|
||||
249 7 # ù
|
||||
@@ -130,3 +130,4 @@ columns 15
|
||||
171 10 # «
|
||||
187 10 # »
|
||||
183 3 # ·
|
||||
169 10 # ©
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,8 +1,8 @@
|
||||
# Font: ari-w9500-display — generado desde ari-w9500-display.ttf size 11
|
||||
# Generado con tools/font_gen/font_gen.py
|
||||
# Generado con font_gen.py
|
||||
|
||||
box_width 12
|
||||
box_height 11
|
||||
box_height 14
|
||||
columns 15
|
||||
|
||||
# codepoint_decimal ancho_visual
|
||||
@@ -105,8 +105,8 @@ columns 15
|
||||
193 9 # Á
|
||||
200 8 # È
|
||||
201 8 # É
|
||||
204 3 # Ì
|
||||
205 3 # Í
|
||||
204 4 # Ì
|
||||
205 4 # Í
|
||||
210 10 # Ò
|
||||
211 10 # Ó
|
||||
217 8 # Ù
|
||||
@@ -118,7 +118,7 @@ columns 15
|
||||
232 8 # è
|
||||
233 8 # é
|
||||
236 4 # ì
|
||||
237 3 # í
|
||||
237 4 # í
|
||||
242 8 # ò
|
||||
243 8 # ó
|
||||
249 8 # ù
|
||||
@@ -130,3 +130,4 @@ columns 15
|
||||
171 10 # «
|
||||
187 10 # »
|
||||
183 3 # ·
|
||||
169 10 # ©
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -1,4 +1,4 @@
|
||||
# AEE 2026 — Català
|
||||
# AEE ARCADE — Català
|
||||
# lang: ca
|
||||
|
||||
notification:
|
||||
@@ -38,6 +38,17 @@ hud:
|
||||
treasures: "Tres"
|
||||
pyramid: "Piramide"
|
||||
room: "Sala"
|
||||
# Prompts mostrats al bloc d'un slot inactiu (en lloc dels zeros)
|
||||
# quan un jugador encara no ha pulsat START o ha esgotat vides.
|
||||
# Dues línies per fer-los caber al bloc lateral (~106 px).
|
||||
join_prompt_can_join_l1: "POLSA START"
|
||||
join_prompt_can_join_l2: "PER JUGAR"
|
||||
join_prompt_wait_l1: "ESPERA"
|
||||
join_prompt_wait_l2: "PER JUGAR"
|
||||
|
||||
# Textos de l'escena TITLE.
|
||||
title:
|
||||
press_start: "Polsa START per a començar"
|
||||
|
||||
# Textos de la transició d'entrada/sortida de sala (FSM de GameScene).
|
||||
# Tant l'INTRO com l'OUTRO es renderitzen com a dues línies separades
|
||||
@@ -49,6 +60,12 @@ game:
|
||||
room_intro_room: "Sala {r}"
|
||||
room_complete_line1: "Fase"
|
||||
room_complete_line2: "Completada"
|
||||
# Rètol del game over (sub-FSM de la GameScene). El countdown es
|
||||
# pinta com a segona línia (un enter) durant la fase 1 de
|
||||
# CONTINUE_PROMPT — només cal traduir el rètol superior.
|
||||
continue_prompt: "Continuar?"
|
||||
game_over_line1: "Fi del"
|
||||
game_over_line2: "Joc"
|
||||
|
||||
menu:
|
||||
titles:
|
||||
@@ -58,6 +75,7 @@ menu:
|
||||
controls: "Controls"
|
||||
keyboard: "Teclat"
|
||||
gamepad: "Comandament"
|
||||
tweaks: "Jugabilitat"
|
||||
system: "Sistema"
|
||||
labels:
|
||||
player_1: "Jugador 1"
|
||||
@@ -69,6 +87,7 @@ menu:
|
||||
controls_submenu: "Controls"
|
||||
keyboard_submenu: "Teclat"
|
||||
gamepad_submenu: "Comandament"
|
||||
tweaks_submenu: "Jugabilitat"
|
||||
system_submenu: "Sistema"
|
||||
move_up: "Amunt"
|
||||
move_down: "Avall"
|
||||
@@ -76,6 +95,7 @@ menu:
|
||||
move_right: "Dreta"
|
||||
jump: "Saltar"
|
||||
attack: "Atacar"
|
||||
start: "Start"
|
||||
service: "Opcions"
|
||||
gamepad_p1: "Comandament P1"
|
||||
gamepad_p2: "Comandament P2"
|
||||
@@ -96,6 +116,9 @@ menu:
|
||||
music_volume: "Volum música"
|
||||
sounds: "Sons"
|
||||
sounds_volume: "Volum sons"
|
||||
whip_pierces_walls: "Látec travessa sarcs"
|
||||
friendly_fire: "Foc amic"
|
||||
players_collide: "Col·lisió entre jugadors"
|
||||
restart: "Reiniciar"
|
||||
exit_game: "Eixir del joc"
|
||||
values:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# AEE 2026 — English
|
||||
# AEE ARCADE — English
|
||||
# lang: en
|
||||
|
||||
notification:
|
||||
@@ -38,6 +38,17 @@ hud:
|
||||
treasures: "Tre"
|
||||
pyramid: "Pyramid"
|
||||
room: "Room"
|
||||
# Prompts shown in an inactive slot's block (instead of zeros) while
|
||||
# a player hasn't pressed START yet or has run out of lives. Two
|
||||
# short lines so they fit in the side block (~106 px wide).
|
||||
join_prompt_can_join_l1: "PRESS START"
|
||||
join_prompt_can_join_l2: "TO PLAY"
|
||||
join_prompt_wait_l1: "PLEASE"
|
||||
join_prompt_wait_l2: "WAIT"
|
||||
|
||||
# Texts of the TITLE scene.
|
||||
title:
|
||||
press_start: "Press START to begin"
|
||||
|
||||
# Room transition texts (GameScene FSM intro/outro). Both INTRO and
|
||||
# OUTRO render as two independent lines with their own animation.
|
||||
@@ -48,6 +59,12 @@ game:
|
||||
room_intro_room: "Room {r}"
|
||||
room_complete_line1: "Stage"
|
||||
room_complete_line2: "Complete"
|
||||
# Game over labels (sub-FSM in GameScene). The countdown is drawn as
|
||||
# a numeric second line during CONTINUE_PROMPT phase 1, so only the
|
||||
# top label needs translation.
|
||||
continue_prompt: "Continue?"
|
||||
game_over_line1: "Game"
|
||||
game_over_line2: "Over"
|
||||
|
||||
menu:
|
||||
titles:
|
||||
@@ -57,6 +74,7 @@ menu:
|
||||
controls: "Controls"
|
||||
keyboard: "Keyboard"
|
||||
gamepad: "Gamepad"
|
||||
tweaks: "Gameplay"
|
||||
system: "System"
|
||||
labels:
|
||||
player_1: "Player 1"
|
||||
@@ -68,6 +86,7 @@ menu:
|
||||
controls_submenu: "Controls"
|
||||
keyboard_submenu: "Keyboard"
|
||||
gamepad_submenu: "Gamepad"
|
||||
tweaks_submenu: "Gameplay"
|
||||
system_submenu: "System"
|
||||
move_up: "Up"
|
||||
move_down: "Down"
|
||||
@@ -75,6 +94,7 @@ menu:
|
||||
move_right: "Right"
|
||||
jump: "Jump"
|
||||
attack: "Attack"
|
||||
start: "Start"
|
||||
service: "Options"
|
||||
gamepad_p1: "Gamepad P1"
|
||||
gamepad_p2: "Gamepad P2"
|
||||
@@ -95,6 +115,9 @@ menu:
|
||||
music_volume: "Music volume"
|
||||
sounds: "Sounds"
|
||||
sounds_volume: "Sound volume"
|
||||
whip_pierces_walls: "Whip pierces sarcs"
|
||||
friendly_fire: "Friendly fire"
|
||||
players_collide: "Player collision"
|
||||
restart: "Restart"
|
||||
exit_game: "Exit game"
|
||||
values:
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Configuració de la GameScene: timings i corbes de la FSM de sala
|
||||
# (ROOM_INTRO i ROOM_OUTRO). Tots els valors són ms i tota la geometria
|
||||
# i font del text es reutilitzen del tema UI (data/ui.yaml > pause +
|
||||
# notification). Si manca el fitxer o alguna clau, GameScene cau als
|
||||
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
|
||||
|
||||
room_intro:
|
||||
fade_in_ms: 500 # negre opac → món visible
|
||||
text_delay_ms: 500 # espera amb el món net abans de sortir el text
|
||||
text_enter_ms: 400 # text esquerra → centre
|
||||
text_hold_ms: 2000 # text quiet al centre
|
||||
text_exit_ms: 400 # text centre → fora per l'esquerra
|
||||
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
|
||||
# Delays d'spawn aeri **mesurats des de la fi de la música d'INTRO**
|
||||
# (callback `Audio::setOnMusicEnded`). 0 = cau al moment exacte que
|
||||
# `room_intro.ogg` acaba; valors > 0 escalonen els jugadors. En mode
|
||||
# single-player s'usa només `player_spawn_delay_ms`.
|
||||
player_spawn_delay_ms: 0
|
||||
player2_spawn_delay_ms: 300
|
||||
# Preset de reverb (catàleg a `data/sounds.yaml`) aplicat al primer
|
||||
# LAND de cada jugador durant la cinemàtica d'entrada. La sala dona
|
||||
# "més detall" sonor a l'INTRO; els salts del joc i els respawns
|
||||
# post-mort cauen al so sec. Buit o absent = sense reverb.
|
||||
land_reverb: stone_hall
|
||||
|
||||
room_outro:
|
||||
text_enter_ms: 400
|
||||
text_hold_ms: 2000
|
||||
text_exit_ms: 400
|
||||
fade_out_ms: 500 # món visible → negre opac
|
||||
line_gap_px: -4 # px extra entre les dues línies (fase / completada)
|
||||
black_hold_ms: 500 # pausa en negre abans d'entrar a la pròxima sala
|
||||
|
||||
text:
|
||||
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
|
||||
# `data/ui.yaml`). El text entra suau i surt accelerant.
|
||||
enter_curve: outCubic
|
||||
exit_curve: inCubic
|
||||
|
||||
# Panel translúcid pintat sota el rètol de transició (INTRO i OUTRO).
|
||||
# Va centrat verticalment al centre del playfield i ocupa tota
|
||||
# l'amplada del canvas. Creix d'altura amb la mateixa enter_curve del
|
||||
# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255);
|
||||
# `alpha` és la transparència del blend (0 = invisible, 255 = opac).
|
||||
text_panel:
|
||||
color: [36, 182, 170]
|
||||
alpha: 128
|
||||
height_px: 32
|
||||
@@ -0,0 +1,93 @@
|
||||
# Configuració de la GameScene: timings i corbes de la FSM de sala
|
||||
# (ROOM_INTRO i ROOM_OUTRO). Tots els valors són ms i tota la geometria
|
||||
# i font del text es reutilitzen del tema UI (data/config/ui.yaml > pause +
|
||||
# notification). Si manca el fitxer o alguna clau, GameScene cau als
|
||||
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
|
||||
|
||||
room_intro:
|
||||
fade_in_ms: 500 # negre opac → món visible
|
||||
text_delay_ms: 500 # espera amb el món net abans de sortir el text
|
||||
text_enter_ms: 400 # text esquerra → centre
|
||||
text_hold_ms: 2000 # text quiet al centre
|
||||
text_exit_ms: 400 # text centre → fora per l'esquerra
|
||||
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
|
||||
# Delays d'spawn aeri **mesurats des de la fi de la música d'INTRO**
|
||||
# (callback `Audio::setOnMusicEnded`). 0 = cau al moment exacte que
|
||||
# `room_intro.ogg` acaba; valors > 0 escalonen els jugadors. En mode
|
||||
# single-player s'usa només `player_spawn_delay_ms`.
|
||||
player_spawn_delay_ms: 0
|
||||
player2_spawn_delay_ms: 300
|
||||
# Preset de reverb (catàleg a `data/config/sounds.yaml`) aplicat al primer
|
||||
# LAND de cada jugador durant la cinemàtica d'entrada. La sala dona
|
||||
# "més detall" sonor a l'INTRO; els salts del joc i els respawns
|
||||
# post-mort cauen al so sec. Buit o absent = sense reverb.
|
||||
land_reverb: stone_hall
|
||||
|
||||
room_outro:
|
||||
text_enter_ms: 400
|
||||
text_hold_ms: 2000
|
||||
text_exit_ms: 400
|
||||
fade_out_ms: 500 # món visible → negre opac
|
||||
line_gap_px: -4 # px extra entre les dues línies (fase / completada)
|
||||
black_hold_ms: 500 # pausa en negre abans d'entrar a la pròxima sala
|
||||
|
||||
text:
|
||||
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
|
||||
# `data/config/ui.yaml`). El text entra suau i surt accelerant.
|
||||
enter_curve: outCubic
|
||||
exit_curve: inCubic
|
||||
|
||||
# Panel translúcid pintat sota el rètol de transició (INTRO i OUTRO).
|
||||
# Va centrat verticalment al centre del playfield i ocupa tota
|
||||
# l'amplada del canvas. Creix d'altura amb la mateixa enter_curve del
|
||||
# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255);
|
||||
# `alpha` és la transparència del blend (0 = invisible, 255 = opac).
|
||||
text_panel:
|
||||
color: [36, 182, 170]
|
||||
alpha: 128
|
||||
height_px: 32
|
||||
|
||||
# Sub-FSM de game over: dispara CONTINUE? quan tots els jugadors
|
||||
# queden fora de partida durant PLAYING, fa countdown, i si ningú
|
||||
# pulsa START acaba en GAME OVER + fade-to-black + transició a LOGO.
|
||||
# Tots els valors són ms (excepte `continue_count_steps`, que és el
|
||||
# nombre de tics visibles del countdown). Si manca el fitxer o alguna
|
||||
# clau, GameScene cau als fallbacks compilats (vegeu `GameOverFsm::Config`).
|
||||
game_over:
|
||||
# CONTINUE? — animació d'entrada i sortida idèntica al room text.
|
||||
continue_text_enter_ms: 400
|
||||
# Hold del countdown: 10 s = (continue_count_start + 1) graons d'1 s.
|
||||
# La sèrie visible és [continue_count_start, ..., 1, 0]; el "0" es
|
||||
# manté l'últim segon abans del salt a GAME OVER.
|
||||
continue_text_hold_ms: 10000
|
||||
continue_text_exit_ms: 400
|
||||
continue_count_start: 9
|
||||
# GAME OVER — apareix sense enter anim (panel ja amunt del CONTINUE?),
|
||||
# es manté un temps i marxa amb la mateixa exit anim del room text.
|
||||
game_over_text_hold_ms: 1500
|
||||
game_over_text_exit_ms: 400
|
||||
game_over_wait_ms: 400
|
||||
# Fade final cap a LOGO. `black_hold_ms` és la pausa en negre opac
|
||||
# abans que el GameScene demani la transició d'escena.
|
||||
fade_out_ms: 500
|
||||
black_hold_ms: 500
|
||||
# Velocitat (frequency ratio) de la música mentre dura el countdown
|
||||
# de CONTINUE_PROMPT. 1.0 = sense canvi, 1.5 = un 50% més ràpid (i
|
||||
# amb el to més agut: efecte arcade clàssic). Es restableix a 1.0 al
|
||||
# sortir del prompt — tant si el jugador reviu com si entra GAME OVER.
|
||||
continue_music_speed: 1.05
|
||||
# Pista de música que sona durant l'estat GAME_OVER (sense loop).
|
||||
# La fase de hold del text es sincronitza amb la durada real de la
|
||||
# pista, així que el rètol "Fi del Joc" es manté visible mentre dura
|
||||
# la música; després ve l'exit anim, el wait i el fade a LOGO.
|
||||
game_over_music_track: "game_over.ogg"
|
||||
# So que sona cada vegada que apareix un nou número del countdown
|
||||
# (al primer cop que es mostra `continue_count_start`, a cada canvi,
|
||||
# i quan apareix el 0 — però NO al saltar a GAME OVER). Buit = silenci.
|
||||
countdown_sound_key: "countdown.wav"
|
||||
# Llindar a partir del qual qualsevol botó de salt o atac de
|
||||
# qualsevol jugador avança al següent número del countdown.
|
||||
# 7 → skip habilitat mentre el número visible és 7..1 (no a 0).
|
||||
# -1 = mai. Útil per accelerar la decisió un cop la urgència ja
|
||||
# és evident al jugador.
|
||||
skip_below_or_equal: 7
|
||||
@@ -0,0 +1,24 @@
|
||||
# Configuració de la LogoScene (logotip de l'editora). El revelat del
|
||||
# logo a la fase SCAN imita un escaneig CRT: una banda lluminosa baixa
|
||||
# de dalt a baix; per damunt es veu la imatge ja "impressa", per sota
|
||||
# encara és negre. Al voltant de la banda hi ha glitch (offset
|
||||
# horitzontal de files amb falloff per distància) i ocasionalment
|
||||
# apareixen bursts VHS (franges desplaçades) damunt la part ja
|
||||
# revelada. Quan la banda toca el peu del logo, un flash blanc tanca
|
||||
# l'efecte i s'entra a MIDDLE (logo estable).
|
||||
logo:
|
||||
scan:
|
||||
duration_ms: 1500 # durada total del scan top→bottom
|
||||
band_height_px: 4 # alçada (px) de la banda lluminosa
|
||||
band_alpha: 200 # opacitat (0..255) de la capa blanca de la banda
|
||||
band_pulse_hz: 20.0 # freqüència del pulse de brillantor (Hz)
|
||||
band_pulse_depth: 0.45 # profunditat del pulse (0..1; 0 = sense pulse)
|
||||
# Glitch d'impacte (files al voltant de la banda)
|
||||
glitch_amplitude_px: 6 # offset horitzontal màxim a la banda
|
||||
glitch_band_radius_px: 12 # files damunt/sota la banda amb glitch (falloff lineal)
|
||||
# Bursts VHS sobre la part ja revelada
|
||||
vhs_burst_per_sec: 3 # bursts/segon mitjà
|
||||
vhs_burst_duration_ms: 80 # durada de cada burst
|
||||
vhs_burst_height_px: 8 # alçada de la franja afectada
|
||||
vhs_burst_amplitude_px: 8 # offset horitzontal del burst
|
||||
flash_ms: 100 # flash blanc final quan acaba el scan
|
||||
|
After Width: | Height: | Size: 494 B |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 99 B |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,77 @@
|
||||
# Configuració de la TitleScene.
|
||||
# `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal):
|
||||
# 1 = (36,0,0) gairebé negre amb un mínim de càlid; 31 = (255,109,0)
|
||||
# taronja pur. `duration_ms` és el timeout abans de saltar a la
|
||||
# següent escena (skipable amb tecla no reservada). Els dos
|
||||
# `blink_*_ms` defineixen el cicle del prompt "press start": el text
|
||||
# es veu durant `visible_ms` i s'amaga durant `hidden_ms` repetint-se.
|
||||
title:
|
||||
duration_ms: 30000
|
||||
bg_color: 1
|
||||
text_color: 31
|
||||
# Color del text del © de copyright (peu de la TITLE). Off-white càlid:
|
||||
# índex 183 = (255,218,170), tipus crema/peach, ~90% blanc amb tint càlid.
|
||||
copyright_color: 183
|
||||
# Color de l'outline del prompt "PRESS START". Marró fosc càlid (índex
|
||||
# 19 = 109,72,0), coherent amb la paleta del logo. Es manté separat de
|
||||
# `copyright_color` perquè l'outline i el text del © tenen rols visuals
|
||||
# diferents (contrast vs. peu informatiu).
|
||||
press_start_outline_color: 19
|
||||
# Posicions Y dels tres elements verticals de la TITLE, en percentatge
|
||||
# sobre l'alçada del canvas (240 px). El logo s'ancora pel seu top
|
||||
# (la imatge cau cap avall des d'aquest Y); el prompt i el copyright
|
||||
# es centren verticalment sobre la línia.
|
||||
logo_y_percent: 15
|
||||
prompt_y_percent: 68
|
||||
copyright_y_percent: 92
|
||||
# Separació vertical en píxels entre el minilogo i el text del © al peu.
|
||||
mini_logo_gap_px: 2
|
||||
blink_visible_ms: 800
|
||||
blink_hidden_ms: 200
|
||||
# Fase STARTING (entre la primera pulsació de START i el salt a la
|
||||
# següent escena). `starting_ms` és la durada fixa; durant aquesta
|
||||
# fase el parpadeig accelera per `starting_blink_speedup` (2.0 = doble
|
||||
# de freqüència). Si l'altre jugador prem START dins d'aquesta
|
||||
# finestra, el comptador es reseteja i s'arrenca amb BOTH.
|
||||
starting_ms: 3000
|
||||
# Durant STARTING el prompt commuta a un cicle simètric 50/50
|
||||
# (visible/ocult) amb aquest període total. 200 ms = 5 Hz, prou
|
||||
# ràpid per llegir-se com a "pulsació confirmada".
|
||||
starting_blink_period_ms: 200
|
||||
# Fade-out estil "mosaic dissolve" durant l'última part de STARTING.
|
||||
# `fade_out_ms` ha de ser <= starting_ms (els últims X ms del STARTING
|
||||
# es fan amb el dissolve actiu fins a quedar negre). `fade_out_max_block`
|
||||
# és la mida màxima de bloc en píxels al final del fade.
|
||||
fade_out_ms: 800
|
||||
fade_out_max_block: 32
|
||||
# Fade-out de `title.ogg` en entrar a STARTING. Independent del visual:
|
||||
# la música pot acabar abans o després del mosaic dissolve.
|
||||
music_fade_out_ms: 3000
|
||||
# Fade-to-black per a la sortida d'attract (timeout cap a LOGO). NO és
|
||||
# mosaic — capa negra lineal damunt del canvas. La música no es toca:
|
||||
# `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa.
|
||||
attract_fade_to_black_ms: 800
|
||||
# Seqüència d'aparició dels elements de la TITLE en fase IDLE. Tots
|
||||
# els temps són absoluts des de l'entrada a l'escena. `fade_in_ms` és
|
||||
# la capa negra inicial que s'esvaeix cap al fons; els altres marquen
|
||||
# quan apareix cada element. Si l'usuari prem START en qualsevol
|
||||
# moment, la seqüència es completa instantàniament.
|
||||
intro:
|
||||
fade_in_ms: 500 # fade-in negre inicial sobre el fons
|
||||
game_logo_at_ms: 1500 # visibilitat binària del game_logo
|
||||
mini_logo_at_ms: 2000 # inici del slide-in del mini-logo
|
||||
mini_logo_slide_ms: 500 # durada del slide
|
||||
copyright_at_ms: 2200 # inici del slide-in del copyright
|
||||
copyright_slide_ms: 500 # durada del slide
|
||||
slide_curve: outCubic # corba compartida per mini_logo i copyright
|
||||
press_start_at_ms: 3000 # quan apareix PRESS START amb parpadeig
|
||||
# Distorsió sinusoidal del fons (efecte de calor del desert).
|
||||
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
|
||||
# `wavelength_px`: distància vertical entre dos pics de l'ona.
|
||||
# `speed_hz`: velocitat amb què avança la fase (cicles per segon).
|
||||
# Si `enabled` és false, el fons es pinta sense distorsió.
|
||||
heat_haze:
|
||||
enabled: true
|
||||
amplitude_px: 1.5
|
||||
wavelength_px: 32.0
|
||||
speed_hz: 0.6
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -22,8 +22,8 @@ real (logo animat, menú de títol, cutscene).
|
||||
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`
|
||||
- Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_arcade/debug.yaml`
|
||||
- Per defecte: `~/.config/jailgames/aee_arcade/debug.yaml`
|
||||
|
||||
### Comportament segons build
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sistema d'entitats
|
||||
|
||||
Registre del disseny, els treballs fets i els pendents per al sistema d'entitats (sarcòfags, porta, momia, antorxa, trampes, roca) d'aee_2026. Material de referència per a decisions futures — no és documentació d'API.
|
||||
Registre del disseny, els treballs fets i els pendents per al sistema d'entitats (sarcòfags, porta, momia, antorxa, trampes, roca) d'aee_arcade. Material de referència per a decisions futures — no és documentació d'API.
|
||||
|
||||
Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
|
||||
|
||||
@@ -24,7 +24,7 @@ Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
|
||||
|
||||
## 1. Context i objectiu inicial
|
||||
|
||||
aee_2026 és un clon tipus Oh Mummy! amb estètica Amidar. Els sarcòfags eren fins ara rectangles purament geomètrics calculats a [Map::getSarcRect](../source/game/map/map.cpp#L67); no hi havia entitats de joc (momies, antorxes, trampes, portes).
|
||||
aee_arcade és un clon tipus Oh Mummy! amb estètica Amidar. Els sarcòfags eren fins ara rectangles purament geomètrics calculats a [Map::getSarcRect](../source/game/map/map.cpp#L67); no hi havia entitats de joc (momies, antorxes, trampes, portes).
|
||||
|
||||
**Objectiu inicial** (prompt de partida):
|
||||
|
||||
@@ -158,7 +158,7 @@ Al CMakeLists: `ENTITY_SOURCES` conté els fitxers core; `APP_SOURCES` afegeix `
|
||||
- `make build`: OK, 0 warnings nous.
|
||||
- `./build/test_entities_smoke`: **9/9 OK**.
|
||||
- `ctest --output-on-failure`: 1/1 suite passed.
|
||||
- `timeout 5 xvfb-run ./build/aee_2026`: 5s sense crash, stdout inclou:
|
||||
- `timeout 5 xvfb-run ./build/aee_arcade`: 5s sense crash, stdout inclou:
|
||||
```
|
||||
Surface : torch.gif ... LOADED
|
||||
Surface : sarcophagus.gif ... LOADED (×2)
|
||||
@@ -323,7 +323,7 @@ Dos events encadenats perquè el *pintat* no és la *presència*: es filtra per
|
||||
|
||||
- `make debug` net, sense warnings ni de clang-tidy ni de cppcheck als fitxers nous.
|
||||
- `./build/test_entities_smoke`: **ALL OK** (14/14). T9 adaptat (publica `tile_painted` en lloc d'`on_tile_entered`); la resta de tests del sistema d'entitats passen sense tocar.
|
||||
- `timeout 5 xvfb-run ./build/aee_2026`: arrenca, log `PaintLayer: 21x15 tiles (336x240 px), tileset data/tilesets/tomb.gif` després de `EntityManager: 27 entitats carregades`, surt amb `Bye!` sense crash.
|
||||
- `timeout 5 xvfb-run ./build/aee_arcade`: arrenca, log `PaintLayer: 21x15 tiles (336x240 px), tileset data/tilesets/tomb.gif` després de `EntityManager: 27 entitats carregades`, surt amb `Bye!` sense crash.
|
||||
|
||||
### Pendent derivat
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Pla d'implementació — Menú de servei
|
||||
|
||||
Pla per a portar el menú de servei de `../aee` (`source/core/rendering/menu.cpp`)
|
||||
a `aee_2026`, adaptat a l'arquitectura actual (Scenes + Director instanciat +
|
||||
a `aee_arcade`, adaptat a l'arquitectura actual (Scenes + Director instanciat +
|
||||
`Options` amb setters persistents + `Screen` ARGB + `Text` bitmap). Document de
|
||||
referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi
|
||||
encara.
|
||||
@@ -33,7 +33,7 @@ Tot el que segueix és la base; la còpia mental serveix per a no reinventar res
|
||||
|
||||
## 2. Diferències d'arquitectura rellevants
|
||||
|
||||
| Punt | `../aee` | `aee_2026` |
|
||||
| Punt | `../aee` | `aee_arcade` |
|
||||
| ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| Overlay | `Overlay::render()` dibuixa Menú + Notifier al buffer | No hi ha Overlay. Cada Scene crida `Notifier::render()` i `RenderInfo::render()` |
|
||||
| Director | Singleton `Director::get()` amb `requestQuit/requestRestart` | Classe instanciada; sortida via `SDL_EVENT_QUIT` a `EventBuffer` |
|
||||
@@ -223,7 +223,7 @@ es necessita.
|
||||
### Limitació: pàgina Controls diferida
|
||||
|
||||
El menú de `../aee` té una pàgina Controls (`buildControls`) que permet
|
||||
remappar `up/down/left/right` i la tecla de menú. A `aee_2026`:
|
||||
remappar `up/down/left/right` i la tecla de menú. A `aee_arcade`:
|
||||
|
||||
- Les tecles de joc són `Defaults::KeysGame::{UP,DOWN,LEFT,RIGHT}` — constants
|
||||
`constexpr`, no mutables.
|
||||
@@ -273,7 +273,7 @@ y, w, h, argb, alpha)` que opere sobre `canvas_.data()`. El canvas és
|
||||
fons del menú pot ser `alpha=220` i es veu el joc apagat per darrere.
|
||||
|
||||
Mida fixa recomanada per al MVP: `BOX_W = 220`, altura dinàmica (igual que
|
||||
`../aee`). Canvas lògic d'`aee_2026` és `GameCanvas::WIDTH`/`HEIGHT` —
|
||||
`../aee`). Canvas lògic d'`aee_arcade` és `GameCanvas::WIDTH`/`HEIGHT` —
|
||||
confirmar mida real (mateixos 320×200?) a `utils/defines.hpp` abans de
|
||||
hardcoded.
|
||||
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Aventures en Egipte ARCADE</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>aee_2026</string>
|
||||
<string>aee_arcade</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icon</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>icon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.jailgames.aee_2026</string>
|
||||
<string>org.jailgames.aee_arcade</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>aee_2026</string>
|
||||
<string>aee_arcade</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// aee_2026.rc
|
||||
// aee_arcade.rc
|
||||
IDI_ICON1 ICON "release/icons/icon.ico"
|
||||
@@ -113,6 +113,13 @@ void Audio::stopMusic() {
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-make-member-function-const)
|
||||
void Audio::setMusicSpeed(float ratio) {
|
||||
if (music_enabled_) {
|
||||
engine_->setMusicSpeed(ratio);
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so per nom
|
||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
||||
void Audio::playSound(const std::string& name, Group group) {
|
||||
@@ -194,6 +201,14 @@ void Audio::setOnMusicEnded(std::function<void()> callback) {
|
||||
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
|
||||
}
|
||||
|
||||
// Resol el nom contra el cache de recursos i retorna la durada pre-calculada
|
||||
// al `loadMusic`. 0 si la pista no existeix — així el caller pot decidir
|
||||
// fallback (p. ex. usar un timeout fix) sense haver de propagar errors.
|
||||
auto Audio::getMusicDurationMs(const std::string& name) -> int {
|
||||
auto* music = AudioResource::getMusic(name);
|
||||
return (music != nullptr) ? music->duration_ms : 0;
|
||||
}
|
||||
|
||||
// Consulta directament l'estat a Ja i el projecta al subconjunt d'estats que
|
||||
// exposa Audio (INVALID/DISABLED de Ja col·lapsen a STOPPED — la capa d'usuari
|
||||
// només vol saber si està sonant, pausat o parat).
|
||||
|
||||
@@ -76,11 +76,16 @@ class Audio {
|
||||
void stopMusic(); // Aturar completament la música
|
||||
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
|
||||
void setOnMusicEnded(std::function<void()> callback); // Callback disparat quan la pista actual acaba de drenar (CONV-03)
|
||||
// Multiplicador de velocitat de la música actual. 1.0 = normal,
|
||||
// 1.5 = un 50% més ràpid (efecte "chipmunk" — també puja el to).
|
||||
// Es reseteja a 1.0 implícitament a cada `playMusic`. No-op si no
|
||||
// hi ha música activa.
|
||||
void setMusicSpeed(float ratio);
|
||||
|
||||
// --- Control de sons ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
||||
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
||||
// Reprodueix un so processat per un efecte definit a data/sounds.yaml
|
||||
// Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
|
||||
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
||||
// sec — l'usuari sent el so igualment, sense la cua.
|
||||
@@ -121,6 +126,11 @@ class Audio {
|
||||
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
||||
[[nodiscard]] static auto getMusicState() -> MusicState; // Estat real consultat a Ja::
|
||||
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
||||
// Durada de la pista resolta per nom (mil·lisegons). 0 si la pista no
|
||||
// existeix al cache de recursos o si el seu header OGG no permet
|
||||
// calcular-la. Pensat per a clients que necessiten un timeline
|
||||
// determinista (p. ex. RoomFsm) sense dependre de callbacks de fi.
|
||||
[[nodiscard]] static auto getMusicDurationMs(const std::string& name) -> int;
|
||||
|
||||
private:
|
||||
// --- Tipus anidats ---
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/jail_audio.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace AudioEffects {
|
||||
|
||||
@@ -46,11 +46,11 @@ namespace AudioEffects {
|
||||
} else if (spec.format == SDL_AUDIO_U8) {
|
||||
bytes_per_sample = 1;
|
||||
} else {
|
||||
std::cerr << "AudioEffects: format de so no suportat (només U8 o S16)\n";
|
||||
Log::error("AudioEffects", "format de so no suportat (només U8 o S16)");
|
||||
return false;
|
||||
}
|
||||
if (spec.channels < 1 || spec.channels > 2) {
|
||||
std::cerr << "AudioEffects: el so ha de ser mono o estèreo\n";
|
||||
Log::error("AudioEffects", "el so ha de ser mono o estèreo");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -127,11 +128,44 @@ namespace Ja {
|
||||
void Engine::updateOutgoingFade() {
|
||||
if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }
|
||||
|
||||
// Mentre la fosa està activa, mantenim el stream amb una reserva
|
||||
// de samples per davant del cursor (mateix patró que pumpMusic
|
||||
// per al current_music_). Així el stream no es buida ni quan SDL
|
||||
// drena més ràpid del previst en haver sounds bound a la mateixa
|
||||
// device. Si l'OGG arriba a EOF, rebobina (la fosa pot ser més
|
||||
// llarga que la pista).
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
const Music& music = *outgoing_music_.music;
|
||||
const int BYTES_PER_SECOND = music.spec.freq * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
const int LOW_WATER = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||
while (SDL_GetAudioStreamAvailable(outgoing_music_.stream) < LOW_WATER) {
|
||||
short chunk[MUSIC_CHUNK_SHORTS];
|
||||
const int SAMPLES = stb_vorbis_get_samples_short_interleaved(
|
||||
music.vorbis,
|
||||
music.spec.channels,
|
||||
static_cast<short*>(chunk),
|
||||
MUSIC_CHUNK_SHORTS);
|
||||
if (SAMPLES <= 0) {
|
||||
stb_vorbis_seek_start(music.vorbis);
|
||||
continue;
|
||||
}
|
||||
const int BYTES = SAMPLES * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(outgoing_music_.stream, static_cast<const void*>(chunk), BYTES);
|
||||
}
|
||||
}
|
||||
|
||||
const float PROGRESS = fadeProgress(outgoing_music_.fade);
|
||||
if (PROGRESS >= 1.0F) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
// Deixem el Vorbis del Music original en un estat conegut per
|
||||
// a la pròxima reproducció. (playMusic també fa seek_start,
|
||||
// però fer-ho ací evita estats intermedis si algú consulta.)
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
|
||||
}
|
||||
outgoing_music_.music = nullptr;
|
||||
} else {
|
||||
SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS));
|
||||
}
|
||||
@@ -191,7 +225,14 @@ namespace Ja {
|
||||
}
|
||||
|
||||
preFillOutgoing(current_music_, duration_ms);
|
||||
|
||||
outgoing_music_.stream = current_music_->stream;
|
||||
// Guardem la referència al Music perquè updateOutgoingFade puga
|
||||
// seguir bombant Vorbis cap al stream durant tota la fosa. NO fem
|
||||
// seek_start ací: la decompressió ha de continuar des d'on estava
|
||||
// perquè el so siga continu. El seek_start es farà quan la fosa
|
||||
// acabe (o quan playMusic la interrompi via stopMusic).
|
||||
outgoing_music_.music = current_music_;
|
||||
outgoing_music_.fade = {
|
||||
.active = true,
|
||||
.start_time = SDL_GetTicks(),
|
||||
@@ -200,7 +241,6 @@ namespace Ja {
|
||||
};
|
||||
current_music_->stream = nullptr;
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
if (current_music_->vorbis != nullptr) { stb_vorbis_seek_start(current_music_->vorbis); }
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
@@ -246,6 +286,11 @@ namespace Ja {
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::setMusicSpeed(float ratio) {
|
||||
if (current_music_ == nullptr || current_music_->stream == nullptr) { return; }
|
||||
SDL_SetAudioStreamFrequencyRatio(current_music_->stream, ratio);
|
||||
}
|
||||
|
||||
void Engine::pauseMusic() {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||
|
||||
@@ -265,6 +310,10 @@ namespace Ja {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
|
||||
}
|
||||
outgoing_music_.music = nullptr;
|
||||
}
|
||||
incoming_fade_.active = false;
|
||||
|
||||
@@ -521,6 +570,15 @@ namespace Ja {
|
||||
music->spec.channels = static_cast<int>(INFO.channels);
|
||||
music->spec.freq = static_cast<int>(INFO.sample_rate);
|
||||
music->spec.format = SDL_AUDIO_S16;
|
||||
// Pre-càlcul de la durada en ms a partir del header. stb_vorbis ja
|
||||
// ha decodificat la informació necessària a `stb_vorbis_open_memory`;
|
||||
// aquesta consulta no descodifica àudio, només llig el comptador
|
||||
// de samples. Si el sample_rate fos 0 (header malmès) deixem
|
||||
// duration_ms a 0.
|
||||
if (INFO.sample_rate > 0) {
|
||||
const auto SAMPLES = stb_vorbis_stream_length_in_samples(music->vorbis);
|
||||
music->duration_ms = static_cast<int>((static_cast<std::uint64_t>(SAMPLES) * 1000ULL) / INFO.sample_rate);
|
||||
}
|
||||
music->state = MusicState::STOPPED;
|
||||
|
||||
return music.release();
|
||||
|
||||
@@ -112,6 +112,12 @@ namespace Ja {
|
||||
std::string filename;
|
||||
|
||||
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
|
||||
// Durada total de la pista en mil·lisegons, mesurada via
|
||||
// `stb_vorbis_stream_length_in_samples / sample_rate` al
|
||||
// `loadMusic`. 0 si el càlcul no és possible (header malmès).
|
||||
// L'usen consumidors que necessiten un timeline pre-calculat —
|
||||
// p. ex. la FSM de sala — sense dependre de callbacks de fi.
|
||||
int duration_ms{0};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
MusicState state{MusicState::INVALID};
|
||||
};
|
||||
@@ -125,6 +131,13 @@ namespace Ja {
|
||||
|
||||
struct OutgoingMusic {
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
// Referència al Music original perquè updateOutgoingFade puga
|
||||
// continuar descomprimint des de Vorbis cap al stream durant
|
||||
// tota la fosa. Sense això, només tenim el pre-fill puntual i
|
||||
// SDL drena el stream més ràpid del previst quan hi ha sounds
|
||||
// bound a la mateixa device (~2x), buidant-lo a meitat del
|
||||
// fade i sentint-se com un tall sec.
|
||||
Music* music{nullptr};
|
||||
FadeState fade;
|
||||
};
|
||||
|
||||
@@ -158,6 +171,14 @@ namespace Ja {
|
||||
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
||||
[[nodiscard]] auto getMusicState() const -> MusicState;
|
||||
auto setMusicVolume(float volume) -> float;
|
||||
// Multiplicador de velocitat de reproducció de la música actual
|
||||
// via `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal, 2.0 =
|
||||
// doble velocitat. Cal saber que també puja el to (efecte
|
||||
// "chipmunk") — és el comportament arcade clàssic dels comptes
|
||||
// enrere. Cada `playMusic` crea un stream nou amb ratio 1.0,
|
||||
// així que un canvi de track reseteja la velocitat
|
||||
// implícitament. No-op si no hi ha música activa.
|
||||
void setMusicSpeed(float ratio);
|
||||
// Registra un callback que es disparà quan la música actual acabi de
|
||||
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
|
||||
// stopMusic, així que el callback pot invocar playMusic sense córrer.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "core/audio/sound_effects_config.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -27,7 +27,7 @@ auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
|
||||
void SoundEffectsConfig::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "SoundEffectsConfig: no s'ha pogut obrir " << file_path << " — cap preset d'efecte disponible\n";
|
||||
Log::warn("SoundEffectsConfig", "no s'ha pogut obrir {} — cap preset d'efecte disponible", file_path);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,9 +60,9 @@ void SoundEffectsConfig::load(const std::string& file_path) {
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "SoundEffectsConfig: " << echoes_.size() << " preset(s) d'eco i " << reverbs_.size() << " de reverb des de " << file_path << '\n';
|
||||
Log::info("SoundEffectsConfig", "{} preset(s) d'eco i {} de reverb des de {}", echoes_.size(), reverbs_.size(), file_path);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "SoundEffectsConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||
Log::error("SoundEffectsConfig", "error parsejant {}: {}", file_path, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Per a Ja::EchoParams / Ja::ReverbParams
|
||||
|
||||
// Catàleg de presets d'efectes carregat des de data/sounds.yaml. La capa
|
||||
// Catàleg de presets d'efectes carregat des de data/config/sounds.yaml. La capa
|
||||
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
|
||||
// preset no existeix, el so es reprodueix sec amb un avís a stderr.
|
||||
//
|
||||
|
||||
@@ -62,7 +62,7 @@ void DefineButtons::disable() {
|
||||
}
|
||||
|
||||
void DefineButtons::resetSequence() {
|
||||
// Ordre: ATTACK, JUMP, SERVICE — replica l'estructura de
|
||||
// Ordre: ATTACK, JUMP, START, SERVICE — replica l'estructura de
|
||||
// `clearButtons()` de coffee_crisis_arcade_edition (seqüència ordenada
|
||||
// d'accions amb la SERVICE al final) adaptada al joc actual. La label
|
||||
// de SERVICE va a una clau pròpia (`menu.items.service`) per no
|
||||
@@ -70,6 +70,7 @@ void DefineButtons::resetSequence() {
|
||||
sequence_.clear();
|
||||
sequence_.push_back({"menu.items.attack", GamepadBindings::Action::ATTACK, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
sequence_.push_back({"menu.items.jump", GamepadBindings::Action::JUMP, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
sequence_.push_back({"menu.items.start", GamepadBindings::Action::START, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
sequence_.push_back({"menu.items.service", GamepadBindings::Action::SERVICE, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
index_ = 0;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace {
|
||||
ActionPair{GamepadBindings::Action::JUMP, std::string_view{"jump"}},
|
||||
ActionPair{GamepadBindings::Action::ATTACK, std::string_view{"attack"}},
|
||||
ActionPair{GamepadBindings::Action::SERVICE, std::string_view{"service"}},
|
||||
ActionPair{GamepadBindings::Action::START, std::string_view{"start"}},
|
||||
};
|
||||
|
||||
using ButtonPair = std::pair<int, std::string_view>;
|
||||
@@ -89,7 +90,7 @@ auto GamepadBindings::get() -> GamepadBindings& {
|
||||
}
|
||||
|
||||
auto GamepadBindings::defaultButton(Action action) -> int {
|
||||
// Defaults històrics de l'aee_2026 (vegeu Player::accumulateGamepadMovement
|
||||
// Defaults històrics de l'aee_arcade (vegeu Player::accumulateGamepadMovement
|
||||
// i les consultes del Director per al menú).
|
||||
switch (action) {
|
||||
case Action::JUMP:
|
||||
@@ -98,6 +99,8 @@ auto GamepadBindings::defaultButton(Action action) -> int {
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_WEST);
|
||||
case Action::SERVICE:
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_BACK);
|
||||
case Action::START:
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_START);
|
||||
}
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID);
|
||||
}
|
||||
@@ -164,8 +167,7 @@ void GamepadBindings::load(const std::string& file_path) {
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(CONTENT);
|
||||
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
|
||||
std::cerr << "GamepadBindings: secció 'gamepads' absent o invàlida a "
|
||||
<< file_path << '\n';
|
||||
Log::error("GamepadBindings", "secció 'gamepads' absent o invàlida a {}", file_path);
|
||||
return;
|
||||
}
|
||||
for (const auto& entry : yaml["gamepads"]) {
|
||||
@@ -185,11 +187,9 @@ void GamepadBindings::load(const std::string& file_path) {
|
||||
}
|
||||
configs_.push_back(std::move(cfg));
|
||||
}
|
||||
std::cout << "GamepadBindings: " << configs_.size() << " mandos carregats de "
|
||||
<< file_path << '\n';
|
||||
Log::info("GamepadBindings", "{} mandos carregats de {}", configs_.size(), file_path);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "GamepadBindings: error parsejant " << file_path << ": "
|
||||
<< e.what() << '\n';
|
||||
Log::error("GamepadBindings", "error parsejant {}: {}", file_path, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ auto GamepadBindings::save() -> bool {
|
||||
if (file_path_.empty()) { return false; }
|
||||
|
||||
std::ostringstream out;
|
||||
out << "# AEE 2026 - Vinculacions de mando per dispositiu.\n";
|
||||
out << "# AEE ARCADE - Vinculacions de mando per dispositiu.\n";
|
||||
out << "# Auto-generat des del menú de servei. Editable a mà.\n";
|
||||
out << "gamepads:\n";
|
||||
for (const auto& cfg : configs_) {
|
||||
@@ -212,7 +212,7 @@ auto GamepadBindings::save() -> bool {
|
||||
|
||||
std::ofstream file(file_path_, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "GamepadBindings: no es pot escriure " << file_path_ << '\n';
|
||||
Log::error("GamepadBindings", "no es pot escriure {}", file_path_);
|
||||
return false;
|
||||
}
|
||||
file << out.str();
|
||||
|
||||
@@ -25,6 +25,7 @@ class GamepadBindings {
|
||||
JUMP,
|
||||
ATTACK,
|
||||
SERVICE,
|
||||
START,
|
||||
};
|
||||
|
||||
// Triggers L2/R2 representats com a "botons virtuals" — replica la
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/notification_presets.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -94,8 +94,7 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
|
||||
if (!SDL_IsGamepad(jid)) { return; }
|
||||
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
|
||||
if (pad == nullptr) {
|
||||
std::cout << "GamepadManager: SDL_OpenGamepad(" << jid
|
||||
<< ") ha fallat: " << SDL_GetError() << '\n';
|
||||
Log::error("GamepadManager", "SDL_OpenGamepad({}) ha fallat: {}", jid, SDL_GetError());
|
||||
return;
|
||||
}
|
||||
std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad)));
|
||||
@@ -104,8 +103,7 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
|
||||
// reconnexió es resoldrà per nom.
|
||||
const char* raw_path = SDL_GetGamepadPath(pad);
|
||||
std::string path = (raw_path != nullptr) ? raw_path : "";
|
||||
std::cout << "GamepadManager: connectat " << name
|
||||
<< (path.empty() ? "" : " [" + path + "]") << '\n';
|
||||
Log::info("GamepadManager", "connectat {}{}", name, path.empty() ? std::string{} : " [" + path + "]");
|
||||
entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name, .path = path});
|
||||
|
||||
// Hot-plug: si alguna assignació pendent encaixa amb aquest mando,
|
||||
@@ -129,7 +127,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
|
||||
std::string name = std::move(it->pretty_name);
|
||||
SDL_Gamepad* pad = it->pad;
|
||||
if (pad != nullptr) { SDL_CloseGamepad(pad); }
|
||||
std::cout << "GamepadManager: desconnectat " << name << '\n';
|
||||
Log::info("GamepadManager", "desconnectat {}", name);
|
||||
entries_.erase(it);
|
||||
|
||||
// Qualsevol assignació que apuntava a aquest mando queda amb `pad`
|
||||
@@ -145,8 +143,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
|
||||
void GamepadManager::init() {
|
||||
if (initialized_) { return; }
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
std::cout << "GamepadManager: SDL_INIT_GAMEPAD ha fallat: "
|
||||
<< SDL_GetError() << '\n';
|
||||
Log::error("GamepadManager", "SDL_INIT_GAMEPAD ha fallat: {}", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
// `gamecontrollerdb.txt` és opcional. Viu a l'arrel del projecte
|
||||
@@ -161,10 +158,9 @@ void GamepadManager::init() {
|
||||
}
|
||||
const int ADDED = SDL_AddGamepadMappingsFromFile(db_path.c_str());
|
||||
if (ADDED < 0) {
|
||||
std::cout << "GamepadManager: no s'ha pogut carregar " << db_path
|
||||
<< ": " << SDL_GetError() << '\n';
|
||||
Log::warn("GamepadManager", "no s'ha pogut carregar {}: {}", db_path, SDL_GetError());
|
||||
} else {
|
||||
std::cout << "GamepadManager: " << ADDED << " mappings carregats de " << db_path << '\n';
|
||||
Log::info("GamepadManager", "{} mappings carregats de {}", ADDED, db_path);
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
@@ -175,7 +171,7 @@ void GamepadManager::init() {
|
||||
}
|
||||
SDL_free(ids);
|
||||
}
|
||||
std::cout << "GamepadManager: " << entries_.size() << " mando(s) detectat(s) a l'inici\n";
|
||||
Log::info("GamepadManager", "{} mando(s) detectat(s) a l'inici", entries_.size());
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
@@ -268,8 +264,7 @@ void GamepadManager::assignToPlayer(int player_id, const std::string& name, cons
|
||||
if (a.pad != nullptr) {
|
||||
for (auto& other : assignments_) {
|
||||
if (&other != &a && other.pad == a.pad) {
|
||||
std::cout << "GamepadManager: mando " << a.name
|
||||
<< " reassignat (last-wins)\n";
|
||||
Log::info("GamepadManager", "mando {} reassignat (last-wins)", a.name);
|
||||
other.pad = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -277,11 +272,9 @@ void GamepadManager::assignToPlayer(int player_id, const std::string& name, cons
|
||||
// Traç de l'assignació persistida (vinculada o pendent) perquè quede
|
||||
// visible a la consola igual que les autoassignacions.
|
||||
if (a.pad != nullptr) {
|
||||
std::cout << "GamepadManager: assignat " << a.name
|
||||
<< " → P" << (player_id + 1) << '\n';
|
||||
Log::info("GamepadManager", "assignat {} → P{}", a.name, player_id + 1);
|
||||
} else {
|
||||
std::cout << "GamepadManager: P" << (player_id + 1) << " pendent ("
|
||||
<< (a.name.empty() ? a.path : a.name) << " no connectat)\n";
|
||||
Log::info("GamepadManager", "P{} pendent ({} no connectat)", player_id + 1, a.name.empty() ? a.path : a.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,8 +300,7 @@ auto GamepadManager::autoAssignFreeSlots() -> std::vector<AutoAssigned> {
|
||||
slot.name = e.pretty_name;
|
||||
slot.path = e.path;
|
||||
assigned.push_back(AutoAssigned{.player_id = pid, .name = e.pretty_name, .path = e.path});
|
||||
std::cout << "GamepadManager: auto-assigned " << e.pretty_name
|
||||
<< " → P" << (pid + 1) << '\n';
|
||||
Log::info("GamepadManager", "auto-assigned {} → P{}", e.pretty_name, pid + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace GlobalInputs {
|
||||
// La lliguem a la durada de la notificació EXIT perquè el missatge i
|
||||
// la confirmació expiren alhora — si no, l'usuari podria veure el
|
||||
// text però ja no ser dins del rang de confirmació (o al revés).
|
||||
// El valor concret viu a data/ui.yaml (timings.exit_ms) via UiConfig.
|
||||
// El valor concret viu a data/config/ui.yaml (timings.exit_ms) via UiConfig.
|
||||
constexpr int MIN_ZOOM = 1;
|
||||
constexpr int MAX_ZOOM = 6;
|
||||
|
||||
@@ -161,7 +161,7 @@ namespace GlobalInputs {
|
||||
// Debug helper: dispara una notificació prou llarga perquè
|
||||
// `wrapToWidth` la parteixi en (almenys) dues línies. Útil per a
|
||||
// verificar visualment el padding asimètric, el line_gap i la caixa
|
||||
// multilinia després de tocar la geometria a `data/ui.yaml`.
|
||||
// multilinia després de tocar la geometria a `data/config/ui.yaml`.
|
||||
void onDebugLongNotification() {
|
||||
Notification::notify("Notificació de prova llarga per a comprovar que el text es parteix correctament en dues línies dins la caixa.");
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "core/input/key_config.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
auto KeyConfig::get() -> KeyConfig& {
|
||||
static KeyConfig instance_;
|
||||
@@ -15,7 +15,7 @@ auto KeyConfig::get() -> KeyConfig& {
|
||||
void KeyConfig::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "KeyConfig: no s'ha pogut obrir " << file_path << '\n';
|
||||
Log::error("KeyConfig", "no s'ha pogut obrir {}", file_path);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ void KeyConfig::load(const std::string& file_path) {
|
||||
const auto* end = begin + bytes.size();
|
||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
|
||||
std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n';
|
||||
Log::error("KeyConfig", "falta la secció 'keys' a {}", file_path);
|
||||
return;
|
||||
}
|
||||
bindings_.clear();
|
||||
@@ -34,14 +34,14 @@ void KeyConfig::load(const std::string& file_path) {
|
||||
const auto CODE = entry["code"].get_value<std::string>();
|
||||
const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str());
|
||||
if (SC == SDL_SCANCODE_UNKNOWN) {
|
||||
std::cerr << "KeyConfig: codi SDL desconegut '" << CODE << "' per id '" << ID << "'\n";
|
||||
Log::warn("KeyConfig", "codi SDL desconegut '{}' per id '{}'", CODE, ID);
|
||||
continue;
|
||||
}
|
||||
bindings_[ID] = SC;
|
||||
}
|
||||
std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << file_path << '\n';
|
||||
Log::info("KeyConfig", "{} bindings des de {}", bindings_.size(), file_path);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "KeyConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||
Log::error("KeyConfig", "error parsejant {}: {}", file_path, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
// Mapeig id → SDL_Scancode carregat des de data/input/keys.yaml. Consumit
|
||||
// Mapeig id → SDL_Scancode carregat des de data/config/input.yaml. Consumit
|
||||
// per GlobalInputs i (en el futur) per menús de remapat. No té cap cost
|
||||
// global: si la clau no existeix, retorna SDL_SCANCODE_UNKNOWN i GlobalInputs
|
||||
// ignora l'acció. D'aquesta manera podem afegir nous bindings al YAML sense
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "core/locale/locale.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -36,7 +36,7 @@ auto Locale::get() -> Locale& {
|
||||
void Locale::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "Locale: no s'ha pogut obrir " << file_path << '\n';
|
||||
Log::error("Locale", "no s'ha pogut obrir {}", file_path);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,15 +46,15 @@ void Locale::load(const std::string& file_path) {
|
||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||
strings_.clear();
|
||||
flatten(yaml, "", strings_);
|
||||
std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n';
|
||||
Log::info("Locale", "{} traduccions des de {}", strings_.size(), file_path);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Locale: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||
Log::error("Locale", "error parsejant {}: {}", file_path, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
auto Locale::text(const std::string& key) const -> std::string {
|
||||
auto it = strings_.find(key);
|
||||
if (it != strings_.end()) { return it->second; }
|
||||
std::cerr << "Locale: clau no trobada: " << key << '\n';
|
||||
Log::warn("Locale", "clau no trobada: {}", key);
|
||||
return key;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "core/rendering/effects/heat_haze.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
namespace {
|
||||
constexpr float TWO_PI = 2.0F * std::numbers::pi_v<float>;
|
||||
}
|
||||
|
||||
HeatHaze::HeatHaze(Params params)
|
||||
: params_(params) {}
|
||||
|
||||
void HeatHaze::tick(int delta_ms) {
|
||||
const float DT = static_cast<float>(delta_ms) / 1000.0F;
|
||||
phase_ = std::fmod(phase_ + (TWO_PI * params_.speed_hz * DT), TWO_PI);
|
||||
}
|
||||
|
||||
void HeatHaze::render(const Surface& src, int x, int y) const {
|
||||
// Pas 1 — blit pla de tota la Surface al destí. És la base que
|
||||
// tapa els marges que les files desplaçades deixarien al
|
||||
// descobert, evitant haver de demanar al caller un pre-fill. Per
|
||||
// a fons amb gradient continu (cels, sorres) la costura amb el
|
||||
// pas 2 no es nota; per a fons d'alt contrast cal moderar
|
||||
// l'amplitud.
|
||||
src.render(x, y);
|
||||
|
||||
// Pas 2 — re-blit fila a fila amb desplaçament horitzontal sin.
|
||||
// Wavelength <=0 dóna divisió absurda: cau a 1 px com a salvaguarda;
|
||||
// a la pràctica el caller passa un valor >0 vingut d'ui.yaml.
|
||||
const float WL = (params_.wavelength_px > 0.0F) ? params_.wavelength_px : 1.0F;
|
||||
const int H = src.getHeight();
|
||||
const auto W = static_cast<float>(src.getWidth());
|
||||
for (int row = 0; row < H; ++row) {
|
||||
const float ARG = (TWO_PI * static_cast<float>(row) / WL) + phase_;
|
||||
const int DX = static_cast<int>(std::lround(params_.amplitude_px * std::sin(ARG)));
|
||||
if (DX == 0) { continue; } // El pas 1 ja ha pintat aquesta fila on toca.
|
||||
SDL_FRect row_rect{0.0F, static_cast<float>(row), W, 1.0F};
|
||||
src.render(x + DX, y + row, &row_rect);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Effects
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
class Surface;
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Distorsió sinusoidal horitzontal de cada fila: dx(y) =
|
||||
// amplitude · sin(2π·y/wavelength + phase). La phase avança al
|
||||
// ritme marcat per `speed_hz` (cicles per segon). Es pinta sobre
|
||||
// qualsevol Surface — habitualment un fons de canvas — i manté el
|
||||
// seu propi estat temporal, així dues instàncies poden tenir
|
||||
// paràmetres i fases independents.
|
||||
//
|
||||
// `render()` fa dos passos: primer un blit pla de tota la Surface
|
||||
// (perquè els marges no quedin descoberts pel desplaçament) i
|
||||
// després el re-blit fila a fila amb el sin. La costura és
|
||||
// invisible en fons amb gradient continu i pràcticament invisible
|
||||
// en altres si l'amplitud és moderada.
|
||||
class HeatHaze {
|
||||
public:
|
||||
struct Params {
|
||||
float amplitude_px;
|
||||
float wavelength_px;
|
||||
float speed_hz;
|
||||
};
|
||||
|
||||
explicit HeatHaze(Params params);
|
||||
|
||||
void tick(int delta_ms);
|
||||
void render(const Surface& src, int x, int y) const;
|
||||
|
||||
private:
|
||||
Params params_;
|
||||
float phase_{0.0F};
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
@@ -0,0 +1,219 @@
|
||||
#include "core/rendering/effects/scan_reveal.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <numbers>
|
||||
#include <random>
|
||||
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/surface.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
namespace {
|
||||
|
||||
// Hash determinista per a (row, time_bucket): retorna float a
|
||||
// [-1, 1]. Usat per generar el jitter horitzontal de la banda
|
||||
// sense necessitat d'un RNG d'estat (cada frame el resultat
|
||||
// canvia perquè el time_bucket canvia, però dins d'un mateix
|
||||
// bucket dues files diferents donen valors distints).
|
||||
auto rowJitter(int row, int time_bucket) -> float {
|
||||
const auto SEED = static_cast<std::uint32_t>((row * 73856093) ^ (time_bucket * 19349663));
|
||||
// Mix barato (xorshift) per descorrelacionar bits.
|
||||
std::uint32_t x = SEED;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
// Normalitzem a [-1, 1].
|
||||
const float NORM = static_cast<float>(x) / static_cast<float>(UINT32_MAX);
|
||||
return (NORM * 2.0F) - 1.0F;
|
||||
}
|
||||
|
||||
// Bucket de temps per al jitter: agrupant en blocs de ~50ms el
|
||||
// jitter "salta" 20 cops per segon — visualment "viu" sense
|
||||
// ser caos pur a 60fps.
|
||||
constexpr int JITTER_TIME_BUCKET_MS = 50;
|
||||
|
||||
} // namespace
|
||||
|
||||
ScanReveal::ScanReveal()
|
||||
: rng_(std::random_device{}()) {}
|
||||
|
||||
void ScanReveal::begin(const Params& params) {
|
||||
params_ = params;
|
||||
elapsed_ms_ = 0;
|
||||
flash_remaining_ms_ = 0;
|
||||
flash_started_ = false;
|
||||
burst_remaining_ms_ = 0;
|
||||
burst_y_norm_ = 0.0F;
|
||||
burst_dx_ = 0;
|
||||
tail_started_ = false;
|
||||
tail_remaining_ms_ = 0;
|
||||
}
|
||||
|
||||
void ScanReveal::tick(int delta_ms) {
|
||||
if (params_.duration_ms <= 0) { return; }
|
||||
|
||||
// 1) Avanç del scan (clampejat a duration_ms).
|
||||
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, params_.duration_ms);
|
||||
|
||||
// 2) Tick del burst VHS actiu, si n'hi ha.
|
||||
if (burst_remaining_ms_ > 0) {
|
||||
burst_remaining_ms_ = std::max(0, burst_remaining_ms_ - delta_ms);
|
||||
}
|
||||
|
||||
// 3) Spawn d'un nou burst. Permès durant el scan i durant el
|
||||
// glitch tail post-flash; bloquejat exactament durant el
|
||||
// flash perquè aquell és un punt visual de tancament net.
|
||||
// Probabilitat per frame ≈ (per_sec/1000) · delta_ms.
|
||||
const bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0;
|
||||
const bool SCAN_ONGOING = elapsed_ms_ < params_.duration_ms;
|
||||
if (burst_remaining_ms_ == 0 && (SCAN_ONGOING || TAIL_ACTIVE) && params_.vhs_burst_per_sec > 0 && params_.vhs_burst_duration_ms > 0) {
|
||||
const float P = (static_cast<float>(params_.vhs_burst_per_sec) / 1000.0F) * static_cast<float>(delta_ms);
|
||||
std::uniform_real_distribution<float> dist01(0.0F, 1.0F);
|
||||
if (dist01(rng_) < P) {
|
||||
burst_remaining_ms_ = params_.vhs_burst_duration_ms;
|
||||
burst_y_norm_ = dist01(rng_);
|
||||
std::uniform_int_distribution<int> dist_sign(0, 1);
|
||||
const int SIGN = (dist_sign(rng_) == 0) ? 1 : -1;
|
||||
burst_dx_ = SIGN * params_.vhs_burst_amplitude_px;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Dispara el flash quan el scan acaba (una sola vegada).
|
||||
if (!flash_started_ && elapsed_ms_ >= params_.duration_ms && params_.flash_ms > 0) {
|
||||
flash_started_ = true;
|
||||
flash_remaining_ms_ = params_.flash_ms;
|
||||
}
|
||||
// Decrement del flash mentre estiga actiu.
|
||||
if (flash_remaining_ms_ > 0) {
|
||||
flash_remaining_ms_ = std::max(0, flash_remaining_ms_ - delta_ms);
|
||||
}
|
||||
|
||||
// 5) Glitch tail: arranca un sol cop quan el scan ha acabat i el
|
||||
// flash s'ha esgotat. Si no hi havia flash configurat, també
|
||||
// arranca tot just acabar el scan.
|
||||
if (!tail_started_ && elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0 && params_.tail_glitch_ms > 0) {
|
||||
tail_started_ = true;
|
||||
tail_remaining_ms_ = params_.tail_glitch_ms;
|
||||
}
|
||||
if (tail_remaining_ms_ > 0) {
|
||||
tail_remaining_ms_ = std::max(0, tail_remaining_ms_ - delta_ms);
|
||||
}
|
||||
}
|
||||
|
||||
auto ScanReveal::isActive() const -> bool {
|
||||
return params_.duration_ms > 0 && !isFinished();
|
||||
}
|
||||
|
||||
auto ScanReveal::isFinished() const -> bool {
|
||||
if (params_.duration_ms <= 0) { return true; }
|
||||
// Acabat = scan al final + flash exhaurit + glitch tail
|
||||
// exhaurit (o sense tail configurat).
|
||||
const bool SCAN_DONE = elapsed_ms_ >= params_.duration_ms;
|
||||
const bool FLASH_DONE = flash_remaining_ms_ <= 0;
|
||||
const bool TAIL_DONE = (params_.tail_glitch_ms <= 0) || (tail_started_ && tail_remaining_ms_ <= 0);
|
||||
return SCAN_DONE && FLASH_DONE && TAIL_DONE;
|
||||
}
|
||||
|
||||
void ScanReveal::render(const Surface& src, int x, int y) const {
|
||||
if (params_.duration_ms <= 0) {
|
||||
// Sense efecte: render pla.
|
||||
src.render(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
const int H = src.getHeight();
|
||||
const auto W_F = static_cast<float>(src.getWidth());
|
||||
const float T = static_cast<float>(elapsed_ms_) / static_cast<float>(params_.duration_ms);
|
||||
|
||||
// Banda: comença a y=-band_height i acaba a y=H. Així al t=0 cap
|
||||
// fila és visible i al t=1 totes ho són.
|
||||
const float SWEEP_F = -static_cast<float>(params_.band_height_px) + (T * static_cast<float>(H + params_.band_height_px));
|
||||
const int BAND_TOP = static_cast<int>(std::floor(SWEEP_F));
|
||||
const int BAND_BOT = BAND_TOP + params_.band_height_px;
|
||||
|
||||
const int TIME_BUCKET = elapsed_ms_ / JITTER_TIME_BUCKET_MS;
|
||||
const int BURST_Y_PX = static_cast<int>(burst_y_norm_ * static_cast<float>(H));
|
||||
const bool BURST_ACTIVE = burst_remaining_ms_ > 0;
|
||||
// Durant el glitch tail post-flash, BAND_TOP queda just damunt
|
||||
// del peu del logo, fet que (amb el codi de jitter que segueix)
|
||||
// generaria jitter residual a les últimes files. Aquí el
|
||||
// suprimim explícitament: el tail només viu de bursts VHS.
|
||||
const bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0;
|
||||
|
||||
// Render fila a fila (només files revelades).
|
||||
for (int row = 0; row <= BAND_BOT && row < H; ++row) {
|
||||
int dx = 0;
|
||||
|
||||
// Jitter de banda: amplitud màxima a la banda, falloff
|
||||
// lineal cap als marges del radi de glitch. Inactiu durant
|
||||
// el glitch tail (no hi ha banda).
|
||||
const int DIST_TO_BAND = std::abs(row - BAND_TOP);
|
||||
if (!TAIL_ACTIVE && params_.glitch_band_radius_px > 0 && DIST_TO_BAND <= params_.glitch_band_radius_px && params_.glitch_amplitude_px > 0) {
|
||||
const float FALLOFF = 1.0F - (static_cast<float>(DIST_TO_BAND) / static_cast<float>(params_.glitch_band_radius_px));
|
||||
const float JIT = rowJitter(row, TIME_BUCKET);
|
||||
dx += static_cast<int>(std::lround(
|
||||
FALLOFF * static_cast<float>(params_.glitch_amplitude_px) * JIT));
|
||||
}
|
||||
|
||||
// Burst VHS: només damunt la part ja revelada (row < BAND_TOP)
|
||||
// perquè per sota encara no hi ha imatge "estable" que poder
|
||||
// distorsionar visualment.
|
||||
if (BURST_ACTIVE && row < BAND_TOP && row >= BURST_Y_PX && row < BURST_Y_PX + params_.vhs_burst_height_px) {
|
||||
dx += burst_dx_;
|
||||
}
|
||||
|
||||
SDL_FRect row_rect{0.0F, static_cast<float>(row), W_F, 1.0F};
|
||||
src.render(x + dx, y + row, &row_rect);
|
||||
}
|
||||
|
||||
auto& screen = Screen::get();
|
||||
|
||||
// Capa lluminosa de la banda. Verticalment només cobreix les
|
||||
// files del logo (és on viu el scan), però horitzontalment
|
||||
// travessa tot el canvas com un feix CRT, així sobre fons negre
|
||||
// no es veuen les costures del rectangle del Surface. L'alpha
|
||||
// base es modula amb un pulse sinusoidal a `band_pulse_hz` per
|
||||
// simular el ritme de refresc d'un CRT.
|
||||
if (params_.band_alpha > 0 && params_.band_height_px > 0) {
|
||||
const int VISIBLE_TOP = std::clamp(BAND_TOP, 0, H);
|
||||
const int VISIBLE_BOT = std::clamp(BAND_BOT, 0, H);
|
||||
const int VISIBLE_H = VISIBLE_BOT - VISIBLE_TOP;
|
||||
if (VISIBLE_H > 0) {
|
||||
// Pulse: fracció ∈ [0,1] avançada a la freqüència donada,
|
||||
// mapejada a un multiplicador ∈ [1-depth, 1] sobre l'alpha
|
||||
// base. Amb depth=0 el pulse és inactiu (multiplicador 1).
|
||||
const float PULSE_DEPTH = std::clamp(params_.band_pulse_depth, 0.0F, 1.0F);
|
||||
const float TWO_PI = 2.0F * std::numbers::pi_v<float>;
|
||||
const float PHASE = TWO_PI * params_.band_pulse_hz * (static_cast<float>(elapsed_ms_) / 1000.0F);
|
||||
const float WAVE = 0.5F + (0.5F * std::sin(PHASE)); // 0..1
|
||||
const float MULT = (1.0F - PULSE_DEPTH) + (PULSE_DEPTH * WAVE);
|
||||
const float ALPHA_F = static_cast<float>(params_.band_alpha) * MULT;
|
||||
const auto ALPHA = static_cast<std::uint8_t>(
|
||||
std::clamp(static_cast<int>(std::round(ALPHA_F)), 0, 255));
|
||||
if (ALPHA > 0) {
|
||||
screen.blendRect(0, y + VISIBLE_TOP, screen.getCanvasWidth(), VISIBLE_H, 0xFFFFFFFFU, ALPHA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flash final: capa blanca damunt de TOT el canvas (no només
|
||||
// del rectangle del logo) — així el flash banya l'escena
|
||||
// sencera com un CRT i no es veuen les costures del Surface
|
||||
// sobre fons negre. Alpha proporcional a `flash_remaining_ms_
|
||||
// / flash_ms` (decreix suau cap a 0 al final del flash).
|
||||
if (flash_remaining_ms_ > 0 && params_.flash_ms > 0) {
|
||||
const float FLASH_T = static_cast<float>(flash_remaining_ms_) / static_cast<float>(params_.flash_ms);
|
||||
const auto ALPHA = static_cast<std::uint8_t>(
|
||||
std::round(255.0F * std::clamp(FLASH_T, 0.0F, 1.0F)));
|
||||
if (ALPHA > 0) {
|
||||
screen.blendRect(0, 0, screen.getCanvasWidth(), screen.getCanvasHeight(), 0xFFFFFFFFU, ALPHA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Effects
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <random>
|
||||
|
||||
class Surface;
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Revelat tipus "scanline CRT" d'una Surface. Una banda lluminosa
|
||||
// baixa des de dalt cap a baix de la imatge: les files damunt de la
|
||||
// banda es renderitzen normalment (ja revelades), les files sota
|
||||
// queden invisibles, i a la banda mateixa s'aplica un overlay
|
||||
// blanc semi-transparent. Al voltant de la banda les files reben un
|
||||
// jitter horitzontal aleatori (falloff lineal amb la distància) i
|
||||
// damunt la part ja revelada apareixen ocasionalment "bursts VHS" —
|
||||
// franges horitzontals desplaçades durant uns ms — per a un toc
|
||||
// demoscene/VHS deteriorat.
|
||||
//
|
||||
// Quan la banda surt per sota del logo i s'esgota `duration_ms`,
|
||||
// dispara un flash blanc breu. Si `tail_glitch_ms > 0`, després del
|
||||
// flash el logo queda totalment visible mentre els bursts VHS
|
||||
// continuen saltant durant aquest temps (glitch tail). Quan el tail
|
||||
// s'exhaureix `isFinished()=true`. El caller decideix què fa amb el
|
||||
// control de fase a partir d'aquí.
|
||||
//
|
||||
// Ús:
|
||||
// ScanReveal sr;
|
||||
// sr.begin({.duration_ms = 1500, ...});
|
||||
// // cada frame:
|
||||
// sr.tick(delta_ms);
|
||||
// sr.render(*logo, x, y);
|
||||
// if (sr.isFinished()) { ... canvi de fase ... }
|
||||
class ScanReveal {
|
||||
public:
|
||||
struct Params {
|
||||
int duration_ms;
|
||||
int band_height_px;
|
||||
int band_alpha;
|
||||
float band_pulse_hz;
|
||||
float band_pulse_depth;
|
||||
int glitch_amplitude_px;
|
||||
int glitch_band_radius_px;
|
||||
int vhs_burst_per_sec;
|
||||
int vhs_burst_duration_ms;
|
||||
int vhs_burst_height_px;
|
||||
int vhs_burst_amplitude_px;
|
||||
int flash_ms;
|
||||
// Glitch tail post-flash: el logo queda visible sencer i els
|
||||
// bursts VHS continuen saltant durant aquest temps. 0 ho
|
||||
// desactiva (efecte acaba al flash).
|
||||
int tail_glitch_ms;
|
||||
};
|
||||
|
||||
ScanReveal();
|
||||
|
||||
void begin(const Params& params);
|
||||
void tick(int delta_ms);
|
||||
|
||||
// Renderitza un frame del revelat sobre la Surface `src`. El
|
||||
// caller s'encarrega de pintar negre per sota (les files no
|
||||
// revelades simplement no es dibuixen — queda el que hi haja al
|
||||
// canvas, normalment negre).
|
||||
void render(const Surface& src, int x, int y) const;
|
||||
|
||||
[[nodiscard]] auto isActive() const -> bool;
|
||||
[[nodiscard]] auto isFinished() const -> bool;
|
||||
|
||||
private:
|
||||
Params params_{};
|
||||
int elapsed_ms_{0};
|
||||
// Flash post-scan: arranca quan el scan toca fons; decreix amb
|
||||
// `delta_ms` fins a 0. Mentre `flash_active_` és true encara no
|
||||
// hem reportat finished (donant marge per pintar el flash sencer).
|
||||
int flash_remaining_ms_{0};
|
||||
bool flash_started_{false};
|
||||
|
||||
// Burst VHS actiu (com a màxim un alhora — simplifica i ja és
|
||||
// suficient per al ritme visual). y_norm ∈ [0,1] es resol en
|
||||
// píxels al `render()` perquè aquí no coneixem l'alçada del logo.
|
||||
int burst_remaining_ms_{0};
|
||||
float burst_y_norm_{0.0F};
|
||||
int burst_dx_{0};
|
||||
|
||||
// Glitch tail post-flash: arranca quan el flash s'esgota i
|
||||
// permet que els bursts continuen saltant amb el logo ja del tot
|
||||
// visible. `isFinished()` espera al final d'aquest tail.
|
||||
bool tail_started_{false};
|
||||
int tail_remaining_ms_{0};
|
||||
|
||||
// RNG per als bursts. Seed des de random_device a `begin()`
|
||||
// perquè cada execució sigui visualment diferent.
|
||||
std::minstd_rand rng_;
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
@@ -1,10 +1,11 @@
|
||||
#include "core/rendering/gif.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace GIF {
|
||||
|
||||
namespace {
|
||||
@@ -111,7 +112,7 @@ namespace GIF {
|
||||
while (cur_code != -1) {
|
||||
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
|
||||
if (dictionary[cur_code].prev == cur_code) {
|
||||
std::cerr << "Internal error; self-reference detected." << '\n';
|
||||
Log::error("GIF", "internal error; self-reference detected.");
|
||||
throw std::runtime_error("Internal error in decompress: self-reference");
|
||||
}
|
||||
cur_code = dictionary[cur_code].prev;
|
||||
@@ -155,8 +156,7 @@ namespace GIF {
|
||||
|
||||
if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) {
|
||||
if (code > dictionary_ind) {
|
||||
std::cerr << "code = " << std::hex << code
|
||||
<< ", but dictionary_ind = " << dictionary_ind << '\n';
|
||||
Log::error("GIF", "code = {:x}, but dictionary_ind = {}", code, dictionary_ind);
|
||||
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
|
||||
}
|
||||
|
||||
@@ -167,8 +167,7 @@ namespace GIF {
|
||||
|
||||
// Verifica que 'code' sea un índice válido antes de usarlo.
|
||||
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
|
||||
std::cerr << "Invalid LZW code " << code
|
||||
<< ", dictionary size " << dictionary.size() << '\n';
|
||||
Log::error("GIF", "invalid LZW code {}, dictionary size {}", code, dictionary.size());
|
||||
throw std::runtime_error("LZW error: invalid code encountered");
|
||||
}
|
||||
|
||||
@@ -287,7 +286,7 @@ namespace GIF {
|
||||
} else if (block_type == IMAGE_DESCRIPTOR) {
|
||||
return processImageDescriptor(buffer);
|
||||
} else {
|
||||
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n';
|
||||
Log::error("GIF", "unrecognized block type {:x}", static_cast<int>(block_type));
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
block_type = *buffer++;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
// Temes per a les notificacions de pantalla. Un "preset" captura els dos
|
||||
// colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
|
||||
// en mil·lisegons. Els valors concrets viuen a data/ui.yaml (carregat per
|
||||
// en mil·lisegons. Els valors concrets viuen a data/config/ui.yaml (carregat per
|
||||
// UiConfig); per llançar una notificació amb un preset concret, cridar
|
||||
// Notification::notify(text, preset). Sense preset s'usa el DEFAULT.
|
||||
namespace Notification {
|
||||
@@ -22,7 +22,7 @@ namespace Notification {
|
||||
|
||||
// Preset per defecte: text clar sobre fons fosc. Es construeix en cada
|
||||
// crida a partir d'UiConfig perquè els valors no són constexpr: viuen a
|
||||
// data/ui.yaml i es carreguen a l'arrencada.
|
||||
// data/config/ui.yaml i es carreguen a l'arrencada.
|
||||
[[nodiscard]] inline auto defaultPreset() -> Preset {
|
||||
return {
|
||||
.text_color = UiConfig::get().notificationText(),
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
|
||||
#include "core/rendering/canvas.hpp"
|
||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -112,11 +112,11 @@ void Screen::createSdl() {
|
||||
backend->setVSync(config_.vsync);
|
||||
if (backend->init(window_, canvas_width_, canvas_height_)) {
|
||||
gpu_driver_ = backend->getDriverName();
|
||||
std::cout << "Screen: path GPU actiu (driver: " << gpu_driver_ << ")\n";
|
||||
Log::info("Screen", "path GPU actiu (driver: {})", gpu_driver_);
|
||||
shader_backend_ = std::move(backend);
|
||||
return;
|
||||
}
|
||||
std::cerr << "Screen: GPU no disponible, fallback a SDL_Renderer\n";
|
||||
Log::warn("Screen", "GPU no disponible, fallback a SDL_Renderer");
|
||||
}
|
||||
gpu_driver_ = "sdl";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include "core/rendering/sdl3gpu/shader_factory.hpp"
|
||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
|
||||
@@ -29,7 +29,7 @@ namespace Rendering {
|
||||
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
if (vert == nullptr || frag == nullptr) {
|
||||
std::cerr << "CrtPiPipeline: creació de shaders fallida: " << SDL_GetError() << '\n';
|
||||
Log::error("CrtPiPipeline", "creació de shaders fallida: {}", SDL_GetError());
|
||||
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
|
||||
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
|
||||
return false;
|
||||
@@ -57,7 +57,7 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUShader(device_, frag);
|
||||
|
||||
if (pipeline_ == nullptr) {
|
||||
std::cerr << "CrtPiPipeline: creació del pipeline ha fallat: " << SDL_GetError() << '\n';
|
||||
Log::error("CrtPiPipeline", "creació del pipeline ha fallat: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include "core/rendering/sdl3gpu/shader_factory.hpp"
|
||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
|
||||
@@ -59,11 +59,11 @@ namespace Rendering {
|
||||
const char* driver = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
|
||||
device_ = SDL_CreateGPUDevice(FORMAT, false, driver);
|
||||
if (device_ == nullptr) {
|
||||
std::cerr << "SDL3GPUShader: SDL_CreateGPUDevice ha fallat: " << SDL_GetError() << '\n';
|
||||
Log::error("SDL3GPUShader", "SDL_CreateGPUDevice ha fallat: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||
std::cerr << "SDL3GPUShader: SDL_ClaimWindowForGPUDevice ha fallat: " << SDL_GetError() << '\n';
|
||||
Log::error("SDL3GPUShader", "SDL_ClaimWindowForGPUDevice ha fallat: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
|
||||
@@ -82,7 +82,7 @@ namespace Rendering {
|
||||
fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
if (vertex_shader_ == nullptr || fragment_shader_ == nullptr) {
|
||||
std::cerr << "SDL3GPUShader: SDL_CreateGPUShader ha fallat: " << SDL_GetError() << '\n';
|
||||
Log::error("SDL3GPUShader", "SDL_CreateGPUShader ha fallat: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -105,7 +105,7 @@ namespace Rendering {
|
||||
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
|
||||
if (pipeline_ == nullptr) {
|
||||
std::cerr << "SDL3GPUShader: SDL_CreateGPUGraphicsPipeline ha fallat: " << SDL_GetError() << '\n';
|
||||
Log::error("SDL3GPUShader", "SDL_CreateGPUGraphicsPipeline ha fallat: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -137,7 +137,7 @@ namespace Rendering {
|
||||
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
|
||||
|
||||
if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) {
|
||||
std::cerr << "SDL3GPUShader: creació de recursos GPU ha fallat: " << SDL_GetError() << '\n';
|
||||
Log::error("SDL3GPUShader", "creació de recursos GPU ha fallat: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
// El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers)
|
||||
// viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni
|
||||
@@ -74,10 +74,10 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||
if (!first.frames.empty()) { setClip(first.frames[0].rect); }
|
||||
}
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
Log::error("AnimatedSprite", "YAML parsing error in animation {}: {}", cached_data.name, e.what());
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
Log::error("AnimatedSprite", "Error loading animation {}: {}", cached_data.name, e.what());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
||||
if (current_animation_ == name) { return; }
|
||||
auto it = animations_.find(name);
|
||||
if (it == animations_.end()) {
|
||||
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
|
||||
Log::warn("AnimatedSprite", "could not find \"{}\" animation", name);
|
||||
return;
|
||||
}
|
||||
current_animation_ = name;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
@@ -63,7 +62,7 @@ Surface::Surface(const std::string& file_path) {
|
||||
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
|
||||
}
|
||||
|
||||
printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]");
|
||||
printWithDots("Surface", getFileName(file_path), "[ LOADED ]");
|
||||
}
|
||||
|
||||
void Surface::clear(Uint8 color) {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -11,6 +10,7 @@
|
||||
#include "core/rendering/sprite/sprite.hpp"
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "utils/log.hpp"
|
||||
#include "utils/utils.hpp"
|
||||
|
||||
// Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte
|
||||
@@ -55,7 +55,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
|
||||
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Fitxer no trobat " << getFileName(file_path) << '\n';
|
||||
Log::error("Text File", "fitxer no trobat: {}", getFileName(file_path));
|
||||
throw std::runtime_error("Fitxer no trobat: " + getFileName(file_path));
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]");
|
||||
printWithDots("Text File", getFileName(file_path), "[ LOADED ]");
|
||||
return tf;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "core/rendering/transitions/fade_to_black.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
namespace Transitions {
|
||||
|
||||
void FadeToBlack::begin(int duration_ms, Direction direction) {
|
||||
duration_ms_ = duration_ms;
|
||||
elapsed_ms_ = 0;
|
||||
direction_ = direction;
|
||||
}
|
||||
|
||||
void FadeToBlack::tick(int delta_ms) {
|
||||
if (duration_ms_ <= 0) { return; }
|
||||
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, duration_ms_);
|
||||
}
|
||||
|
||||
auto FadeToBlack::progress() const -> float {
|
||||
if (duration_ms_ <= 0) { return 0.0F; }
|
||||
return std::clamp(static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_), 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
void FadeToBlack::render() const {
|
||||
if (duration_ms_ <= 0) { return; }
|
||||
const float P = progress();
|
||||
// Alpha lineal — sense corba quadràtica (això és prerogativa del
|
||||
// dissolve mosaic, on volem que la mascara fosca arribe tard
|
||||
// perquè es vegen els blocs). Per al fade net, lineal 0..255.
|
||||
// Direcció FADE_IN inverteix la rampa: el negre comença opac i
|
||||
// s'esvaeix cap a transparent, deixant veure l'escena pintada
|
||||
// darrere abans de `render()`.
|
||||
const float ALPHA_F = (direction_ == Direction::FADE_IN) ? (1.0F - P) : P;
|
||||
const auto ALPHA = static_cast<std::uint8_t>(std::round(255.0F * ALPHA_F));
|
||||
if (ALPHA == 0) { return; }
|
||||
|
||||
auto& screen = Screen::get();
|
||||
screen.blendRect(0, 0, screen.getCanvasWidth(), screen.getCanvasHeight(), 0x00000000U, ALPHA);
|
||||
}
|
||||
|
||||
} // namespace Transitions
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Transitions {
|
||||
|
||||
// Fade lineal contra negre. A diferència de `Mosaic`, no rebina cap
|
||||
// píxel; només posa una capa negra al damunt del canvas amb alpha
|
||||
// que evoluciona linealment al llarg de la durada. És la transició
|
||||
// "neta" per defecte; ideal per a entrades/sortides on no convé
|
||||
// l'estètica chunky de l'arcade dissolve.
|
||||
//
|
||||
// La direcció controla el sentit de l'alpha:
|
||||
// FADE_OUT: alpha 0 → 255 (món visible → negre opac)
|
||||
// FADE_IN: alpha 255 → 0 (negre opac → món visible)
|
||||
// En tots dos casos `progress()` segueix sent monotònic 0 → 1
|
||||
// (mateixa semàntica per al caller); només el render inverteix.
|
||||
//
|
||||
// Mateixa interfície que `Mosaic` (begin/tick/render/isActive/
|
||||
// isFinished/progress) perquè els llocs de crida puguen escollir-ne
|
||||
// una o l'altra sense canviar el patró d'ús.
|
||||
class FadeToBlack {
|
||||
public:
|
||||
enum class Direction : std::uint8_t { FADE_OUT,
|
||||
FADE_IN };
|
||||
|
||||
FadeToBlack() = default;
|
||||
|
||||
// Arrenca un fade nou. Si ja n'hi havia un actiu, el reemplaça.
|
||||
// `duration_ms <= 0` deixa l'efecte inactiu.
|
||||
void begin(int duration_ms, Direction direction = Direction::FADE_OUT);
|
||||
|
||||
void tick(int delta_ms);
|
||||
|
||||
[[nodiscard]] auto isActive() const -> bool { return duration_ms_ > 0 && elapsed_ms_ < duration_ms_; }
|
||||
[[nodiscard]] auto isFinished() const -> bool { return duration_ms_ > 0 && elapsed_ms_ >= duration_ms_; }
|
||||
[[nodiscard]] auto progress() const -> float;
|
||||
[[nodiscard]] auto direction() const -> Direction { return direction_; }
|
||||
|
||||
// Pinta la capa negra amb alpha derivat del `progress()` i la
|
||||
// direcció. Cridar després del render normal de l'escena,
|
||||
// abans de `Screen::present()`.
|
||||
void render() const;
|
||||
|
||||
private:
|
||||
int duration_ms_{0};
|
||||
int elapsed_ms_{0};
|
||||
Direction direction_{Direction::FADE_OUT};
|
||||
};
|
||||
|
||||
} // namespace Transitions
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "core/rendering/transitions/mosaic.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
namespace Transitions {
|
||||
|
||||
Mosaic::Mosaic(Params params)
|
||||
: params_(params) {}
|
||||
|
||||
void Mosaic::begin(int duration_ms) {
|
||||
duration_ms_ = duration_ms;
|
||||
elapsed_ms_ = 0;
|
||||
}
|
||||
|
||||
void Mosaic::tick(int delta_ms) {
|
||||
if (duration_ms_ <= 0) { return; }
|
||||
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, duration_ms_);
|
||||
}
|
||||
|
||||
auto Mosaic::progress() const -> float {
|
||||
if (duration_ms_ <= 0) { return 0.0F; }
|
||||
return std::clamp(static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_), 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
void Mosaic::render() const {
|
||||
if (duration_ms_ <= 0) { return; }
|
||||
const float P = progress();
|
||||
const int MAX_BLOCK = std::max(1, params_.max_block_size);
|
||||
// Blocs de mida creixent al llarg del fade. A p=0 → 1 px (sense
|
||||
// efecte visible); a p=1 → max_block. Es fa cap-amunt amb ceil
|
||||
// perquè el primer increment arribi aviat (el salt visual de
|
||||
// 1→2 ja es nota).
|
||||
const int BLOCK = std::clamp(static_cast<int>(std::ceil(static_cast<float>(MAX_BLOCK) * P)), 1, MAX_BLOCK);
|
||||
|
||||
auto& screen = Screen::get();
|
||||
std::uint32_t* canvas = screen.getCanvasArgb();
|
||||
const int W = screen.getCanvasWidth();
|
||||
const int H = screen.getCanvasHeight();
|
||||
|
||||
if (BLOCK > 1) {
|
||||
// Sample-and-fill in-place. Per a cada bloc, mostregem el
|
||||
// píxel central (les vores poden caure fora del canvas en
|
||||
// blocs de la última columna/fila → clamp).
|
||||
for (int by = 0; by < H; by += BLOCK) {
|
||||
const int Y_END = std::min(by + BLOCK, H);
|
||||
const int SY = std::min(by + (BLOCK / 2), H - 1);
|
||||
for (int bx = 0; bx < W; bx += BLOCK) {
|
||||
const int X_END = std::min(bx + BLOCK, W);
|
||||
const int SX = std::min(bx + (BLOCK / 2), W - 1);
|
||||
const std::uint32_t COLOR = canvas[(SY * W) + SX];
|
||||
for (int yy = by; yy < Y_END; ++yy) {
|
||||
std::uint32_t* row = canvas + (static_cast<std::ptrdiff_t>(yy) * W);
|
||||
std::fill(row + bx, row + X_END, COLOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fade-to-black quadràtic per damunt: la mascara fosca arriba
|
||||
// tard (a p=0.5 només 25% d'alpha) perquè es vegin els blocs.
|
||||
const auto ALPHA = static_cast<std::uint8_t>(std::round(255.0F * P * P));
|
||||
if (ALPHA > 0) {
|
||||
screen.blendRect(0, 0, W, H, 0x00000000U, ALPHA);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Transitions
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
namespace Transitions {
|
||||
|
||||
// Fade-out estil "mosaic dissolve": al llarg del temps els píxels del
|
||||
// canvas es reagrupen en blocs cada cop més grans (1×1 → max_block_size×
|
||||
// max_block_size). Cada bloc es pinta amb el píxel central del què
|
||||
// tenia abans, donant aquell aspecte chunky d'arcade. Al damunt
|
||||
// s'aplica un fade-to-black quadràtic (alpha = p² × 255) perquè el
|
||||
// resultat acabi en negre opac.
|
||||
//
|
||||
// La classe NO sap res del Screen abans de `render()`. El caller hi
|
||||
// crida després de pintar la seva escena: l'efecte llegeix els
|
||||
// píxels actuals del canvas, els rebina i escriu pixels modificats
|
||||
// al mateix canvas. És, per tant, un post-procés in-place.
|
||||
class Mosaic {
|
||||
public:
|
||||
struct Params {
|
||||
int max_block_size;
|
||||
};
|
||||
|
||||
explicit Mosaic(Params params);
|
||||
|
||||
// Arrenca un fade nou. Si ja n'hi havia un actiu, el reemplaça.
|
||||
// `duration_ms <= 0` deixa l'efecte inactiu.
|
||||
void begin(int duration_ms);
|
||||
|
||||
void tick(int delta_ms);
|
||||
|
||||
[[nodiscard]] auto isActive() const -> bool { return duration_ms_ > 0 && elapsed_ms_ < duration_ms_; }
|
||||
[[nodiscard]] auto isFinished() const -> bool { return duration_ms_ > 0 && elapsed_ms_ >= duration_ms_; }
|
||||
[[nodiscard]] auto progress() const -> float;
|
||||
|
||||
// Aplica el mosaic + alpha negre sobre el canvas actual de Screen.
|
||||
// Cridar després del render normal de l'escena, abans de
|
||||
// `Screen::present()`.
|
||||
void render() const;
|
||||
|
||||
private:
|
||||
Params params_;
|
||||
int duration_ms_{0};
|
||||
int elapsed_ms_{0};
|
||||
};
|
||||
|
||||
} // namespace Transitions
|
||||
@@ -1,12 +1,12 @@
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "core/audio/jail_audio.hpp"
|
||||
#include "core/resources/palette_loader.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "utils/log.hpp"
|
||||
#include "utils/utils.hpp"
|
||||
|
||||
// Deleters dels recursos d'àudio: necessiten veure el tipus complet de
|
||||
@@ -37,19 +37,19 @@ namespace Resource {
|
||||
throw std::runtime_error("Cache: cannot load animation " + file_path);
|
||||
}
|
||||
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
|
||||
printWithDots("Animation : ", getFileName(file_path), "[ LOADED ]");
|
||||
printWithDots("Animation", getFileName(file_path), "[ LOADED ]");
|
||||
}
|
||||
|
||||
void Cache::loadPalette(const std::string& name, const std::string& file_path) {
|
||||
auto palette = PaletteLoader::loadFromFile(file_path);
|
||||
palettes_.emplace_back(PaletteResource{.name = name, .palette = palette});
|
||||
printWithDots("Palette : ", getFileName(file_path), "[ LOADED ]");
|
||||
printWithDots("Palette", getFileName(file_path), "[ LOADED ]");
|
||||
}
|
||||
|
||||
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
|
||||
auto it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; });
|
||||
if (it == surfaces_.end()) {
|
||||
std::cerr << "Cache: surface not found: " << name << '\n';
|
||||
Log::error("Cache", "surface not found: {}", name);
|
||||
throw std::runtime_error("Surface not found: " + name);
|
||||
}
|
||||
return it->surface;
|
||||
@@ -58,7 +58,7 @@ namespace Resource {
|
||||
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& {
|
||||
auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; });
|
||||
if (it == animations_.end()) {
|
||||
std::cerr << "Cache: animation not found: " << name << '\n';
|
||||
Log::error("Cache", "animation not found: {}", name);
|
||||
throw std::runtime_error("Animation not found: " + name);
|
||||
}
|
||||
return *it;
|
||||
@@ -67,7 +67,7 @@ namespace Resource {
|
||||
auto Cache::getPalette(const std::string& name) -> const Palette& {
|
||||
auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; });
|
||||
if (it == palettes_.end()) {
|
||||
std::cerr << "Cache: palette not found: " << name << '\n';
|
||||
Log::error("Cache", "palette not found: {}", name);
|
||||
throw std::runtime_error("Palette not found: " + name);
|
||||
}
|
||||
return it->palette;
|
||||
@@ -83,7 +83,7 @@ namespace Resource {
|
||||
throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path);
|
||||
}
|
||||
musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)});
|
||||
printWithDots("Music : ", getFileName(file_path), "[ LOADED ]");
|
||||
printWithDots("Music", getFileName(file_path), "[ LOADED ]");
|
||||
}
|
||||
|
||||
void Cache::loadSound(const std::string& name, const std::string& file_path) {
|
||||
@@ -96,13 +96,13 @@ namespace Resource {
|
||||
throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path);
|
||||
}
|
||||
sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)});
|
||||
printWithDots("Sound : ", getFileName(file_path), "[ LOADED ]");
|
||||
printWithDots("Sound", getFileName(file_path), "[ LOADED ]");
|
||||
}
|
||||
|
||||
auto Cache::getMusic(const std::string& name) -> Ja::Music* {
|
||||
auto it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; });
|
||||
if (it == musics_.end()) {
|
||||
std::cerr << "Cache: music not found: " << name << '\n';
|
||||
Log::error("Cache", "music not found: {}", name);
|
||||
throw std::runtime_error("Music not found: " + name);
|
||||
}
|
||||
return it->music.get();
|
||||
@@ -111,7 +111,7 @@ namespace Resource {
|
||||
auto Cache::getSound(const std::string& name) -> Ja::Sound* {
|
||||
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; });
|
||||
if (it == sounds_.end()) {
|
||||
std::cerr << "Cache: sound not found: " << name << '\n';
|
||||
Log::error("Cache", "sound not found: {}", name);
|
||||
throw std::runtime_error("Sound not found: " + name);
|
||||
}
|
||||
return it->sound.get();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/resources/resource_pack.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace Resource::Helper {
|
||||
|
||||
@@ -26,14 +27,14 @@ namespace Resource::Helper {
|
||||
// Silenciem si veníem de prefer_disk: aleshores caurem
|
||||
// al pack i no és cap error real que el fitxer no estigui
|
||||
// al disc.
|
||||
if (verbose) { std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; }
|
||||
if (verbose) { Log::error("Resource::Helper", "cannot open {}", FULL); }
|
||||
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 " << FULL << '\n';
|
||||
Log::error("Resource::Helper", "failed to read {}", FULL);
|
||||
return {};
|
||||
}
|
||||
return data;
|
||||
@@ -51,14 +52,11 @@ namespace Resource::Helper {
|
||||
g_pack_loaded = g_pack.loadPack(pack_file);
|
||||
|
||||
if (g_pack_loaded) {
|
||||
std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount()
|
||||
<< " entries) from " << pack_file << '\n';
|
||||
Log::info("Resource::Helper", "pack loaded ({} entries) from {}", g_pack.getResourceCount(), pack_file);
|
||||
} else if (enable_fallback) {
|
||||
std::cout << "Resource::Helper: no pack at " << pack_file
|
||||
<< " — using filesystem fallback\n";
|
||||
Log::info("Resource::Helper", "no pack at {} — using filesystem fallback", pack_file);
|
||||
} else {
|
||||
std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file
|
||||
<< " and fallback disabled\n";
|
||||
Log::critical("Resource::Helper", "FATAL — no pack at {} and fallback disabled", pack_file);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -92,7 +90,7 @@ namespace Resource::Helper {
|
||||
// carpeta `data/`).
|
||||
const auto FROM_PACK = [&filepath]() -> std::vector<std::uint8_t> {
|
||||
if (!g_pack_loaded) { return {}; }
|
||||
constexpr std::string_view PREFIX = "data/";
|
||||
const std::string_view PREFIX{Defaults::Paths::DATA_ROOT};
|
||||
std::string key = filepath;
|
||||
if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); }
|
||||
if (g_pack.hasResource(key)) { return g_pack.getResource(key); }
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace Resource {
|
||||
|
||||
const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_2026_PACK_KEY";
|
||||
const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_ARCADE_PACK_KEY";
|
||||
|
||||
namespace {
|
||||
constexpr const char* MAGIC = "AE26";
|
||||
@@ -40,14 +41,14 @@ namespace Resource {
|
||||
std::array<char, 4> header{};
|
||||
file.read(header.data(), 4);
|
||||
if (std::string(header.data(), 4) != MAGIC) {
|
||||
std::cerr << "Resource::Pack: invalid pack file format (bad magic): " << pack_file << '\n';
|
||||
Log::error("Resource::Pack", "invalid pack file format (bad magic): {}", pack_file);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint32_t version = 0;
|
||||
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||
if (version != VERSION) {
|
||||
std::cerr << "Resource::Pack: unsupported pack version: " << version << '\n';
|
||||
Log::error("Resource::Pack", "unsupported pack version: {}", version);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -86,7 +87,7 @@ namespace Resource {
|
||||
auto Pack::savePack(const std::string& pack_file) const -> bool {
|
||||
std::ofstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "Resource::Pack: could not create pack file: " << pack_file << '\n';
|
||||
Log::error("Resource::Pack", "could not create pack file: {}", pack_file);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ namespace Resource {
|
||||
auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool {
|
||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
std::cerr << "Resource::Pack: could not open file: " << filepath << '\n';
|
||||
Log::error("Resource::Pack", "could not open file: {}", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -129,7 +130,7 @@ namespace Resource {
|
||||
|
||||
std::vector<std::uint8_t> file_data(file_size);
|
||||
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
|
||||
std::cerr << "Resource::Pack: could not read file: " << filepath << '\n';
|
||||
Log::error("Resource::Pack", "could not read file: {}", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -146,7 +147,7 @@ namespace Resource {
|
||||
|
||||
auto Pack::addDirectory(const std::string& directory) -> bool {
|
||||
if (!std::filesystem::exists(directory)) {
|
||||
std::cerr << "Resource::Pack: directory does not exist: " << directory << '\n';
|
||||
Log::error("Resource::Pack", "directory does not exist: {}", directory);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -169,7 +170,7 @@ namespace Resource {
|
||||
|
||||
const PackEntry& entry = it->second;
|
||||
if (entry.offset + entry.size > data_.size()) {
|
||||
std::cerr << "Resource::Pack: invalid resource data: " << filename << '\n';
|
||||
Log::error("Resource::Pack", "invalid resource data: {}", filename);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -179,7 +180,7 @@ namespace Resource {
|
||||
|
||||
std::uint32_t checksum = calculateChecksum(result);
|
||||
if (checksum != entry.checksum) {
|
||||
std::cerr << "Resource::Pack: checksum mismatch for: " << filename << '\n';
|
||||
Log::error("Resource::Pack", "checksum mismatch for: {}", filename);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <any>
|
||||
#include <utility>
|
||||
|
||||
// Base de totes les escenes. La transició entre escenes va per
|
||||
// `exit_request_`: l'escena hi posa un `ExitRequest` (definit a
|
||||
// `game/scenes/scene_flow.hpp`) amagat dins d'un `std::any` perquè
|
||||
// `core/scene/scene.hpp` no ha de saber dels tipus del joc. `Director`
|
||||
// fa el `std::any_cast` al canvi de frame i construeix la nova escena.
|
||||
class Scene {
|
||||
public:
|
||||
virtual ~Scene() = default;
|
||||
@@ -10,9 +16,9 @@ class Scene {
|
||||
virtual void render() = 0;
|
||||
|
||||
[[nodiscard]] auto wantsQuit() const -> bool { return wants_quit_; }
|
||||
[[nodiscard]] auto nextScene() -> std::unique_ptr<Scene> { return std::move(next_scene_); }
|
||||
[[nodiscard]] auto takeExitRequest() -> std::any { return std::exchange(exit_request_, {}); }
|
||||
|
||||
protected:
|
||||
bool wants_quit_{false};
|
||||
std::unique_ptr<Scene> next_scene_;
|
||||
std::any exit_request_;
|
||||
};
|
||||
@@ -3,13 +3,13 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <any>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/audio/sound_effects_config.hpp"
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "game/debug_config.hpp"
|
||||
#include "game/gameplay_config.hpp"
|
||||
#ifdef _DEBUG
|
||||
#include "game/editor/editor_config.hpp"
|
||||
#endif
|
||||
@@ -40,10 +41,11 @@
|
||||
#include "game/scenes/title_scene.hpp"
|
||||
#include "game/ui/service_menu.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
Director::Director() {
|
||||
if (!SDL_Init(0)) {
|
||||
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
|
||||
Log::critical("Director", "SDL_Init failed: {}", SDL_GetError());
|
||||
}
|
||||
|
||||
// Sistema de recursos: el pack viu al costat del binari (build/), el
|
||||
@@ -64,12 +66,13 @@ Director::Director() {
|
||||
constexpr bool ENABLE_FALLBACK = false;
|
||||
#endif
|
||||
if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) {
|
||||
std::fprintf(stderr, "Director: sistema de recursos no disponible; avortem\n");
|
||||
Log::critical("Director", "sistema de recursos no disponible; avortem");
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega
|
||||
// les opcions; si no existeix config.yaml el crea amb els defaults.
|
||||
// Prepara la carpeta de sistema (per plataforma: %APPDATA% a Windows,
|
||||
// ~/Library/Application Support a macOS, ~/.config a Linux) i carrega les
|
||||
// opcions; si no existeix config.yaml el crea amb els defaults.
|
||||
createSystemFolder();
|
||||
|
||||
// Gamepad: descobreix mandos presents silenciosament. Cal fer-ho ABANS
|
||||
@@ -83,7 +86,7 @@ Director::Director() {
|
||||
|
||||
Options::setConfigFile(system_folder_ + "config.yaml");
|
||||
if (!Options::loadFromFile()) {
|
||||
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
||||
Log::error("Director", "no s'ha pogut carregar ni desar config.yaml a {}", system_folder_);
|
||||
}
|
||||
|
||||
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
|
||||
@@ -91,16 +94,21 @@ Director::Director() {
|
||||
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
|
||||
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
|
||||
|
||||
// Tweaks de jugabilitat (látigo travessa sarcs, friendly fire, col·lisió
|
||||
// entre jugadors). Fitxer escrivible des del menú de servei (F9). Si
|
||||
// no existeix, GameplayConfig::load el crea amb els defaults.
|
||||
GameplayConfig::get().load(system_folder_ + "gameplay.yaml");
|
||||
|
||||
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
|
||||
// es crea amb presets per defecte (off, crt, ntsc).
|
||||
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
||||
if (!Options::loadPostFXFromFile()) {
|
||||
std::cerr << "Director: no s'ha pogut carregar ni desar postfx.yaml.\n";
|
||||
Log::error("Director", "no s'ha pogut carregar ni desar postfx.yaml.");
|
||||
}
|
||||
// Presets del shader CRT-Pi (shaders/crtpi.yaml).
|
||||
Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE);
|
||||
if (!Options::loadCrtPiFromFile()) {
|
||||
std::cerr << "Director: no s'ha pogut carregar ni desar crtpi.yaml.\n";
|
||||
Log::error("Director", "no s'ha pogut carregar ni desar crtpi.yaml.");
|
||||
}
|
||||
|
||||
// Screen es configura ara (subsystem + finestra + GPU/renderer); la seua
|
||||
@@ -116,22 +124,29 @@ Director::Director() {
|
||||
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");
|
||||
KeyConfig::get().load(Defaults::Paths::INPUT_YAML);
|
||||
|
||||
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
|
||||
// data/ui.yaml cau al valor compilat a Defaults::UI.
|
||||
UiConfig::get().load("data/ui.yaml");
|
||||
// data/config/ui.yaml cau al valor compilat a Defaults::UI.
|
||||
UiConfig::get().load(Defaults::Paths::UI_YAML);
|
||||
// Configuració específica de les escenes TITLE i LOGO. Es carreguen
|
||||
// additivament sobre el mateix UiConfig: cada fitxer només conté la
|
||||
// seua subclau (`title:` o `logo:`), i les claus absents a la base
|
||||
// (ui.yaml) o als yamls d'escena cauen al valor compilat a
|
||||
// Defaults::UI::Title / Defaults::UI::Logo.
|
||||
UiConfig::get().load(Defaults::Paths::TITLE_YAML);
|
||||
UiConfig::get().load(Defaults::Paths::LOGO_YAML);
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Tema visual de l'editor de tilemap (Debug-only). Claus absents
|
||||
// cauen als valors compilats a Defaults::Editor.
|
||||
EditorConfig::get().load("data/editor/editor.yaml");
|
||||
EditorConfig::get().load(Defaults::Paths::EDITOR_YAML);
|
||||
#endif
|
||||
|
||||
// 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 al fitxer data/ui.yaml via UiConfig.
|
||||
Resource::Cache::get().loadPalette("ui", "data/palettes/uzebox.pal");
|
||||
// noms de font viuen al fitxer data/config/ui.yaml via UiConfig.
|
||||
Resource::Cache::get().loadPalette("ui", Defaults::Paths::UI_PALETTE);
|
||||
const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif";
|
||||
Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, UiConfig::get().hudFont().gif);
|
||||
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||
@@ -150,22 +165,36 @@ Director::Director() {
|
||||
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden
|
||||
// ací perquè formen part del preload d'assets, com la font i la paleta;
|
||||
// la lògica de quina sona en cada estat viu a `GameScene`.
|
||||
Resource::Cache::get().loadMusic("room_intro.ogg", "data/music/room_intro.ogg");
|
||||
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg");
|
||||
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg");
|
||||
Resource::Cache::get().loadMusic("room_intro.ogg", Defaults::Paths::MUSIC_ROOM_INTRO);
|
||||
Resource::Cache::get().loadMusic("playing.ogg", Defaults::Paths::MUSIC_PLAYING);
|
||||
Resource::Cache::get().loadMusic("room_outro.ogg", Defaults::Paths::MUSIC_ROOM_OUTRO);
|
||||
// Pista de game over: pre-load igual que la resta perquè el playMusic
|
||||
// del moment en què es dispara la sub-FSM no facti disc.
|
||||
Resource::Cache::get().loadMusic("game_over.ogg", Defaults::Paths::MUSIC_GAME_OVER);
|
||||
// Música de la TITLE: arrenca al constructor de l'escena i es fa
|
||||
// fade-out quan l'escena entra a STARTING.
|
||||
Resource::Cache::get().loadMusic("title.ogg", Defaults::Paths::MUSIC_TITLE);
|
||||
|
||||
// Sons del menú de servei. Els noms venen de `data/ui.yaml` (amb
|
||||
// Sons del menú de servei. Els noms venen de `data/config/ui.yaml` (amb
|
||||
// fallback a `Defaults::UI::MENU_*_SOUND`); el ServiceMenu els
|
||||
// dispara via Audio::Group::INTERFACE.
|
||||
const auto& menu_select = UiConfig::get().menuSelectSound();
|
||||
const auto& menu_accept = UiConfig::get().menuAcceptSound();
|
||||
Resource::Cache::get().loadSound(menu_select, "data/sounds/" + menu_select);
|
||||
Resource::Cache::get().loadSound(menu_accept, "data/sounds/" + menu_accept);
|
||||
Resource::Cache::get().loadSound(menu_select, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + menu_select);
|
||||
Resource::Cache::get().loadSound(menu_accept, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + menu_accept);
|
||||
// So del START a la TITLE: el dispara TitleScene quan un jugador
|
||||
// prem START (vore enterStarting i el reset del segon jugador).
|
||||
const auto& player_start = UiConfig::get().playerStartSound();
|
||||
Resource::Cache::get().loadSound(player_start, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + player_start);
|
||||
// Soroll de LogoScene (scanline reveal). El dispara LogoScene al
|
||||
// ctor; amb fallback al nom de Defaults si la clau YAML no hi és.
|
||||
const auto& logo_glitch = UiConfig::get().logoGlitchSound();
|
||||
Resource::Cache::get().loadSound(logo_glitch, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + logo_glitch);
|
||||
|
||||
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
|
||||
// perquè els primers playSoundWith* (al començament del joc) ja
|
||||
// troben els presets disponibles.
|
||||
SoundEffectsConfig::get().load("data/sounds.yaml");
|
||||
SoundEffectsConfig::get().load(Defaults::Paths::SOUNDS_YAML);
|
||||
|
||||
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
|
||||
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
||||
@@ -189,10 +218,21 @@ Director::Director() {
|
||||
auto placeholder_font_surface = Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY);
|
||||
auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, UiConfig::get().placeholderFont().fnt);
|
||||
placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
|
||||
placeholder_text_ = placeholder_text;
|
||||
|
||||
// Font del copyright de la TITLE (Tiny5-Regular size 8). Pixel-art
|
||||
// estricte; va amb la paleta UI per compartir índex de color amb la
|
||||
// resta de textos placeholder.
|
||||
const std::string COPYRIGHT_FONT_GIF_KEY = UiConfig::get().copyrightFont().name + ".gif";
|
||||
Resource::Cache::get().loadSurface(COPYRIGHT_FONT_GIF_KEY, UiConfig::get().copyrightFont().gif);
|
||||
Resource::Cache::get().getSurface(COPYRIGHT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||
auto copyright_text = std::make_shared<Text>(Resource::Cache::get().getSurface(COPYRIGHT_FONT_GIF_KEY), UiConfig::get().copyrightFont().fnt);
|
||||
copyright_text->setDefaultKerning(UiConfig::get().copyrightFont().kerning);
|
||||
copyright_text_ = copyright_text;
|
||||
|
||||
// Font del menú de servei (F9). El títol va amb la variant `display`
|
||||
// i els items amb `condensed-display` (més compacte perquè caben més
|
||||
// files). Els dos noms i kernings viuen a data/ui.yaml via UiConfig.
|
||||
// files). Els dos noms i kernings viuen a data/config/ui.yaml via UiConfig.
|
||||
auto load_menu_font = [&](const UiConfig::Font& font) {
|
||||
const std::string KEY = font.name + ".gif";
|
||||
Resource::Cache::get().loadSurface(KEY, font.gif);
|
||||
@@ -203,6 +243,36 @@ Director::Director() {
|
||||
};
|
||||
auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont());
|
||||
auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont());
|
||||
|
||||
// Mini-logo del peu de la TITLE. Es preloda ací per consistència
|
||||
// amb la resta d'assets d'UI; manté la seua paleta original (no la
|
||||
// sobreescrivim amb la "ui" perquè el GIF ja porta els colors finals
|
||||
// del logo).
|
||||
constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif";
|
||||
Resource::Cache::get().loadSurface(MINI_LOGO_KEY, Defaults::Paths::LOGO_MINI_GIF);
|
||||
auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY);
|
||||
mini_logo_surface_ = mini_logo_surface;
|
||||
|
||||
// Logo sencer per a l'escena LOGO (centrat damunt fons negre). També
|
||||
// conserva la seua paleta nadiua: els colors finals del logo viuen
|
||||
// dins del propi GIF i no han de passar per la paleta UI.
|
||||
constexpr const char* LOGO_FULL_KEY = "logo_jailgames.gif";
|
||||
Resource::Cache::get().loadSurface(LOGO_FULL_KEY, Defaults::Paths::LOGO_FULL_GIF);
|
||||
logo_full_surface_ = Resource::Cache::get().getSurface(LOGO_FULL_KEY);
|
||||
|
||||
// Logotip del joc per a la TITLE (substitueix el text "Aventures en
|
||||
// Egipte ARCADE"). Igual que la resta de logos, paleta nadiua —
|
||||
// l'art ja porta els colors finals i no passa per la UI.
|
||||
constexpr const char* GAME_LOGO_KEY = "game_logo.gif";
|
||||
Resource::Cache::get().loadSurface(GAME_LOGO_KEY, Defaults::Paths::TITLE_GAME_LOGO_GIF);
|
||||
game_logo_surface_ = Resource::Cache::get().getSurface(GAME_LOGO_KEY);
|
||||
|
||||
// Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva
|
||||
// la seua paleta nadiua perquè el GIF ja porta els colors finals.
|
||||
constexpr const char* TITLE_BG_KEY = "title_bg.gif";
|
||||
Resource::Cache::get().loadSurface(TITLE_BG_KEY, Defaults::Paths::TITLE_BG_GIF);
|
||||
auto title_bg_surface = Resource::Cache::get().getSurface(TITLE_BG_KEY);
|
||||
title_bg_surface_ = title_bg_surface;
|
||||
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
|
||||
|
||||
// Overlay hook: Screen::present() cridarà aquesta lambda al principi,
|
||||
@@ -234,39 +304,20 @@ Director::Director() {
|
||||
const auto START = DebugConfig::StartScene::LOGO;
|
||||
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
||||
#endif
|
||||
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
||||
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
|
||||
|
||||
// El branch Release inicialitza STARTING_PLAYERS amb una constant
|
||||
// literal i clang considera la captura redundant; en Debug el valor és
|
||||
// runtime i la captura sí és imprescindible. La lambda viu dins de
|
||||
// IntroScene, més enllà del scope d'aquest ctor — capturar per
|
||||
// referència seria UB.
|
||||
// NOLINTNEXTLINE(clang-diagnostic-unused-lambda-capture)
|
||||
auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr<Scene> {
|
||||
auto cfg = GameScene::defaultConfig();
|
||||
cfg.starting_players = STARTING_PLAYERS;
|
||||
return std::make_unique<GameScene>(std::move(cfg));
|
||||
};
|
||||
auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr<Scene> {
|
||||
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);
|
||||
};
|
||||
Log::info("Director", "start_scene={} starting_players={}", DebugConfig::toString(START), DebugConfig::toString(STARTING_PLAYERS));
|
||||
|
||||
switch (START) {
|
||||
case DebugConfig::StartScene::LOGO:
|
||||
current_scene_ = std::make_unique<LogoScene>(placeholder_text, make_title);
|
||||
current_scene_ = buildScene(SceneId::LOGO, LogoParams{});
|
||||
break;
|
||||
case DebugConfig::StartScene::TITLE:
|
||||
current_scene_ = make_title();
|
||||
current_scene_ = buildScene(SceneId::TITLE, TitleParams{});
|
||||
break;
|
||||
case DebugConfig::StartScene::INTRO:
|
||||
current_scene_ = make_intro();
|
||||
current_scene_ = buildScene(SceneId::INTRO, IntroParams{.starting_players = STARTING_PLAYERS});
|
||||
break;
|
||||
case DebugConfig::StartScene::GAME:
|
||||
current_scene_ = make_game();
|
||||
current_scene_ = buildScene(SceneId::GAME, GameParams{.starting_players = STARTING_PLAYERS});
|
||||
break;
|
||||
}
|
||||
last_ticks_ms_ = SDL_GetTicks();
|
||||
@@ -290,25 +341,37 @@ Director::~Director() {
|
||||
}
|
||||
|
||||
void Director::createSystemFolder() {
|
||||
// Volem la config a $XDG_CONFIG_HOME/jailgames/aee_2026/ (defecte
|
||||
// ~/.config/jailgames/aee_2026/). SDL_GetPrefPath apunta a XDG_DATA_HOME
|
||||
// (.local/share), que és el lloc per a dades, no per a configuració.
|
||||
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
||||
// Carpeta de configuració per plataforma:
|
||||
// - Windows: %APPDATA%\jailgames\aee_arcade\
|
||||
// - macOS: ~/Library/Application Support/jailgames/aee_arcade/
|
||||
// - Linux: $XDG_CONFIG_HOME/jailgames/aee_arcade/ (defecte
|
||||
// ~/.config/jailgames/aee_arcade/)
|
||||
// No fem servir SDL_GetPrefPath: a Linux apunta a XDG_DATA_HOME
|
||||
// (~/.local/share), que és per a dades, no per a configuració.
|
||||
std::string base;
|
||||
#ifdef _WIN32
|
||||
const char* appdata = std::getenv("APPDATA");
|
||||
base = (appdata != nullptr && *appdata != '\0') ? std::string(appdata) : ".";
|
||||
#elif defined(__APPLE__)
|
||||
const char* home = std::getenv("HOME");
|
||||
base = (home != nullptr) ? std::string(home) + "/Library/Application Support" : ".";
|
||||
#else
|
||||
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
||||
if (xdg != nullptr && *xdg != '\0') {
|
||||
base = xdg;
|
||||
} else {
|
||||
const char* home = std::getenv("HOME");
|
||||
base = (home != nullptr) ? std::string(home) + "/.config" : ".";
|
||||
}
|
||||
system_folder_ = base + "/jailgames/aee_2026/";
|
||||
#endif
|
||||
system_folder_ = base + "/jailgames/aee_arcade/";
|
||||
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(system_folder_, error);
|
||||
// Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix.
|
||||
std::filesystem::create_directories(system_folder_ + "shaders/", error);
|
||||
if (error) {
|
||||
std::cerr << "Director: no s'ha pogut crear " << system_folder_ << ": " << error.message() << '\n';
|
||||
Log::error("Director", "no s'ha pogut crear {}: {}", system_folder_, error.message());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,11 +447,56 @@ void Director::processMenuInput() {
|
||||
|
||||
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);
|
||||
// Prioritat al menú: si un item del ServiceMenu ha encuat una
|
||||
// transició (típicament "Reiniciar" → LOGO amb reset), s'aplica
|
||||
// primer. Substitueix l'`ExitRequest` que pugui haver publicat
|
||||
// l'escena en aquest mateix frame — l'usuari ha cancel·lat l'estat
|
||||
// de joc explícitament i no ens interessa mantenir-ne la sortida.
|
||||
if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) {
|
||||
Log::info("Director", "scene transition (menu) → {}", toString(menu_req->next));
|
||||
current_scene_ = buildScene(menu_req->next, menu_req->params);
|
||||
return;
|
||||
}
|
||||
// L'escena ha publicat un `ExitRequest` dins de `exit_request_`.
|
||||
// Director el desempaqueta i construeix la nova escena via
|
||||
// `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el
|
||||
// log avisa i no canviem d'escena.
|
||||
auto request = current_scene_->takeExitRequest();
|
||||
if (!request.has_value()) { return; }
|
||||
try {
|
||||
const auto& req = std::any_cast<const ExitRequest&>(request);
|
||||
Log::info("Director", "scene transition → {}", toString(req.next));
|
||||
current_scene_ = buildScene(req.next, req.params);
|
||||
} catch (const std::bad_any_cast&) {
|
||||
Log::error("Director", "exit_request_ amb tipus inesperat — ignorant");
|
||||
}
|
||||
}
|
||||
|
||||
auto Director::buildScene(SceneId id, const SceneParams& params) -> std::unique_ptr<Scene> {
|
||||
// Durant la migració, les escenes que encara fan servir factories
|
||||
// legacy reben lambdes primes que tornen a delegar a `buildScene`.
|
||||
// Així el motor d'escena ja és Director-driven encara que els
|
||||
// ctors antics no l'utilitzen directament. Quan totes estiguen
|
||||
// migrades a `ExitRequest`, aquestes lambdes desapareixeran.
|
||||
switch (id) {
|
||||
case SceneId::LOGO: {
|
||||
const auto& p = std::get<LogoParams>(params);
|
||||
return std::make_unique<LogoScene>(logo_full_surface_, p.reset);
|
||||
}
|
||||
case SceneId::TITLE:
|
||||
return std::make_unique<TitleScene>(placeholder_text_, copyright_text_, title_bg_surface_, game_logo_surface_, mini_logo_surface_);
|
||||
case SceneId::INTRO: {
|
||||
const auto& p = std::get<IntroParams>(params);
|
||||
return std::make_unique<IntroScene>(placeholder_text_, p.starting_players);
|
||||
}
|
||||
case SceneId::GAME: {
|
||||
const auto& p = std::get<GameParams>(params);
|
||||
auto cfg = GameScene::defaultConfig();
|
||||
cfg.starting_players = p.starting_players;
|
||||
return std::make_unique<GameScene>(std::move(cfg));
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto Director::iterate() -> SDL_AppResult {
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "game/scenes/scene_flow.hpp"
|
||||
|
||||
class Scene;
|
||||
class Surface;
|
||||
class Text;
|
||||
|
||||
class Director {
|
||||
public:
|
||||
@@ -20,9 +24,27 @@ class Director {
|
||||
std::unique_ptr<Scene> current_scene_;
|
||||
std::uint64_t last_ticks_ms_{0};
|
||||
|
||||
// Recursos compartits per la fabricació d'escenes. S'instancien una
|
||||
// vegada al ctor (parsejant fnt's, retenint shared_ptrs del
|
||||
// Resource::Cache) i `buildScene` els passa pel valor a cada nova
|
||||
// escena. No són `static`/Meyers perquè depenen de l'inici d'SDL i
|
||||
// de Resource::Cache.
|
||||
std::shared_ptr<Text> placeholder_text_;
|
||||
std::shared_ptr<Text> copyright_text_;
|
||||
std::shared_ptr<Surface> title_bg_surface_;
|
||||
std::shared_ptr<Surface> game_logo_surface_;
|
||||
std::shared_ptr<Surface> mini_logo_surface_;
|
||||
std::shared_ptr<Surface> logo_full_surface_;
|
||||
|
||||
void createSystemFolder();
|
||||
void processEvents();
|
||||
static void processGamepadEvents();
|
||||
static void processMenuInput();
|
||||
void switchSceneIfRequested();
|
||||
|
||||
// Fabrica una escena nova a partir d'un identificador i els seus
|
||||
// paràmetres. És l'únic punt del codi que coneix com construir cada
|
||||
// tipus concret d'escena, així les escenes només declaren `ExitRequest`
|
||||
// i no s'enllacen entre si.
|
||||
[[nodiscard]] auto buildScene(SceneId id, const SceneParams& params) -> std::unique_ptr<Scene>;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "core/ui/ui_config.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
@@ -9,6 +8,7 @@
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/easing.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace {
|
||||
if (!node.contains(key)) { return; }
|
||||
const int VALUE = node[key].get_value<int>();
|
||||
if (VALUE < 0 || VALUE > 255) {
|
||||
std::cerr << "UiConfig: índex '" << key << "'=" << VALUE << " fora de rang 0..255 — ignorat\n";
|
||||
Log::warn("UiConfig", "índex '{}'={} fora de rang 0..255 — ignorat", key, VALUE);
|
||||
return;
|
||||
}
|
||||
dst = static_cast<Uint8>(VALUE);
|
||||
@@ -96,13 +96,71 @@ UiConfig::UiConfig()
|
||||
pause_exit_ms_(Defaults::UI::PAUSE_EXIT_MS),
|
||||
pause_enter_curve_(curveByName(Defaults::UI::PAUSE_ENTER_CURVE)),
|
||||
pause_exit_curve_(curveByName(Defaults::UI::PAUSE_EXIT_CURVE)),
|
||||
title_duration_ms_(Defaults::UI::Title::DURATION_MS),
|
||||
title_bg_color_(Defaults::UI::Title::BG_COLOR),
|
||||
title_text_color_(Defaults::UI::Title::TEXT_COLOR),
|
||||
title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR),
|
||||
title_press_start_outline_color_(Defaults::UI::Title::PRESS_START_OUTLINE_COLOR),
|
||||
title_logo_y_percent_(Defaults::UI::Title::LOGO_Y_PERCENT),
|
||||
title_prompt_y_percent_(Defaults::UI::Title::PROMPT_Y_PERCENT),
|
||||
title_copyright_y_percent_(Defaults::UI::Title::COPYRIGHT_Y_PERCENT),
|
||||
title_mini_logo_gap_px_(Defaults::UI::Title::MINI_LOGO_GAP_PX),
|
||||
title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS),
|
||||
title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS),
|
||||
title_starting_ms_(Defaults::UI::Title::STARTING_MS),
|
||||
title_starting_blink_period_ms_(Defaults::UI::Title::STARTING_BLINK_PERIOD_MS),
|
||||
title_fade_out_ms_(Defaults::UI::Title::FADE_OUT_MS),
|
||||
title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK),
|
||||
title_music_fade_out_ms_(Defaults::UI::Title::MUSIC_FADE_OUT_MS),
|
||||
title_attract_fade_to_black_ms_(Defaults::UI::Title::ATTRACT_FADE_TO_BLACK_MS),
|
||||
title_intro_{
|
||||
.fade_in_ms = Defaults::UI::Title::Intro::FADE_IN_MS,
|
||||
.game_logo_at_ms = Defaults::UI::Title::Intro::GAME_LOGO_AT_MS,
|
||||
.mini_logo_at_ms = Defaults::UI::Title::Intro::MINI_LOGO_AT_MS,
|
||||
.mini_logo_slide_ms = Defaults::UI::Title::Intro::MINI_LOGO_SLIDE_MS,
|
||||
.copyright_at_ms = Defaults::UI::Title::Intro::COPYRIGHT_AT_MS,
|
||||
.copyright_slide_ms = Defaults::UI::Title::Intro::COPYRIGHT_SLIDE_MS,
|
||||
.slide_curve = curveByName(Defaults::UI::Title::Intro::SLIDE_CURVE),
|
||||
.press_start_at_ms = Defaults::UI::Title::Intro::PRESS_START_AT_MS,
|
||||
},
|
||||
title_heat_haze_{
|
||||
.enabled = Defaults::UI::Title::HeatHaze::ENABLED,
|
||||
.amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX,
|
||||
.wavelength_px = Defaults::UI::Title::HeatHaze::WAVELENGTH_PX,
|
||||
.speed_hz = Defaults::UI::Title::HeatHaze::SPEED_HZ,
|
||||
},
|
||||
logo_scan_{
|
||||
.duration_ms = Defaults::UI::Logo::Scan::DURATION_MS,
|
||||
.tail_glitch_ms = Defaults::UI::Logo::Scan::TAIL_GLITCH_MS,
|
||||
.band_height_px = Defaults::UI::Logo::Scan::BAND_HEIGHT_PX,
|
||||
.band_alpha = Defaults::UI::Logo::Scan::BAND_ALPHA,
|
||||
.band_pulse_hz = Defaults::UI::Logo::Scan::BAND_PULSE_HZ,
|
||||
.band_pulse_depth = Defaults::UI::Logo::Scan::BAND_PULSE_DEPTH,
|
||||
.glitch_amplitude_px = Defaults::UI::Logo::Scan::GLITCH_AMPLITUDE_PX,
|
||||
.glitch_band_radius_px = Defaults::UI::Logo::Scan::GLITCH_BAND_RADIUS_PX,
|
||||
.vhs_burst_per_sec = Defaults::UI::Logo::Scan::VHS_BURST_PER_SEC,
|
||||
.vhs_burst_duration_ms = Defaults::UI::Logo::Scan::VHS_BURST_DURATION_MS,
|
||||
.vhs_burst_height_px = Defaults::UI::Logo::Scan::VHS_BURST_HEIGHT_PX,
|
||||
.vhs_burst_amplitude_px = Defaults::UI::Logo::Scan::VHS_BURST_AMPLITUDE_PX,
|
||||
.flash_ms = Defaults::UI::Logo::Scan::FLASH_MS,
|
||||
},
|
||||
logo_timeline_{
|
||||
.black_intro_ms = Defaults::UI::Logo::Timeline::BLACK_INTRO_MS,
|
||||
.hold_silent_ms = Defaults::UI::Logo::Timeline::HOLD_SILENT_MS,
|
||||
.hold_with_music_ms = Defaults::UI::Logo::Timeline::HOLD_WITH_MUSIC_MS,
|
||||
.fade_out_ms = Defaults::UI::Logo::Timeline::FADE_OUT_MS,
|
||||
.black_outro_ms = Defaults::UI::Logo::Timeline::BLACK_OUTRO_MS,
|
||||
},
|
||||
menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND),
|
||||
menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND),
|
||||
player_start_sound_(Defaults::UI::PLAYER_START_SOUND),
|
||||
logo_glitch_sound_(Defaults::UI::LOGO_GLITCH_SOUND),
|
||||
hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING},
|
||||
placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING},
|
||||
room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING},
|
||||
menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING},
|
||||
menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING},
|
||||
copyright_font_{.name = Defaults::UI::Font::COPYRIGHT_NAME, .gif = Defaults::UI::Font::COPYRIGHT_GIF, .fnt = Defaults::UI::Font::COPYRIGHT_FNT, .kerning = Defaults::UI::Font::COPYRIGHT_KERNING},
|
||||
notifier_{
|
||||
.padding_h = Defaults::UI::Notifier::PADDING_H,
|
||||
.padding_top = Defaults::UI::Notifier::PADDING_TOP,
|
||||
@@ -125,7 +183,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
|
||||
const auto& reg = curveRegistry();
|
||||
const auto IT = reg.find(name);
|
||||
if (IT == reg.end()) {
|
||||
std::cerr << "UiConfig: corba '" << name << "' desconeguda — s'usa linear\n";
|
||||
Log::warn("UiConfig", "corba '{}' desconeguda — s'usa linear", name);
|
||||
return &Easing::linear;
|
||||
}
|
||||
return IT->second;
|
||||
@@ -134,7 +192,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
|
||||
void UiConfig::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "UiConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n";
|
||||
Log::warn("UiConfig", "no s'ha pogut obrir {} — s'usaran els defaults compilats", file_path);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,6 +238,7 @@ void UiConfig::load(const std::string& file_path) {
|
||||
readFont(f, "room_text", room_text_font_);
|
||||
readFont(f, "menu_title", menu_title_font_);
|
||||
readFont(f, "menu_items", menu_items_font_);
|
||||
readFont(f, "copyright", copyright_font_);
|
||||
}
|
||||
|
||||
if (yaml.contains("hud")) {
|
||||
@@ -202,6 +261,75 @@ void UiConfig::load(const std::string& file_path) {
|
||||
const auto& s = yaml["sounds"];
|
||||
readField(s, "menu_select", menu_select_sound_);
|
||||
readField(s, "menu_accept", menu_accept_sound_);
|
||||
readField(s, "player_start", player_start_sound_);
|
||||
readField(s, "logo_glitch", logo_glitch_sound_);
|
||||
}
|
||||
|
||||
if (yaml.contains("title")) {
|
||||
const auto& t = yaml["title"];
|
||||
readField(t, "duration_ms", title_duration_ms_);
|
||||
readColorIndex(t, "bg_color", title_bg_color_);
|
||||
readColorIndex(t, "text_color", title_text_color_);
|
||||
readColorIndex(t, "copyright_color", title_copyright_color_);
|
||||
readColorIndex(t, "press_start_outline_color", title_press_start_outline_color_);
|
||||
readField(t, "logo_y_percent", title_logo_y_percent_);
|
||||
readField(t, "prompt_y_percent", title_prompt_y_percent_);
|
||||
readField(t, "copyright_y_percent", title_copyright_y_percent_);
|
||||
readField(t, "mini_logo_gap_px", title_mini_logo_gap_px_);
|
||||
readField(t, "blink_visible_ms", title_blink_visible_ms_);
|
||||
readField(t, "blink_hidden_ms", title_blink_hidden_ms_);
|
||||
readField(t, "starting_ms", title_starting_ms_);
|
||||
readField(t, "starting_blink_period_ms", title_starting_blink_period_ms_);
|
||||
readField(t, "fade_out_ms", title_fade_out_ms_);
|
||||
readField(t, "fade_out_max_block", title_fade_out_max_block_);
|
||||
readField(t, "music_fade_out_ms", title_music_fade_out_ms_);
|
||||
readField(t, "attract_fade_to_black_ms", title_attract_fade_to_black_ms_);
|
||||
if (t.contains("intro")) {
|
||||
const auto& in = t["intro"];
|
||||
readField(in, "fade_in_ms", title_intro_.fade_in_ms);
|
||||
readField(in, "game_logo_at_ms", title_intro_.game_logo_at_ms);
|
||||
readField(in, "mini_logo_at_ms", title_intro_.mini_logo_at_ms);
|
||||
readField(in, "mini_logo_slide_ms", title_intro_.mini_logo_slide_ms);
|
||||
readField(in, "copyright_at_ms", title_intro_.copyright_at_ms);
|
||||
readField(in, "copyright_slide_ms", title_intro_.copyright_slide_ms);
|
||||
readCurve(in, "slide_curve", title_intro_.slide_curve);
|
||||
readField(in, "press_start_at_ms", title_intro_.press_start_at_ms);
|
||||
}
|
||||
if (t.contains("heat_haze")) {
|
||||
const auto& hh = t["heat_haze"];
|
||||
readField(hh, "enabled", title_heat_haze_.enabled);
|
||||
readField(hh, "amplitude_px", title_heat_haze_.amplitude_px);
|
||||
readField(hh, "wavelength_px", title_heat_haze_.wavelength_px);
|
||||
readField(hh, "speed_hz", title_heat_haze_.speed_hz);
|
||||
}
|
||||
}
|
||||
|
||||
if (yaml.contains("logo")) {
|
||||
const auto& l = yaml["logo"];
|
||||
if (l.contains("scan")) {
|
||||
const auto& s = l["scan"];
|
||||
readField(s, "duration_ms", logo_scan_.duration_ms);
|
||||
readField(s, "tail_glitch_ms", logo_scan_.tail_glitch_ms);
|
||||
readField(s, "band_height_px", logo_scan_.band_height_px);
|
||||
readField(s, "band_alpha", logo_scan_.band_alpha);
|
||||
readField(s, "band_pulse_hz", logo_scan_.band_pulse_hz);
|
||||
readField(s, "band_pulse_depth", logo_scan_.band_pulse_depth);
|
||||
readField(s, "glitch_amplitude_px", logo_scan_.glitch_amplitude_px);
|
||||
readField(s, "glitch_band_radius_px", logo_scan_.glitch_band_radius_px);
|
||||
readField(s, "vhs_burst_per_sec", logo_scan_.vhs_burst_per_sec);
|
||||
readField(s, "vhs_burst_duration_ms", logo_scan_.vhs_burst_duration_ms);
|
||||
readField(s, "vhs_burst_height_px", logo_scan_.vhs_burst_height_px);
|
||||
readField(s, "vhs_burst_amplitude_px", logo_scan_.vhs_burst_amplitude_px);
|
||||
readField(s, "flash_ms", logo_scan_.flash_ms);
|
||||
}
|
||||
if (l.contains("timeline")) {
|
||||
const auto& tl = l["timeline"];
|
||||
readField(tl, "black_intro_ms", logo_timeline_.black_intro_ms);
|
||||
readField(tl, "hold_silent_ms", logo_timeline_.hold_silent_ms);
|
||||
readField(tl, "hold_with_music_ms", logo_timeline_.hold_with_music_ms);
|
||||
readField(tl, "fade_out_ms", logo_timeline_.fade_out_ms);
|
||||
readField(tl, "black_outro_ms", logo_timeline_.black_outro_ms);
|
||||
}
|
||||
}
|
||||
|
||||
if (yaml.contains("notifier")) {
|
||||
@@ -217,8 +345,8 @@ void UiConfig::load(const std::string& file_path) {
|
||||
readCurve(n, "slide_out_curve", notifier_.slide_out_curve);
|
||||
}
|
||||
|
||||
std::cout << "UiConfig: carregat des de " << file_path << '\n';
|
||||
Log::info("UiConfig", "carregat des de {}", file_path);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "UiConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||
Log::error("UiConfig", "error parsejant {}: {}", file_path, e.what());
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <string>
|
||||
|
||||
// Configuració del tema UI (índexs de paleta, timings, fonts i flags)
|
||||
// carregada des de data/ui.yaml. Cada camp s'inicialitza amb el valor
|
||||
// carregada des de data/config/ui.yaml. Cada camp s'inicialitza amb el valor
|
||||
// de Defaults::UI com a fallback: si el fitxer falta o li manca una
|
||||
// clau, el joc s'executa amb el tema compilat.
|
||||
//
|
||||
@@ -21,12 +21,12 @@ class UiConfig {
|
||||
};
|
||||
|
||||
// Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1].
|
||||
// Permet triar corbes des de data/ui.yaml per nom (cadena) i guardar
|
||||
// Permet triar corbes des de data/config/ui.yaml per nom (cadena) i guardar
|
||||
// el punter a la corresponent funció de `utils/easing.hpp`.
|
||||
using CurveFn = auto (*)(float) -> float;
|
||||
|
||||
// Geometria del Notifier (popup superior). Carregada des de
|
||||
// data/ui.yaml secció `notifier:`; els valors per defecte vénen de
|
||||
// data/config/ui.yaml secció `notifier:`; els valors per defecte vénen de
|
||||
// Defaults::UI::Notifier. El moviment és per temps (slide_ms) i no per
|
||||
// velocitat, perquè una caixa més gran no triga més a entrar/eixir.
|
||||
struct Notifier {
|
||||
@@ -81,6 +81,83 @@ class UiConfig {
|
||||
[[nodiscard]] auto pauseEnterMs() const -> int { return pause_enter_ms_; }
|
||||
[[nodiscard]] auto pauseExitMs() const -> int { return pause_exit_ms_; }
|
||||
|
||||
// Escena TITLE (placeholder visual fins que tinguem artwork).
|
||||
struct TitleHeatHaze {
|
||||
bool enabled;
|
||||
float amplitude_px;
|
||||
float wavelength_px;
|
||||
float speed_hz;
|
||||
};
|
||||
|
||||
// Seqüència d'aparició dels elements en fase IDLE: tots els temps
|
||||
// són absoluts des de l'entrada a l'escena. `slide_curve` es
|
||||
// comparteix entre el slide-in del mini-logo i el del copyright.
|
||||
struct TitleIntro {
|
||||
int fade_in_ms;
|
||||
int game_logo_at_ms;
|
||||
int mini_logo_at_ms;
|
||||
int mini_logo_slide_ms;
|
||||
int copyright_at_ms;
|
||||
int copyright_slide_ms;
|
||||
CurveFn slide_curve;
|
||||
int press_start_at_ms;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto titleDurationMs() const -> int { return title_duration_ms_; }
|
||||
[[nodiscard]] auto titleBgColor() const -> Uint8 { return title_bg_color_; }
|
||||
[[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_color_; }
|
||||
[[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; }
|
||||
[[nodiscard]] auto titlePressStartOutlineColor() const -> Uint8 { return title_press_start_outline_color_; }
|
||||
[[nodiscard]] auto titleLogoYPercent() const -> int { return title_logo_y_percent_; }
|
||||
[[nodiscard]] auto titlePromptYPercent() const -> int { return title_prompt_y_percent_; }
|
||||
[[nodiscard]] auto titleCopyrightYPercent() const -> int { return title_copyright_y_percent_; }
|
||||
[[nodiscard]] auto titleMiniLogoGapPx() const -> int { return title_mini_logo_gap_px_; }
|
||||
[[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; }
|
||||
[[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; }
|
||||
[[nodiscard]] auto titleStartingMs() const -> int { return title_starting_ms_; }
|
||||
[[nodiscard]] auto titleStartingBlinkPeriodMs() const -> int { return title_starting_blink_period_ms_; }
|
||||
[[nodiscard]] auto titleFadeOutMs() const -> int { return title_fade_out_ms_; }
|
||||
[[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; }
|
||||
[[nodiscard]] auto titleMusicFadeOutMs() const -> int { return title_music_fade_out_ms_; }
|
||||
[[nodiscard]] auto titleAttractFadeToBlackMs() const -> int { return title_attract_fade_to_black_ms_; }
|
||||
[[nodiscard]] auto titleIntro() const -> const TitleIntro& { return title_intro_; }
|
||||
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
|
||||
|
||||
// Escena LOGO — paràmetres del scanline reveal del logo de l'editora.
|
||||
// `duration_ms` és la durada de la línia descendent; `tail_glitch_ms`
|
||||
// és el temps post-flash en què el logo queda totalment visible
|
||||
// però els bursts VHS encara hi salten (glitch tail).
|
||||
struct LogoScan {
|
||||
int duration_ms;
|
||||
int tail_glitch_ms;
|
||||
int band_height_px;
|
||||
int band_alpha;
|
||||
float band_pulse_hz;
|
||||
float band_pulse_depth;
|
||||
int glitch_amplitude_px;
|
||||
int glitch_band_radius_px;
|
||||
int vhs_burst_per_sec;
|
||||
int vhs_burst_duration_ms;
|
||||
int vhs_burst_height_px;
|
||||
int vhs_burst_amplitude_px;
|
||||
int flash_ms;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto logoScan() const -> const LogoScan& { return logo_scan_; }
|
||||
|
||||
// Timeline lineal de la LogoScene. Cada camp és la durada (ms) d'una
|
||||
// fase. La SCAN no apareix aquí perquè la marca el propi
|
||||
// `LogoScan` (duration_ms + tail_glitch_ms + flash_ms).
|
||||
struct LogoTimeline {
|
||||
int black_intro_ms;
|
||||
int hold_silent_ms;
|
||||
int hold_with_music_ms;
|
||||
int fade_out_ms;
|
||||
int black_outro_ms;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto logoTimeline() const -> const LogoTimeline& { return logo_timeline_; }
|
||||
|
||||
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
|
||||
// Easing::linear com a fallback.
|
||||
[[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; }
|
||||
@@ -93,9 +170,11 @@ class UiConfig {
|
||||
[[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn;
|
||||
|
||||
// Sons del menú de servei. Claus de Resource::Cache::loadSound (els
|
||||
// fitxers viuen a `data/sounds/`).
|
||||
// fitxers viuen a `data/audio/sounds/`).
|
||||
[[nodiscard]] auto menuSelectSound() const -> const std::string& { return menu_select_sound_; }
|
||||
[[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_sound_; }
|
||||
[[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; }
|
||||
[[nodiscard]] auto logoGlitchSound() const -> const std::string& { return logo_glitch_sound_; }
|
||||
|
||||
// Fonts de UI.
|
||||
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
|
||||
@@ -103,6 +182,7 @@ class UiConfig {
|
||||
[[nodiscard]] auto roomTextFont() const -> const Font& { return room_text_font_; }
|
||||
[[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; }
|
||||
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
|
||||
[[nodiscard]] auto copyrightFont() const -> const Font& { return copyright_font_; }
|
||||
|
||||
// Geometria del Notifier.
|
||||
[[nodiscard]] auto notifier() const -> const Notifier& { return notifier_; }
|
||||
@@ -145,14 +225,39 @@ class UiConfig {
|
||||
CurveFn pause_enter_curve_;
|
||||
CurveFn pause_exit_curve_;
|
||||
|
||||
int title_duration_ms_;
|
||||
Uint8 title_bg_color_;
|
||||
Uint8 title_text_color_;
|
||||
Uint8 title_copyright_color_;
|
||||
Uint8 title_press_start_outline_color_;
|
||||
int title_logo_y_percent_;
|
||||
int title_prompt_y_percent_;
|
||||
int title_copyright_y_percent_;
|
||||
int title_mini_logo_gap_px_;
|
||||
int title_blink_visible_ms_;
|
||||
int title_blink_hidden_ms_;
|
||||
int title_starting_ms_;
|
||||
int title_starting_blink_period_ms_;
|
||||
int title_fade_out_ms_;
|
||||
int title_fade_out_max_block_;
|
||||
int title_music_fade_out_ms_;
|
||||
int title_attract_fade_to_black_ms_;
|
||||
TitleIntro title_intro_;
|
||||
TitleHeatHaze title_heat_haze_;
|
||||
LogoScan logo_scan_;
|
||||
LogoTimeline logo_timeline_;
|
||||
|
||||
std::string menu_select_sound_;
|
||||
std::string menu_accept_sound_;
|
||||
std::string player_start_sound_;
|
||||
std::string logo_glitch_sound_;
|
||||
|
||||
Font hud_font_;
|
||||
Font placeholder_font_;
|
||||
Font room_text_font_;
|
||||
Font menu_title_font_;
|
||||
Font menu_items_font_;
|
||||
Font copyright_font_;
|
||||
|
||||
Notifier notifier_;
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace DebugConfig {
|
||||
|
||||
@@ -53,12 +53,12 @@ namespace DebugConfig {
|
||||
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";
|
||||
Log::warn("debug.yaml", "start_scene \"{}\" invàlid, arrancant a GAME", RAW);
|
||||
return StartScene::GAME;
|
||||
}
|
||||
return fromName(RAW);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "debug.yaml: error parsejant: " << e.what() << " — arrancant a GAME\n";
|
||||
Log::error("debug.yaml", "error parsejant: {} — arrancant a GAME", e.what());
|
||||
return StartScene::GAME;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ namespace DebugConfig {
|
||||
}
|
||||
const auto CONTENT = readFile(file_path);
|
||||
if (CONTENT.empty()) {
|
||||
std::cerr << "debug.yaml: fitxer buit a " << file_path << ", arrancant a GAME\n";
|
||||
Log::warn("debug.yaml", "fitxer buit a {}, arrancant a GAME", file_path);
|
||||
return StartScene::GAME;
|
||||
}
|
||||
return parseStartScene(CONTENT);
|
||||
@@ -85,12 +85,12 @@ namespace DebugConfig {
|
||||
if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; }
|
||||
const auto RAW = yaml["starting_players"].get_value<std::string>();
|
||||
if (!isKnownPlayers(RAW)) {
|
||||
std::cerr << "debug.yaml: starting_players \"" << RAW << "\" invàlid, arrancant amb BOTH\n";
|
||||
Log::warn("debug.yaml", "starting_players \"{}\" invàlid, arrancant amb BOTH", RAW);
|
||||
return StartingPlayers::BOTH;
|
||||
}
|
||||
return playersFromName(RAW);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "debug.yaml: error parsejant starting_players: " << e.what() << " — arrancant amb BOTH\n";
|
||||
Log::error("debug.yaml", "error parsejant starting_players: {} — arrancant amb BOTH", e.what());
|
||||
return StartingPlayers::BOTH;
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ namespace DebugConfig {
|
||||
if (!yaml.contains("starting_level")) { return std::nullopt; }
|
||||
const auto& sl = yaml["starting_level"];
|
||||
if (!sl.contains("pyramid") || !sl.contains("room")) {
|
||||
std::cerr << "debug.yaml: starting_level necessita els camps `pyramid` i `room` — ignorat\n";
|
||||
Log::warn("debug.yaml", "starting_level necessita els camps `pyramid` i `room` — ignorat");
|
||||
return std::nullopt;
|
||||
}
|
||||
return StartingLevel{
|
||||
@@ -118,7 +118,7 @@ namespace DebugConfig {
|
||||
.room = sl["room"].get_value<int>(),
|
||||
};
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "debug.yaml: error parsejant starting_level: " << e.what() << " — ignorat\n";
|
||||
Log::error("debug.yaml", "error parsejant starting_level: {} — ignorat", e.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ namespace DebugConfig {
|
||||
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';
|
||||
Log::error("debug.yaml", "no s'ha pogut crear {}", file_path);
|
||||
return;
|
||||
}
|
||||
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
||||
@@ -173,7 +173,7 @@ namespace DebugConfig {
|
||||
file << "# clau, comença a la primera (P1-R1).\n";
|
||||
file << "start_scene: GAME\n";
|
||||
file << "starting_players: BOTH\n";
|
||||
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME starting_players=BOTH\n";
|
||||
Log::info("debug.yaml", "creat a {} amb start_scene=GAME starting_players=BOTH", file_path);
|
||||
}
|
||||
|
||||
} // namespace DebugConfig
|
||||
@@ -5,9 +5,10 @@
|
||||
#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.
|
||||
// carpeta de sistema del joc (vegeu `Director::createSystemFolder` per la
|
||||
// ubicació real per plataforma). 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,
|
||||
|
||||
@@ -234,7 +234,7 @@ void Editor::ensureEditableEntities() {
|
||||
// llança si no troba — provem primer i, en cas de miss, carreguem.
|
||||
const auto REL = sp["path"].get_value<std::string>();
|
||||
const std::string KEY = std::filesystem::path(REL).filename().string();
|
||||
std::string full = "data/entities/";
|
||||
std::string full = Defaults::Paths::ENTITIES_DIR;
|
||||
full += name;
|
||||
full += '/';
|
||||
full += REL;
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace {
|
||||
if (!s.empty() && s.front() == '#') { s.erase(0, 1); }
|
||||
dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0));
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "EditorConfig: color '" << key << "' invàlid (" << e.what() << ") — ignorat\n";
|
||||
Log::warn("EditorConfig", "color '{}' invàlid ({}) — ignorat", key, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ auto EditorConfig::get() -> EditorConfig& {
|
||||
void EditorConfig::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "EditorConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n";
|
||||
Log::warn("EditorConfig", "no s'ha pogut obrir {} — s'usaran els defaults compilats", file_path);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -76,9 +76,9 @@ void EditorConfig::load(const std::string& file_path) {
|
||||
readArgb(c, "picker_hover", picker_hover_argb_);
|
||||
}
|
||||
|
||||
std::cout << "EditorConfig: carregat des de " << file_path << '\n';
|
||||
Log::info("EditorConfig", "carregat des de {}", file_path);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "EditorConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||
Log::error("EditorConfig", "error parsejant {}: {}", file_path, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <string>
|
||||
|
||||
// Configuració visual de l'editor de tilemap (Debug-only). Carregada des
|
||||
// de data/editor/editor.yaml; les claus absents cauen als valors compilats
|
||||
// de data/config/editor.yaml; les claus absents cauen als valors compilats
|
||||
// a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load).
|
||||
class EditorConfig {
|
||||
public:
|
||||
|
||||