Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -8,8 +8,8 @@ dist/
|
|||||||
*.dylib
|
*.dylib
|
||||||
*.dll
|
*.dll
|
||||||
*.exe
|
*.exe
|
||||||
aee_2026
|
aee_arcade
|
||||||
_aee_2026
|
_aee_arcade
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
.cache/
|
.cache/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
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)
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||||
@@ -104,18 +104,22 @@ set(APP_SOURCES
|
|||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
source/core/resources/palette_loader.cpp
|
source/core/resources/palette_loader.cpp
|
||||||
source/utils/easing.cpp
|
source/utils/easing.cpp
|
||||||
|
source/utils/log.cpp
|
||||||
source/utils/utils.cpp
|
source/utils/utils.cpp
|
||||||
source/game/debug_config.cpp
|
source/game/debug_config.cpp
|
||||||
|
source/game/gameplay_config.cpp
|
||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
source/game/options_apply.cpp
|
source/game/options_apply.cpp
|
||||||
source/game/ui/service_menu.cpp
|
source/game/ui/service_menu.cpp
|
||||||
source/game/ui/hud.cpp
|
source/game/ui/hud.cpp
|
||||||
source/game/scenes/placeholder_input.cpp
|
source/game/scenes/placeholder_input.cpp
|
||||||
|
source/game/scenes/start_input.cpp
|
||||||
source/game/scenes/logo_scene.cpp
|
source/game/scenes/logo_scene.cpp
|
||||||
source/game/scenes/title_scene.cpp
|
source/game/scenes/title_scene.cpp
|
||||||
source/game/scenes/intro_scene.cpp
|
source/game/scenes/intro_scene.cpp
|
||||||
source/game/scenes/game_scene.cpp
|
source/game/scenes/game_scene.cpp
|
||||||
source/game/scenes/room_fsm.cpp
|
source/game/scenes/room_fsm.cpp
|
||||||
|
source/game/scenes/game_over_fsm.cpp
|
||||||
source/game/entities/player.cpp
|
source/game/entities/player.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
source/game/map/tilemap.cpp
|
source/game/map/tilemap.cpp
|
||||||
@@ -218,8 +222,10 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
|||||||
add_executable(pack_resources EXCLUDE_FROM_ALL
|
add_executable(pack_resources EXCLUDE_FROM_ALL
|
||||||
tools/pack_resources/pack_resources.cpp
|
tools/pack_resources/pack_resources.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
|
source/utils/log.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
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)
|
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
|
||||||
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
||||||
@@ -361,9 +367,11 @@ if(BUILD_TESTS)
|
|||||||
add_executable(test_entities_smoke
|
add_executable(test_entities_smoke
|
||||||
tests/test_entities_smoke.cpp
|
tests/test_entities_smoke.cpp
|
||||||
tests/hole_player_stubs.cpp
|
tests/hole_player_stubs.cpp
|
||||||
|
tests/audio_cache_stubs.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
|
source/utils/log.cpp
|
||||||
${ENTITY_SOURCES_BASE}
|
${ENTITY_SOURCES_BASE}
|
||||||
)
|
)
|
||||||
target_include_directories(test_entities_smoke PRIVATE
|
target_include_directories(test_entities_smoke PRIVATE
|
||||||
@@ -389,9 +397,12 @@ if(BUILD_TESTS)
|
|||||||
tests/test_whip_attack.cpp
|
tests/test_whip_attack.cpp
|
||||||
tests/whip_attack_player_stubs.cpp
|
tests/whip_attack_player_stubs.cpp
|
||||||
tests/hole_player_stubs.cpp
|
tests/hole_player_stubs.cpp
|
||||||
|
tests/audio_cache_stubs.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
|
source/game/gameplay_config.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
|
source/utils/log.cpp
|
||||||
${ENTITY_SOURCES_BASE}
|
${ENTITY_SOURCES_BASE}
|
||||||
source/game/entities/concrete/whip_attack.cpp
|
source/game/entities/concrete/whip_attack.cpp
|
||||||
)
|
)
|
||||||
@@ -416,6 +427,7 @@ if(BUILD_TESTS)
|
|||||||
source/core/input/key_config.cpp
|
source/core/input/key_config.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
|
source/utils/log.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(test_scene_flow PRIVATE
|
target_include_directories(test_scene_flow PRIVATE
|
||||||
"${CMAKE_SOURCE_DIR}/source"
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
@@ -455,6 +467,7 @@ if(BUILD_TESTS)
|
|||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
source/utils/easing.cpp
|
source/utils/easing.cpp
|
||||||
|
source/utils/log.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(test_game_fsm PRIVATE
|
target_include_directories(test_game_fsm PRIVATE
|
||||||
"${CMAKE_SOURCE_DIR}/source"
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
PROJECT := aee_2026
|
PROJECT := aee_arcade
|
||||||
BUILDDIR := build
|
BUILDDIR := build
|
||||||
|
|
||||||
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
|
# 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
|
# 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
|
# de moviment del jugador viuen a part (Defaults::KeysGame); ací només hi ha
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Tema de la UI: índexs de paleta, timings, fonts i flags visuals. Tota
|
||||||
|
# clau absent cau al valor compilat a Defaults::UI (veure defaults.hpp).
|
||||||
|
|
||||||
|
colors:
|
||||||
|
render_info_text: 58
|
||||||
|
render_info_shadow: 16
|
||||||
|
canvas_bg: 72
|
||||||
|
notification_text: 232
|
||||||
|
notification_bg: 64
|
||||||
|
exit_text: 71
|
||||||
|
exit_bg: 2
|
||||||
|
menu_bg: 2
|
||||||
|
menu_border: 232
|
||||||
|
menu_title: 237
|
||||||
|
menu_label: 58
|
||||||
|
menu_value: 232
|
||||||
|
menu_cursor: 71
|
||||||
|
menu_disabled: 16
|
||||||
|
hud_bg: 9
|
||||||
|
hud_label: 58
|
||||||
|
hud_value: 187
|
||||||
|
hud_flash: 237
|
||||||
|
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
|
||||||
|
# de notification_* perquè la fase i les notificacions són dos canals
|
||||||
|
# visuals distints: ací pots canviar el color del rètol sense tocar
|
||||||
|
# els missatges del HUD.
|
||||||
|
room_text: 160
|
||||||
|
room_text_outline: 253
|
||||||
|
|
||||||
|
timings:
|
||||||
|
notification_ms: 4000
|
||||||
|
exit_ms: 4000
|
||||||
|
|
||||||
|
fonts:
|
||||||
|
hud:
|
||||||
|
name: "8bithud"
|
||||||
|
gif: "data/fonts/8bithud.gif"
|
||||||
|
fnt: "data/fonts/8bithud.fnt"
|
||||||
|
kerning: 1
|
||||||
|
placeholder:
|
||||||
|
name: "ari-w9500-condensed-display"
|
||||||
|
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||||
|
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||||
|
kerning: 1
|
||||||
|
# Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
||||||
|
room_text:
|
||||||
|
name: "CaesarDressing-Regular"
|
||||||
|
gif: "data/fonts/CaesarDressing-Regular.gif"
|
||||||
|
fnt: "data/fonts/CaesarDressing-Regular.fnt"
|
||||||
|
kerning: 1
|
||||||
|
menu_title:
|
||||||
|
name: "ari-w9500-display"
|
||||||
|
gif: "data/fonts/ari-w9500-display.gif"
|
||||||
|
fnt: "data/fonts/ari-w9500-display.fnt"
|
||||||
|
kerning: 0
|
||||||
|
menu_items:
|
||||||
|
name: "ari-w9500-condensed-display"
|
||||||
|
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||||
|
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||||
|
kerning: 0
|
||||||
|
# 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
|
||||||
|
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
|
||||||
|
flash_ms: 250 # durada del flash quan un jugador rep un hit
|
||||||
|
shake_ms: 300 # durada del shake que acompanya el flash
|
||||||
|
|
||||||
|
# Geometria del popup superior (Notifier). En píxels del canvas, llevat de
|
||||||
|
# slide_ms (durada en ms de l'animació d'entrada i d'eixida; constant
|
||||||
|
# independentment de la mida de la caixa). El padding vertical és asimètric a
|
||||||
|
# propòsit: padding_top > padding_bottom puja una mica el text dins la caixa.
|
||||||
|
# Corbes disponibles: linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
|
||||||
|
notifier:
|
||||||
|
padding_h: 6
|
||||||
|
padding_top: 4
|
||||||
|
padding_bottom: 3
|
||||||
|
line_gap: 1
|
||||||
|
margin_top: 3
|
||||||
|
border_px: 1
|
||||||
|
slide_ms: 300
|
||||||
|
slide_in_curve: outCubic
|
||||||
|
slide_out_curve: inCubic
|
||||||
|
|
||||||
|
# Pausa graduada de la GameScene: el món no para d'un colp, s'atura amb
|
||||||
|
# easing i torna a arrancar amb una altra corba. Els timings es poden
|
||||||
|
# afinar independentment, i les corbes permeten estils diferents entrant
|
||||||
|
# i sortint (p.ex. frenar suau i arrancar seca). Corbes disponibles:
|
||||||
|
# linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
|
||||||
|
pause:
|
||||||
|
enter_ms: 400
|
||||||
|
exit_ms: 250
|
||||||
|
enter_curve: outCubic
|
||||||
|
exit_curve: inCubic
|
||||||
|
|
||||||
|
# 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"
|
||||||
@@ -58,3 +58,6 @@ hurt:
|
|||||||
duration_ms: 600
|
duration_ms: 600
|
||||||
speed: 60
|
speed: 60
|
||||||
whippable: true
|
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_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||||
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||||
|
|
||||||
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
|
attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
|
||||||
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
|
attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
|
||||||
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
|
attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
|
||||||
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
|
attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
|
||||||
|
|
||||||
recover_south: { frames: [23] }
|
recover_south: { frames: [23] }
|
||||||
recover_north: { frames: [27] }
|
recover_north: { frames: [27] }
|
||||||
@@ -119,7 +119,7 @@ hurt:
|
|||||||
# recuperació en què la hitbox ja no fa dany.
|
# recuperació en què la hitbox ja no fa dany.
|
||||||
whip:
|
whip:
|
||||||
range_tiles: 3
|
range_tiles: 3
|
||||||
swing_ms: 200
|
swing_ms: 320
|
||||||
cooldown_ms: 200
|
cooldown_ms: 200
|
||||||
damage: 1
|
damage: 1
|
||||||
sound: whip.wav
|
sound: whip.wav
|
||||||
@@ -136,7 +136,7 @@ death:
|
|||||||
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
||||||
# caiguda no és dany convencional). Visualment es manifesta amb un
|
# caiguda no és dany convencional). Visualment es manifesta amb un
|
||||||
# parpadeig que alterna la paleta normal del sprite amb la paleta
|
# 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
|
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
|
||||||
# mecànica queda desactivada i el comportament és l'antic.
|
# mecànica queda desactivada i el comportament és l'antic.
|
||||||
immunity:
|
immunity:
|
||||||
@@ -40,10 +40,10 @@ animations:
|
|||||||
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||||
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||||
|
|
||||||
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
|
attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
|
||||||
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
|
attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
|
||||||
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
|
attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
|
||||||
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
|
attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
|
||||||
|
|
||||||
recover_south: { frames: [23] }
|
recover_south: { frames: [23] }
|
||||||
recover_north: { frames: [27] }
|
recover_north: { frames: [27] }
|
||||||
@@ -119,7 +119,7 @@ hurt:
|
|||||||
# recuperació en què la hitbox ja no fa dany.
|
# recuperació en què la hitbox ja no fa dany.
|
||||||
whip:
|
whip:
|
||||||
range_tiles: 3
|
range_tiles: 3
|
||||||
swing_ms: 200
|
swing_ms: 320
|
||||||
cooldown_ms: 200
|
cooldown_ms: 200
|
||||||
damage: 1
|
damage: 1
|
||||||
sound: whip.wav
|
sound: whip.wav
|
||||||
@@ -136,7 +136,7 @@ death:
|
|||||||
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
||||||
# caiguda no és dany convencional). Visualment es manifesta amb un
|
# caiguda no és dany convencional). Visualment es manifesta amb un
|
||||||
# parpadeig que alterna la paleta normal del sprite amb la paleta
|
# 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
|
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
|
||||||
# mecànica queda desactivada i el comportament és l'antic.
|
# mecànica queda desactivada i el comportament és l'antic.
|
||||||
immunity:
|
immunity:
|
||||||
@@ -25,7 +25,7 @@ sprites:
|
|||||||
# Animació compartida entre les 4 direccions: la seqüència de frames i les
|
# 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.
|
# durades són idèntiques; només canvia quina surface/mida es clipa.
|
||||||
# - `frames` : índex de tile dins l'spritesheet (graella deduïda).
|
# - `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`).
|
# - `loop: false` : el látigo viu una animació i mor (`markDead`).
|
||||||
# - `active_frames` : valors dins de `frames` durant els quals la
|
# - `active_frames` : valors dins de `frames` durant els quals la
|
||||||
# hitbox aplica dany. Wind-up (0) i recovery (3) no
|
# hitbox aplica dany. Wind-up (0) i recovery (3) no
|
||||||
@@ -33,6 +33,6 @@ sprites:
|
|||||||
animations:
|
animations:
|
||||||
swing:
|
swing:
|
||||||
frames: [0, 1, 2, 3]
|
frames: [0, 1, 2, 3]
|
||||||
frame_ms: 50
|
frame_ms: 80
|
||||||
loop: false
|
loop: false
|
||||||
active_frames: [1, 2]
|
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 |
@@ -1,4 +1,4 @@
|
|||||||
# AEE 2026 — Català
|
# AEE ARCADE — Català
|
||||||
# lang: ca
|
# lang: ca
|
||||||
|
|
||||||
notification:
|
notification:
|
||||||
@@ -38,6 +38,13 @@ hud:
|
|||||||
treasures: "Tres"
|
treasures: "Tres"
|
||||||
pyramid: "Piramide"
|
pyramid: "Piramide"
|
||||||
room: "Sala"
|
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.
|
# Textos de l'escena TITLE.
|
||||||
title:
|
title:
|
||||||
@@ -53,6 +60,12 @@ game:
|
|||||||
room_intro_room: "Sala {r}"
|
room_intro_room: "Sala {r}"
|
||||||
room_complete_line1: "Fase"
|
room_complete_line1: "Fase"
|
||||||
room_complete_line2: "Completada"
|
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:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
@@ -62,6 +75,7 @@ menu:
|
|||||||
controls: "Controls"
|
controls: "Controls"
|
||||||
keyboard: "Teclat"
|
keyboard: "Teclat"
|
||||||
gamepad: "Comandament"
|
gamepad: "Comandament"
|
||||||
|
tweaks: "Jugabilitat"
|
||||||
system: "Sistema"
|
system: "Sistema"
|
||||||
labels:
|
labels:
|
||||||
player_1: "Jugador 1"
|
player_1: "Jugador 1"
|
||||||
@@ -73,6 +87,7 @@ menu:
|
|||||||
controls_submenu: "Controls"
|
controls_submenu: "Controls"
|
||||||
keyboard_submenu: "Teclat"
|
keyboard_submenu: "Teclat"
|
||||||
gamepad_submenu: "Comandament"
|
gamepad_submenu: "Comandament"
|
||||||
|
tweaks_submenu: "Jugabilitat"
|
||||||
system_submenu: "Sistema"
|
system_submenu: "Sistema"
|
||||||
move_up: "Amunt"
|
move_up: "Amunt"
|
||||||
move_down: "Avall"
|
move_down: "Avall"
|
||||||
@@ -101,6 +116,9 @@ menu:
|
|||||||
music_volume: "Volum música"
|
music_volume: "Volum música"
|
||||||
sounds: "Sons"
|
sounds: "Sons"
|
||||||
sounds_volume: "Volum sons"
|
sounds_volume: "Volum sons"
|
||||||
|
whip_pierces_walls: "Látec travessa sarcs"
|
||||||
|
friendly_fire: "Foc amic"
|
||||||
|
players_collide: "Col·lisió entre jugadors"
|
||||||
restart: "Reiniciar"
|
restart: "Reiniciar"
|
||||||
exit_game: "Eixir del joc"
|
exit_game: "Eixir del joc"
|
||||||
values:
|
values:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# AEE 2026 — English
|
# AEE ARCADE — English
|
||||||
# lang: en
|
# lang: en
|
||||||
|
|
||||||
notification:
|
notification:
|
||||||
@@ -38,6 +38,13 @@ hud:
|
|||||||
treasures: "Tre"
|
treasures: "Tre"
|
||||||
pyramid: "Pyramid"
|
pyramid: "Pyramid"
|
||||||
room: "Room"
|
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.
|
# Texts of the TITLE scene.
|
||||||
title:
|
title:
|
||||||
@@ -52,6 +59,12 @@ game:
|
|||||||
room_intro_room: "Room {r}"
|
room_intro_room: "Room {r}"
|
||||||
room_complete_line1: "Stage"
|
room_complete_line1: "Stage"
|
||||||
room_complete_line2: "Complete"
|
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:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
@@ -61,6 +74,7 @@ menu:
|
|||||||
controls: "Controls"
|
controls: "Controls"
|
||||||
keyboard: "Keyboard"
|
keyboard: "Keyboard"
|
||||||
gamepad: "Gamepad"
|
gamepad: "Gamepad"
|
||||||
|
tweaks: "Gameplay"
|
||||||
system: "System"
|
system: "System"
|
||||||
labels:
|
labels:
|
||||||
player_1: "Player 1"
|
player_1: "Player 1"
|
||||||
@@ -72,6 +86,7 @@ menu:
|
|||||||
controls_submenu: "Controls"
|
controls_submenu: "Controls"
|
||||||
keyboard_submenu: "Keyboard"
|
keyboard_submenu: "Keyboard"
|
||||||
gamepad_submenu: "Gamepad"
|
gamepad_submenu: "Gamepad"
|
||||||
|
tweaks_submenu: "Gameplay"
|
||||||
system_submenu: "System"
|
system_submenu: "System"
|
||||||
move_up: "Up"
|
move_up: "Up"
|
||||||
move_down: "Down"
|
move_down: "Down"
|
||||||
@@ -100,6 +115,9 @@ menu:
|
|||||||
music_volume: "Music volume"
|
music_volume: "Music volume"
|
||||||
sounds: "Sounds"
|
sounds: "Sounds"
|
||||||
sounds_volume: "Sound volume"
|
sounds_volume: "Sound volume"
|
||||||
|
whip_pierces_walls: "Whip pierces sarcs"
|
||||||
|
friendly_fire: "Friendly fire"
|
||||||
|
players_collide: "Player collision"
|
||||||
restart: "Restart"
|
restart: "Restart"
|
||||||
exit_game: "Exit game"
|
exit_game: "Exit game"
|
||||||
values:
|
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
|
||||||
@@ -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
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
# Tema de la UI: índexs de paleta, timings, fonts i flags visuals. Tota
|
|
||||||
# clau absent cau al valor compilat a Defaults::UI (veure defaults.hpp).
|
|
||||||
|
|
||||||
colors:
|
|
||||||
render_info_text: 58
|
|
||||||
render_info_shadow: 16
|
|
||||||
canvas_bg: 72
|
|
||||||
notification_text: 232
|
|
||||||
notification_bg: 64
|
|
||||||
exit_text: 71
|
|
||||||
exit_bg: 2
|
|
||||||
menu_bg: 2
|
|
||||||
menu_border: 232
|
|
||||||
menu_title: 237
|
|
||||||
menu_label: 58
|
|
||||||
menu_value: 232
|
|
||||||
menu_cursor: 71
|
|
||||||
menu_disabled: 16
|
|
||||||
hud_bg: 9
|
|
||||||
hud_label: 58
|
|
||||||
hud_value: 187
|
|
||||||
hud_flash: 237
|
|
||||||
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
|
|
||||||
# de notification_* perquè la fase i les notificacions són dos canals
|
|
||||||
# visuals distints: ací pots canviar el color del rètol sense tocar
|
|
||||||
# els missatges del HUD.
|
|
||||||
room_text: 160
|
|
||||||
room_text_outline: 253
|
|
||||||
|
|
||||||
timings:
|
|
||||||
notification_ms: 4000
|
|
||||||
exit_ms: 4000
|
|
||||||
|
|
||||||
fonts:
|
|
||||||
hud:
|
|
||||||
name: "8bithud"
|
|
||||||
gif: "data/fonts/8bithud.gif"
|
|
||||||
fnt: "data/fonts/8bithud.fnt"
|
|
||||||
kerning: 1
|
|
||||||
placeholder:
|
|
||||||
name: "ari-w9500-condensed-display"
|
|
||||||
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
|
||||||
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
|
||||||
kerning: 1
|
|
||||||
# Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
|
||||||
room_text:
|
|
||||||
name: "CaesarDressing-Regular"
|
|
||||||
gif: "data/fonts/CaesarDressing-Regular.gif"
|
|
||||||
fnt: "data/fonts/CaesarDressing-Regular.fnt"
|
|
||||||
kerning: 1
|
|
||||||
menu_title:
|
|
||||||
name: "ari-w9500-display"
|
|
||||||
gif: "data/fonts/ari-w9500-display.gif"
|
|
||||||
fnt: "data/fonts/ari-w9500-display.fnt"
|
|
||||||
kerning: 0
|
|
||||||
menu_items:
|
|
||||||
name: "ari-w9500-condensed-display"
|
|
||||||
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
|
||||||
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
|
||||||
kerning: 0
|
|
||||||
# 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
|
|
||||||
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
|
|
||||||
flash_ms: 250 # durada del flash quan un jugador rep un hit
|
|
||||||
shake_ms: 300 # durada del shake que acompanya el flash
|
|
||||||
|
|
||||||
# Geometria del popup superior (Notifier). En píxels del canvas, llevat de
|
|
||||||
# slide_ms (durada en ms de l'animació d'entrada i d'eixida; constant
|
|
||||||
# independentment de la mida de la caixa). El padding vertical és asimètric a
|
|
||||||
# propòsit: padding_top > padding_bottom puja una mica el text dins la caixa.
|
|
||||||
# Corbes disponibles: linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
|
|
||||||
notifier:
|
|
||||||
padding_h: 6
|
|
||||||
padding_top: 4
|
|
||||||
padding_bottom: 3
|
|
||||||
line_gap: 1
|
|
||||||
margin_top: 3
|
|
||||||
border_px: 1
|
|
||||||
slide_ms: 300
|
|
||||||
slide_in_curve: outCubic
|
|
||||||
slide_out_curve: inCubic
|
|
||||||
|
|
||||||
# Pausa graduada de la GameScene: el món no para d'un colp, s'atura amb
|
|
||||||
# easing i torna a arrancar amb una altra corba. Els timings es poden
|
|
||||||
# afinar independentment, i les corbes permeten estils diferents entrant
|
|
||||||
# i sortint (p.ex. frenar suau i arrancar seca). Corbes disponibles:
|
|
||||||
# linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
|
|
||||||
pause:
|
|
||||||
enter_ms: 400
|
|
||||||
exit_ms: 250
|
|
||||||
enter_curve: outCubic
|
|
||||||
exit_curve: inCubic
|
|
||||||
|
|
||||||
# Sons del menú de servei. Els fitxers viuen a data/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"
|
|
||||||
|
|
||||||
# Escena TITLE (placeholder visual mentre no hi haja artwork).
|
|
||||||
# `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
|
|
||||||
|
|
||||||
# Escena LOGO (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
|
|
||||||
@@ -22,8 +22,8 @@ real (logo animat, menú de títol, cutscene).
|
|||||||
Fitxer personal de desenvolupament, **fora del repo**. Viu a la mateixa carpeta
|
Fitxer personal de desenvolupament, **fora del repo**. Viu a la mateixa carpeta
|
||||||
de sistema que `config.yaml`:
|
de sistema que `config.yaml`:
|
||||||
|
|
||||||
- Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml`
|
- Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_arcade/debug.yaml`
|
||||||
- Per defecte: `~/.config/jailgames/aee_2026/debug.yaml`
|
- Per defecte: `~/.config/jailgames/aee_arcade/debug.yaml`
|
||||||
|
|
||||||
### Comportament segons build
|
### Comportament segons build
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Sistema d'entitats
|
# 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`.
|
Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
|
|||||||
|
|
||||||
## 1. Context i objectiu inicial
|
## 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):
|
**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.
|
- `make build`: OK, 0 warnings nous.
|
||||||
- `./build/test_entities_smoke`: **9/9 OK**.
|
- `./build/test_entities_smoke`: **9/9 OK**.
|
||||||
- `ctest --output-on-failure`: 1/1 suite passed.
|
- `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 : torch.gif ... LOADED
|
||||||
Surface : sarcophagus.gif ... LOADED (×2)
|
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.
|
- `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.
|
- `./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
|
### Pendent derivat
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Pla d'implementació — Menú de servei
|
# Pla d'implementació — Menú de servei
|
||||||
|
|
||||||
Pla per a portar el menú de servei de `../aee` (`source/core/rendering/menu.cpp`)
|
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
|
`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
|
referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi
|
||||||
encara.
|
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
|
## 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()` |
|
| 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` |
|
| 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
|
### Limitació: pàgina Controls diferida
|
||||||
|
|
||||||
El menú de `../aee` té una pàgina Controls (`buildControls`) que permet
|
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
|
- Les tecles de joc són `Defaults::KeysGame::{UP,DOWN,LEFT,RIGHT}` — constants
|
||||||
`constexpr`, no mutables.
|
`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.
|
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
|
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
|
confirmar mida real (mateixos 320×200?) a `utils/defines.hpp` abans de
|
||||||
hardcoded.
|
hardcoded.
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,17 @@
|
|||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Aventures en Egipte ARCADE</string>
|
<string>Aventures en Egipte ARCADE</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>aee_2026</string>
|
<string>aee_arcade</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>icon</string>
|
<string>icon</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleIconName</key>
|
||||||
<string>icon</string>
|
<string>icon</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>org.jailgames.aee_2026</string>
|
<string>org.jailgames.aee_arcade</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>aee_2026</string>
|
<string>aee_arcade</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// aee_2026.rc
|
// aee_arcade.rc
|
||||||
IDI_ICON1 ICON "release/icons/icon.ico"
|
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
|
// Reprodueix un so per nom
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
// 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) {
|
void Audio::playSound(const std::string& name, Group group) {
|
||||||
|
|||||||
@@ -76,11 +76,16 @@ class Audio {
|
|||||||
void stopMusic(); // Aturar completament la música
|
void stopMusic(); // Aturar completament la música
|
||||||
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
|
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)
|
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 ---
|
// --- Control de sons ---
|
||||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
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)
|
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
|
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||||
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
||||||
// sec — l'usuari sent el so igualment, sense la cua.
|
// sec — l'usuari sent el so igualment, sense la cua.
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/audio/jail_audio.hpp"
|
#include "core/audio/jail_audio.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace AudioEffects {
|
namespace AudioEffects {
|
||||||
|
|
||||||
@@ -46,11 +46,11 @@ namespace AudioEffects {
|
|||||||
} else if (spec.format == SDL_AUDIO_U8) {
|
} else if (spec.format == SDL_AUDIO_U8) {
|
||||||
bytes_per_sample = 1;
|
bytes_per_sample = 1;
|
||||||
} else {
|
} 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;
|
return false;
|
||||||
}
|
}
|
||||||
if (spec.channels < 1 || spec.channels > 2) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,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() {
|
void Engine::pauseMusic() {
|
||||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,14 @@ namespace Ja {
|
|||||||
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
||||||
[[nodiscard]] auto getMusicState() const -> MusicState;
|
[[nodiscard]] auto getMusicState() const -> MusicState;
|
||||||
auto setMusicVolume(float volume) -> float;
|
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
|
// Registra un callback que es disparà quan la música actual acabi de
|
||||||
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
|
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
|
||||||
// stopMusic, així que el callback pot invocar playMusic sense córrer.
|
// stopMusic, així que el callback pot invocar playMusic sense córrer.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#include "core/audio/sound_effects_config.hpp"
|
#include "core/audio/sound_effects_config.hpp"
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
|
|||||||
void SoundEffectsConfig::load(const std::string& file_path) {
|
void SoundEffectsConfig::load(const std::string& file_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(file_path);
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
if (bytes.empty()) {
|
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;
|
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) {
|
} 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
|
#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
|
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
|
||||||
// preset no existeix, el so es reprodueix sec amb un avís a stderr.
|
// preset no existeix, el so es reprodueix sec amb un avís a stderr.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ auto GamepadBindings::get() -> GamepadBindings& {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto GamepadBindings::defaultButton(Action action) -> int {
|
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ú).
|
// i les consultes del Director per al menú).
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case Action::JUMP:
|
case Action::JUMP:
|
||||||
@@ -167,8 +167,7 @@ void GamepadBindings::load(const std::string& file_path) {
|
|||||||
try {
|
try {
|
||||||
auto yaml = fkyaml::node::deserialize(CONTENT);
|
auto yaml = fkyaml::node::deserialize(CONTENT);
|
||||||
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
|
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
|
||||||
std::cerr << "GamepadBindings: secció 'gamepads' absent o invàlida a "
|
Log::error("GamepadBindings", "secció 'gamepads' absent o invàlida a {}", file_path);
|
||||||
<< file_path << '\n';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const auto& entry : yaml["gamepads"]) {
|
for (const auto& entry : yaml["gamepads"]) {
|
||||||
@@ -188,11 +187,9 @@ void GamepadBindings::load(const std::string& file_path) {
|
|||||||
}
|
}
|
||||||
configs_.push_back(std::move(cfg));
|
configs_.push_back(std::move(cfg));
|
||||||
}
|
}
|
||||||
std::cout << "GamepadBindings: " << configs_.size() << " mandos carregats de "
|
Log::info("GamepadBindings", "{} mandos carregats de {}", configs_.size(), file_path);
|
||||||
<< file_path << '\n';
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "GamepadBindings: error parsejant " << file_path << ": "
|
Log::error("GamepadBindings", "error parsejant {}: {}", file_path, e.what());
|
||||||
<< e.what() << '\n';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +197,7 @@ auto GamepadBindings::save() -> bool {
|
|||||||
if (file_path_.empty()) { return false; }
|
if (file_path_.empty()) { return false; }
|
||||||
|
|
||||||
std::ostringstream out;
|
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 << "# Auto-generat des del menú de servei. Editable a mà.\n";
|
||||||
out << "gamepads:\n";
|
out << "gamepads:\n";
|
||||||
for (const auto& cfg : configs_) {
|
for (const auto& cfg : configs_) {
|
||||||
@@ -215,7 +212,7 @@ auto GamepadBindings::save() -> bool {
|
|||||||
|
|
||||||
std::ofstream file(file_path_, std::ios::binary);
|
std::ofstream file(file_path_, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
std::cerr << "GamepadBindings: no es pot escriure " << file_path_ << '\n';
|
Log::error("GamepadBindings", "no es pot escriure {}", file_path_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
file << out.str();
|
file << out.str();
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/notification_presets.hpp"
|
#include "core/rendering/notification_presets.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -94,8 +94,7 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
|
|||||||
if (!SDL_IsGamepad(jid)) { return; }
|
if (!SDL_IsGamepad(jid)) { return; }
|
||||||
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
|
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
|
||||||
if (pad == nullptr) {
|
if (pad == nullptr) {
|
||||||
std::cout << "GamepadManager: SDL_OpenGamepad(" << jid
|
Log::error("GamepadManager", "SDL_OpenGamepad({}) ha fallat: {}", jid, SDL_GetError());
|
||||||
<< ") ha fallat: " << SDL_GetError() << '\n';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad)));
|
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.
|
// reconnexió es resoldrà per nom.
|
||||||
const char* raw_path = SDL_GetGamepadPath(pad);
|
const char* raw_path = SDL_GetGamepadPath(pad);
|
||||||
std::string path = (raw_path != nullptr) ? raw_path : "";
|
std::string path = (raw_path != nullptr) ? raw_path : "";
|
||||||
std::cout << "GamepadManager: connectat " << name
|
Log::info("GamepadManager", "connectat {}{}", name, path.empty() ? std::string{} : " [" + path + "]");
|
||||||
<< (path.empty() ? "" : " [" + path + "]") << '\n';
|
|
||||||
entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name, .path = 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,
|
// 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);
|
std::string name = std::move(it->pretty_name);
|
||||||
SDL_Gamepad* pad = it->pad;
|
SDL_Gamepad* pad = it->pad;
|
||||||
if (pad != nullptr) { SDL_CloseGamepad(pad); }
|
if (pad != nullptr) { SDL_CloseGamepad(pad); }
|
||||||
std::cout << "GamepadManager: desconnectat " << name << '\n';
|
Log::info("GamepadManager", "desconnectat {}", name);
|
||||||
entries_.erase(it);
|
entries_.erase(it);
|
||||||
|
|
||||||
// Qualsevol assignació que apuntava a aquest mando queda amb `pad`
|
// Qualsevol assignació que apuntava a aquest mando queda amb `pad`
|
||||||
@@ -145,8 +143,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
|
|||||||
void GamepadManager::init() {
|
void GamepadManager::init() {
|
||||||
if (initialized_) { return; }
|
if (initialized_) { return; }
|
||||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||||
std::cout << "GamepadManager: SDL_INIT_GAMEPAD ha fallat: "
|
Log::error("GamepadManager", "SDL_INIT_GAMEPAD ha fallat: {}", SDL_GetError());
|
||||||
<< SDL_GetError() << '\n';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// `gamecontrollerdb.txt` és opcional. Viu a l'arrel del projecte
|
// `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());
|
const int ADDED = SDL_AddGamepadMappingsFromFile(db_path.c_str());
|
||||||
if (ADDED < 0) {
|
if (ADDED < 0) {
|
||||||
std::cout << "GamepadManager: no s'ha pogut carregar " << db_path
|
Log::warn("GamepadManager", "no s'ha pogut carregar {}: {}", db_path, SDL_GetError());
|
||||||
<< ": " << SDL_GetError() << '\n';
|
|
||||||
} else {
|
} else {
|
||||||
std::cout << "GamepadManager: " << ADDED << " mappings carregats de " << db_path << '\n';
|
Log::info("GamepadManager", "{} mappings carregats de {}", ADDED, db_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@@ -175,7 +171,7 @@ void GamepadManager::init() {
|
|||||||
}
|
}
|
||||||
SDL_free(ids);
|
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;
|
initialized_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,8 +264,7 @@ void GamepadManager::assignToPlayer(int player_id, const std::string& name, cons
|
|||||||
if (a.pad != nullptr) {
|
if (a.pad != nullptr) {
|
||||||
for (auto& other : assignments_) {
|
for (auto& other : assignments_) {
|
||||||
if (&other != &a && other.pad == a.pad) {
|
if (&other != &a && other.pad == a.pad) {
|
||||||
std::cout << "GamepadManager: mando " << a.name
|
Log::info("GamepadManager", "mando {} reassignat (last-wins)", a.name);
|
||||||
<< " reassignat (last-wins)\n";
|
|
||||||
other.pad = nullptr;
|
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
|
// Traç de l'assignació persistida (vinculada o pendent) perquè quede
|
||||||
// visible a la consola igual que les autoassignacions.
|
// visible a la consola igual que les autoassignacions.
|
||||||
if (a.pad != nullptr) {
|
if (a.pad != nullptr) {
|
||||||
std::cout << "GamepadManager: assignat " << a.name
|
Log::info("GamepadManager", "assignat {} → P{}", a.name, player_id + 1);
|
||||||
<< " → P" << (player_id + 1) << '\n';
|
|
||||||
} else {
|
} else {
|
||||||
std::cout << "GamepadManager: P" << (player_id + 1) << " pendent ("
|
Log::info("GamepadManager", "P{} pendent ({} no connectat)", player_id + 1, a.name.empty() ? a.path : a.name);
|
||||||
<< (a.name.empty() ? a.path : a.name) << " no connectat)\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,8 +300,7 @@ auto GamepadManager::autoAssignFreeSlots() -> std::vector<AutoAssigned> {
|
|||||||
slot.name = e.pretty_name;
|
slot.name = e.pretty_name;
|
||||||
slot.path = e.path;
|
slot.path = e.path;
|
||||||
assigned.push_back(AutoAssigned{.player_id = pid, .name = e.pretty_name, .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
|
Log::info("GamepadManager", "auto-assigned {} → P{}", e.pretty_name, pid + 1);
|
||||||
<< " → P" << (pid + 1) << '\n';
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace GlobalInputs {
|
|||||||
// La lliguem a la durada de la notificació EXIT perquè el missatge i
|
// La lliguem a la durada de la notificació EXIT perquè el missatge i
|
||||||
// la confirmació expiren alhora — si no, l'usuari podria veure el
|
// la confirmació expiren alhora — si no, l'usuari podria veure el
|
||||||
// text però ja no ser dins del rang de confirmació (o al revés).
|
// text però ja no ser dins del rang de confirmació (o al revés).
|
||||||
// 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 MIN_ZOOM = 1;
|
||||||
constexpr int MAX_ZOOM = 6;
|
constexpr int MAX_ZOOM = 6;
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ namespace GlobalInputs {
|
|||||||
// Debug helper: dispara una notificació prou llarga perquè
|
// Debug helper: dispara una notificació prou llarga perquè
|
||||||
// `wrapToWidth` la parteixi en (almenys) dues línies. Útil per a
|
// `wrapToWidth` la parteixi en (almenys) dues línies. Útil per a
|
||||||
// verificar visualment el padding asimètric, el line_gap i la caixa
|
// 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() {
|
void onDebugLongNotification() {
|
||||||
Notification::notify("Notificació de prova llarga per a comprovar que el text es parteix correctament en dues línies dins la caixa.");
|
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 "core/input/key_config.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
auto KeyConfig::get() -> KeyConfig& {
|
auto KeyConfig::get() -> KeyConfig& {
|
||||||
static KeyConfig instance_;
|
static KeyConfig instance_;
|
||||||
@@ -15,7 +15,7 @@ auto KeyConfig::get() -> KeyConfig& {
|
|||||||
void KeyConfig::load(const std::string& file_path) {
|
void KeyConfig::load(const std::string& file_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(file_path);
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
if (bytes.empty()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ void KeyConfig::load(const std::string& file_path) {
|
|||||||
const auto* end = begin + bytes.size();
|
const auto* end = begin + bytes.size();
|
||||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
|
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
|
||||||
std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n';
|
Log::error("KeyConfig", "falta la secció 'keys' a {}", file_path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bindings_.clear();
|
bindings_.clear();
|
||||||
@@ -34,14 +34,14 @@ void KeyConfig::load(const std::string& file_path) {
|
|||||||
const auto CODE = entry["code"].get_value<std::string>();
|
const auto CODE = entry["code"].get_value<std::string>();
|
||||||
const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str());
|
const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str());
|
||||||
if (SC == SDL_SCANCODE_UNKNOWN) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
bindings_[ID] = SC;
|
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) {
|
} 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 <string>
|
||||||
#include <unordered_map>
|
#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
|
// 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
|
// 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
|
// ignora l'acció. D'aquesta manera podem afegir nous bindings al YAML sense
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ auto Locale::get() -> Locale& {
|
|||||||
void Locale::load(const std::string& file_path) {
|
void Locale::load(const std::string& file_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(file_path);
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
if (bytes.empty()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,15 +46,15 @@ void Locale::load(const std::string& file_path) {
|
|||||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
strings_.clear();
|
strings_.clear();
|
||||||
flatten(yaml, "", strings_);
|
flatten(yaml, "", strings_);
|
||||||
std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n';
|
Log::info("Locale", "{} traduccions des de {}", strings_.size(), file_path);
|
||||||
} catch (const std::exception& e) {
|
} 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 Locale::text(const std::string& key) const -> std::string {
|
||||||
auto it = strings_.find(key);
|
auto it = strings_.find(key);
|
||||||
if (it != strings_.end()) { return it->second; }
|
if (it != strings_.end()) { return it->second; }
|
||||||
std::cerr << "Locale: clau no trobada: " << key << '\n';
|
Log::warn("Locale", "clau no trobada: {}", key);
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
#include "core/rendering/gif.hpp"
|
#include "core/rendering/gif.hpp"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace GIF {
|
namespace GIF {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -111,7 +112,7 @@ namespace GIF {
|
|||||||
while (cur_code != -1) {
|
while (cur_code != -1) {
|
||||||
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
|
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
|
||||||
if (dictionary[cur_code].prev == cur_code) {
|
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");
|
throw std::runtime_error("Internal error in decompress: self-reference");
|
||||||
}
|
}
|
||||||
cur_code = dictionary[cur_code].prev;
|
cur_code = dictionary[cur_code].prev;
|
||||||
@@ -155,8 +156,7 @@ namespace GIF {
|
|||||||
|
|
||||||
if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) {
|
if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) {
|
||||||
if (code > dictionary_ind) {
|
if (code > dictionary_ind) {
|
||||||
std::cerr << "code = " << std::hex << code
|
Log::error("GIF", "code = {:x}, but dictionary_ind = {}", code, dictionary_ind);
|
||||||
<< ", but dictionary_ind = " << dictionary_ind << '\n';
|
|
||||||
throw std::runtime_error("LZW error: code exceeds 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.
|
// Verifica que 'code' sea un índice válido antes de usarlo.
|
||||||
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
|
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
|
||||||
std::cerr << "Invalid LZW code " << code
|
Log::error("GIF", "invalid LZW code {}, dictionary size {}", code, dictionary.size());
|
||||||
<< ", dictionary size " << dictionary.size() << '\n';
|
|
||||||
throw std::runtime_error("LZW error: invalid code encountered");
|
throw std::runtime_error("LZW error: invalid code encountered");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +286,7 @@ namespace GIF {
|
|||||||
} else if (block_type == IMAGE_DESCRIPTOR) {
|
} else if (block_type == IMAGE_DESCRIPTOR) {
|
||||||
return processImageDescriptor(buffer);
|
return processImageDescriptor(buffer);
|
||||||
} else {
|
} 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>{};
|
return std::vector<uint8_t>{};
|
||||||
}
|
}
|
||||||
block_type = *buffer++;
|
block_type = *buffer++;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
// Temes per a les notificacions de pantalla. Un "preset" captura els dos
|
// Temes per a les notificacions de pantalla. Un "preset" captura els dos
|
||||||
// colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
|
// colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
|
||||||
// en mil·lisegons. 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
|
// UiConfig); per llançar una notificació amb un preset concret, cridar
|
||||||
// Notification::notify(text, preset). Sense preset s'usa el DEFAULT.
|
// Notification::notify(text, preset). Sense preset s'usa el DEFAULT.
|
||||||
namespace Notification {
|
namespace Notification {
|
||||||
@@ -22,7 +22,7 @@ namespace Notification {
|
|||||||
|
|
||||||
// Preset per defecte: text clar sobre fons fosc. Es construeix en cada
|
// 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
|
// 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 {
|
[[nodiscard]] inline auto defaultPreset() -> Preset {
|
||||||
return {
|
return {
|
||||||
.text_color = UiConfig::get().notificationText(),
|
.text_color = UiConfig::get().notificationText(),
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
|
||||||
#include "core/rendering/canvas.hpp"
|
#include "core/rendering/canvas.hpp"
|
||||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -112,11 +112,11 @@ void Screen::createSdl() {
|
|||||||
backend->setVSync(config_.vsync);
|
backend->setVSync(config_.vsync);
|
||||||
if (backend->init(window_, canvas_width_, canvas_height_)) {
|
if (backend->init(window_, canvas_width_, canvas_height_)) {
|
||||||
gpu_driver_ = backend->getDriverName();
|
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);
|
shader_backend_ = std::move(backend);
|
||||||
return;
|
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";
|
gpu_driver_ = "sdl";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp"
|
#include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "core/rendering/sdl3gpu/shader_factory.hpp"
|
#include "core/rendering/sdl3gpu/shader_factory.hpp"
|
||||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
|
#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);
|
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
#endif
|
#endif
|
||||||
if (vert == nullptr || frag == nullptr) {
|
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 (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
|
||||||
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
|
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
|
||||||
return false;
|
return false;
|
||||||
@@ -57,7 +57,7 @@ namespace Rendering {
|
|||||||
SDL_ReleaseGPUShader(device_, frag);
|
SDL_ReleaseGPUShader(device_, frag);
|
||||||
|
|
||||||
if (pipeline_ == nullptr) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "core/rendering/sdl3gpu/shader_factory.hpp"
|
#include "core/rendering/sdl3gpu/shader_factory.hpp"
|
||||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
|
#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();
|
const char* driver = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
|
||||||
device_ = SDL_CreateGPUDevice(FORMAT, false, driver);
|
device_ = SDL_CreateGPUDevice(FORMAT, false, driver);
|
||||||
if (device_ == nullptr) {
|
if (device_ == nullptr) {
|
||||||
std::cerr << "SDL3GPUShader: SDL_CreateGPUDevice ha fallat: " << SDL_GetError() << '\n';
|
Log::error("SDL3GPUShader", "SDL_CreateGPUDevice ha fallat: {}", SDL_GetError());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
|
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);
|
fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
#endif
|
#endif
|
||||||
if (vertex_shader_ == nullptr || fragment_shader_ == nullptr) {
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -105,7 +105,7 @@ namespace Rendering {
|
|||||||
|
|
||||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
|
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
|
||||||
if (pipeline_ == nullptr) {
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -137,7 +137,7 @@ namespace Rendering {
|
|||||||
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
|
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
|
||||||
|
|
||||||
if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) {
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iostream>
|
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
// El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers)
|
// El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers)
|
||||||
// viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni
|
// 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); }
|
if (!first.frames.empty()) { setClip(first.frames[0].rect); }
|
||||||
}
|
}
|
||||||
} catch (const fkyaml::exception& e) {
|
} 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;
|
throw;
|
||||||
} catch (const std::exception& e) {
|
} 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;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
|||||||
if (current_animation_ == name) { return; }
|
if (current_animation_ == name) { return; }
|
||||||
auto it = animations_.find(name);
|
auto it = animations_.find(name);
|
||||||
if (it == animations_.end()) {
|
if (it == animations_.end()) {
|
||||||
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
|
Log::warn("AnimatedSprite", "could not find \"{}\" animation", name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
current_animation_ = name;
|
current_animation_ = name;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ Surface::Surface(const std::string& file_path) {
|
|||||||
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
|
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]");
|
printWithDots("Surface", getFileName(file_path), "[ LOADED ]");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Surface::clear(Uint8 color) {
|
void Surface::clear(Uint8 color) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -11,6 +10,7 @@
|
|||||||
#include "core/rendering/sprite/sprite.hpp"
|
#include "core/rendering/sprite/sprite.hpp"
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
#include "utils/utils.hpp"
|
#include "utils/utils.hpp"
|
||||||
|
|
||||||
// Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte
|
// 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);
|
auto file_data = Resource::Helper::loadFile(file_path);
|
||||||
if (file_data.empty()) {
|
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));
|
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;
|
return tf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
#include "core/audio/jail_audio.hpp"
|
#include "core/audio/jail_audio.hpp"
|
||||||
#include "core/resources/palette_loader.hpp"
|
#include "core/resources/palette_loader.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
#include "utils/utils.hpp"
|
#include "utils/utils.hpp"
|
||||||
|
|
||||||
// Deleters dels recursos d'àudio: necessiten veure el tipus complet de
|
// 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);
|
throw std::runtime_error("Cache: cannot load animation " + file_path);
|
||||||
}
|
}
|
||||||
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
|
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) {
|
void Cache::loadPalette(const std::string& name, const std::string& file_path) {
|
||||||
auto palette = PaletteLoader::loadFromFile(file_path);
|
auto palette = PaletteLoader::loadFromFile(file_path);
|
||||||
palettes_.emplace_back(PaletteResource{.name = name, .palette = palette});
|
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 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; });
|
auto it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; });
|
||||||
if (it == surfaces_.end()) {
|
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);
|
throw std::runtime_error("Surface not found: " + name);
|
||||||
}
|
}
|
||||||
return it->surface;
|
return it->surface;
|
||||||
@@ -58,7 +58,7 @@ namespace Resource {
|
|||||||
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& {
|
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& {
|
||||||
auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; });
|
auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; });
|
||||||
if (it == animations_.end()) {
|
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);
|
throw std::runtime_error("Animation not found: " + name);
|
||||||
}
|
}
|
||||||
return *it;
|
return *it;
|
||||||
@@ -67,7 +67,7 @@ namespace Resource {
|
|||||||
auto Cache::getPalette(const std::string& name) -> const Palette& {
|
auto Cache::getPalette(const std::string& name) -> const Palette& {
|
||||||
auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; });
|
auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; });
|
||||||
if (it == palettes_.end()) {
|
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);
|
throw std::runtime_error("Palette not found: " + name);
|
||||||
}
|
}
|
||||||
return it->palette;
|
return it->palette;
|
||||||
@@ -83,7 +83,7 @@ namespace Resource {
|
|||||||
throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path);
|
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)});
|
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) {
|
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);
|
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)});
|
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 Cache::getMusic(const std::string& name) -> Ja::Music* {
|
||||||
auto it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; });
|
auto it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; });
|
||||||
if (it == musics_.end()) {
|
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);
|
throw std::runtime_error("Music not found: " + name);
|
||||||
}
|
}
|
||||||
return it->music.get();
|
return it->music.get();
|
||||||
@@ -111,7 +111,7 @@ namespace Resource {
|
|||||||
auto Cache::getSound(const std::string& name) -> Ja::Sound* {
|
auto Cache::getSound(const std::string& name) -> Ja::Sound* {
|
||||||
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; });
|
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; });
|
||||||
if (it == sounds_.end()) {
|
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);
|
throw std::runtime_error("Sound not found: " + name);
|
||||||
}
|
}
|
||||||
return it->sound.get();
|
return it->sound.get();
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include "core/resources/resource_pack.hpp"
|
#include "core/resources/resource_pack.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace Resource::Helper {
|
namespace Resource::Helper {
|
||||||
|
|
||||||
@@ -26,14 +27,14 @@ namespace Resource::Helper {
|
|||||||
// Silenciem si veníem de prefer_disk: aleshores caurem
|
// Silenciem si veníem de prefer_disk: aleshores caurem
|
||||||
// al pack i no és cap error real que el fitxer no estigui
|
// al pack i no és cap error real que el fitxer no estigui
|
||||||
// al disc.
|
// al disc.
|
||||||
if (verbose) { std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; }
|
if (verbose) { Log::error("Resource::Helper", "cannot open {}", FULL); }
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::streamsize size = file.tellg();
|
std::streamsize size = file.tellg();
|
||||||
file.seekg(0, std::ios::beg);
|
file.seekg(0, std::ios::beg);
|
||||||
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
|
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
|
||||||
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
||||||
std::cerr << "Resource::Helper: failed to read " << FULL << '\n';
|
Log::error("Resource::Helper", "failed to read {}", FULL);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
@@ -51,14 +52,11 @@ namespace Resource::Helper {
|
|||||||
g_pack_loaded = g_pack.loadPack(pack_file);
|
g_pack_loaded = g_pack.loadPack(pack_file);
|
||||||
|
|
||||||
if (g_pack_loaded) {
|
if (g_pack_loaded) {
|
||||||
std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount()
|
Log::info("Resource::Helper", "pack loaded ({} entries) from {}", g_pack.getResourceCount(), pack_file);
|
||||||
<< " entries) from " << pack_file << '\n';
|
|
||||||
} else if (enable_fallback) {
|
} else if (enable_fallback) {
|
||||||
std::cout << "Resource::Helper: no pack at " << pack_file
|
Log::info("Resource::Helper", "no pack at {} — using filesystem fallback", pack_file);
|
||||||
<< " — using filesystem fallback\n";
|
|
||||||
} else {
|
} else {
|
||||||
std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file
|
Log::critical("Resource::Helper", "FATAL — no pack at {} and fallback disabled", pack_file);
|
||||||
<< " and fallback disabled\n";
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -92,7 +90,7 @@ namespace Resource::Helper {
|
|||||||
// carpeta `data/`).
|
// carpeta `data/`).
|
||||||
const auto FROM_PACK = [&filepath]() -> std::vector<std::uint8_t> {
|
const auto FROM_PACK = [&filepath]() -> std::vector<std::uint8_t> {
|
||||||
if (!g_pack_loaded) { return {}; }
|
if (!g_pack_loaded) { return {}; }
|
||||||
constexpr std::string_view PREFIX = "data/";
|
const std::string_view PREFIX{Defaults::Paths::DATA_ROOT};
|
||||||
std::string key = filepath;
|
std::string key = filepath;
|
||||||
if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); }
|
if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); }
|
||||||
if (g_pack.hasResource(key)) { return g_pack.getResource(key); }
|
if (g_pack.hasResource(key)) { return g_pack.getResource(key); }
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace Resource {
|
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 {
|
namespace {
|
||||||
constexpr const char* MAGIC = "AE26";
|
constexpr const char* MAGIC = "AE26";
|
||||||
@@ -40,14 +41,14 @@ namespace Resource {
|
|||||||
std::array<char, 4> header{};
|
std::array<char, 4> header{};
|
||||||
file.read(header.data(), 4);
|
file.read(header.data(), 4);
|
||||||
if (std::string(header.data(), 4) != MAGIC) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint32_t version = 0;
|
std::uint32_t version = 0;
|
||||||
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||||
if (version != VERSION) {
|
if (version != VERSION) {
|
||||||
std::cerr << "Resource::Pack: unsupported pack version: " << version << '\n';
|
Log::error("Resource::Pack", "unsupported pack version: {}", version);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ namespace Resource {
|
|||||||
auto Pack::savePack(const std::string& pack_file) const -> bool {
|
auto Pack::savePack(const std::string& pack_file) const -> bool {
|
||||||
std::ofstream file(pack_file, std::ios::binary);
|
std::ofstream file(pack_file, std::ios::binary);
|
||||||
if (!file) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ namespace Resource {
|
|||||||
auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool {
|
auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool {
|
||||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
std::cerr << "Resource::Pack: could not open file: " << filepath << '\n';
|
Log::error("Resource::Pack", "could not open file: {}", filepath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ namespace Resource {
|
|||||||
|
|
||||||
std::vector<std::uint8_t> file_data(file_size);
|
std::vector<std::uint8_t> file_data(file_size);
|
||||||
if (!file.read(reinterpret_cast<char*>(file_data.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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ namespace Resource {
|
|||||||
|
|
||||||
auto Pack::addDirectory(const std::string& directory) -> bool {
|
auto Pack::addDirectory(const std::string& directory) -> bool {
|
||||||
if (!std::filesystem::exists(directory)) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +170,7 @@ namespace Resource {
|
|||||||
|
|
||||||
const PackEntry& entry = it->second;
|
const PackEntry& entry = it->second;
|
||||||
if (entry.offset + entry.size > data_.size()) {
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +180,7 @@ namespace Resource {
|
|||||||
|
|
||||||
std::uint32_t checksum = calculateChecksum(result);
|
std::uint32_t checksum = calculateChecksum(result);
|
||||||
if (checksum != entry.checksum) {
|
if (checksum != entry.checksum) {
|
||||||
std::cerr << "Resource::Pack: checksum mismatch for: " << filename << '\n';
|
Log::error("Resource::Pack", "checksum mismatch for: {}", filename);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <any>
|
#include <any>
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iostream>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -31,6 +29,7 @@
|
|||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/debug_config.hpp"
|
#include "game/debug_config.hpp"
|
||||||
|
#include "game/gameplay_config.hpp"
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
#include "game/editor/editor_config.hpp"
|
#include "game/editor/editor_config.hpp"
|
||||||
#endif
|
#endif
|
||||||
@@ -42,10 +41,11 @@
|
|||||||
#include "game/scenes/title_scene.hpp"
|
#include "game/scenes/title_scene.hpp"
|
||||||
#include "game/ui/service_menu.hpp"
|
#include "game/ui/service_menu.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
Director::Director() {
|
Director::Director() {
|
||||||
if (!SDL_Init(0)) {
|
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
|
// Sistema de recursos: el pack viu al costat del binari (build/), el
|
||||||
@@ -66,12 +66,13 @@ Director::Director() {
|
|||||||
constexpr bool ENABLE_FALLBACK = false;
|
constexpr bool ENABLE_FALLBACK = false;
|
||||||
#endif
|
#endif
|
||||||
if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) {
|
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);
|
std::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega
|
// Prepara la carpeta de sistema (per plataforma: %APPDATA% a Windows,
|
||||||
// les opcions; si no existeix config.yaml el crea amb els defaults.
|
// ~/Library/Application Support a macOS, ~/.config a Linux) i carrega les
|
||||||
|
// opcions; si no existeix config.yaml el crea amb els defaults.
|
||||||
createSystemFolder();
|
createSystemFolder();
|
||||||
|
|
||||||
// Gamepad: descobreix mandos presents silenciosament. Cal fer-ho ABANS
|
// Gamepad: descobreix mandos presents silenciosament. Cal fer-ho ABANS
|
||||||
@@ -85,7 +86,7 @@ Director::Director() {
|
|||||||
|
|
||||||
Options::setConfigFile(system_folder_ + "config.yaml");
|
Options::setConfigFile(system_folder_ + "config.yaml");
|
||||||
if (!Options::loadFromFile()) {
|
if (!Options::loadFromFile()) {
|
||||||
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
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`
|
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
|
||||||
@@ -93,16 +94,21 @@ Director::Director() {
|
|||||||
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
|
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
|
||||||
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
|
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
|
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
|
||||||
// es crea amb presets per defecte (off, crt, ntsc).
|
// es crea amb presets per defecte (off, crt, ntsc).
|
||||||
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
||||||
if (!Options::loadPostFXFromFile()) {
|
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).
|
// Presets del shader CRT-Pi (shaders/crtpi.yaml).
|
||||||
Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE);
|
Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE);
|
||||||
if (!Options::loadCrtPiFromFile()) {
|
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
|
// Screen es configura ara (subsystem + finestra + GPU/renderer); la seua
|
||||||
@@ -118,22 +124,29 @@ Director::Director() {
|
|||||||
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml");
|
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml");
|
||||||
|
|
||||||
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
|
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
|
||||||
KeyConfig::get().load("data/input/keys.yaml");
|
KeyConfig::get().load(Defaults::Paths::INPUT_YAML);
|
||||||
|
|
||||||
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
|
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
|
||||||
// data/ui.yaml cau al valor compilat a Defaults::UI.
|
// data/config/ui.yaml cau al valor compilat a Defaults::UI.
|
||||||
UiConfig::get().load("data/ui.yaml");
|
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
|
#ifdef _DEBUG
|
||||||
// Tema visual de l'editor de tilemap (Debug-only). Claus absents
|
// Tema visual de l'editor de tilemap (Debug-only). Claus absents
|
||||||
// cauen als valors compilats a Defaults::Editor.
|
// cauen als valors compilats a Defaults::Editor.
|
||||||
EditorConfig::get().load("data/editor/editor.yaml");
|
EditorConfig::get().load(Defaults::Paths::EDITOR_YAML);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
|
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
|
||||||
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
|
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
|
||||||
// noms de font viuen al fitxer data/ui.yaml via UiConfig.
|
// noms de font viuen al fitxer data/config/ui.yaml via UiConfig.
|
||||||
Resource::Cache::get().loadPalette("ui", "data/palettes/uzebox.pal");
|
Resource::Cache::get().loadPalette("ui", Defaults::Paths::UI_PALETTE);
|
||||||
const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif";
|
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().loadSurface(HUD_FONT_GIF_KEY, UiConfig::get().hudFont().gif);
|
||||||
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||||
@@ -152,33 +165,36 @@ Director::Director() {
|
|||||||
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden
|
// 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;
|
// 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`.
|
// 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("room_intro.ogg", Defaults::Paths::MUSIC_ROOM_INTRO);
|
||||||
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg");
|
Resource::Cache::get().loadMusic("playing.ogg", Defaults::Paths::MUSIC_PLAYING);
|
||||||
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg");
|
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
|
// Música de la TITLE: arrenca al constructor de l'escena i es fa
|
||||||
// fade-out quan l'escena entra a STARTING.
|
// fade-out quan l'escena entra a STARTING.
|
||||||
Resource::Cache::get().loadMusic("title.ogg", "data/music/title.ogg");
|
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
|
// fallback a `Defaults::UI::MENU_*_SOUND`); el ServiceMenu els
|
||||||
// dispara via Audio::Group::INTERFACE.
|
// dispara via Audio::Group::INTERFACE.
|
||||||
const auto& menu_select = UiConfig::get().menuSelectSound();
|
const auto& menu_select = UiConfig::get().menuSelectSound();
|
||||||
const auto& menu_accept = UiConfig::get().menuAcceptSound();
|
const auto& menu_accept = UiConfig::get().menuAcceptSound();
|
||||||
Resource::Cache::get().loadSound(menu_select, "data/sounds/" + menu_select);
|
Resource::Cache::get().loadSound(menu_select, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + menu_select);
|
||||||
Resource::Cache::get().loadSound(menu_accept, "data/sounds/" + menu_accept);
|
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
|
// So del START a la TITLE: el dispara TitleScene quan un jugador
|
||||||
// prem START (vore enterStarting i el reset del segon jugador).
|
// prem START (vore enterStarting i el reset del segon jugador).
|
||||||
const auto& player_start = UiConfig::get().playerStartSound();
|
const auto& player_start = UiConfig::get().playerStartSound();
|
||||||
Resource::Cache::get().loadSound(player_start, "data/sounds/" + player_start);
|
Resource::Cache::get().loadSound(player_start, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + player_start);
|
||||||
// Soroll de LogoScene (scanline reveal). El dispara LogoScene al
|
// Soroll de LogoScene (scanline reveal). El dispara LogoScene al
|
||||||
// ctor; amb fallback al nom de Defaults si la clau YAML no hi és.
|
// ctor; amb fallback al nom de Defaults si la clau YAML no hi és.
|
||||||
const auto& logo_glitch = UiConfig::get().logoGlitchSound();
|
const auto& logo_glitch = UiConfig::get().logoGlitchSound();
|
||||||
Resource::Cache::get().loadSound(logo_glitch, "data/sounds/" + logo_glitch);
|
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
|
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
|
||||||
// perquè els primers playSoundWith* (al començament del joc) ja
|
// perquè els primers playSoundWith* (al començament del joc) ja
|
||||||
// troben els presets disponibles.
|
// 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
|
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
|
||||||
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
||||||
@@ -216,7 +232,7 @@ Director::Director() {
|
|||||||
|
|
||||||
// Font del menú de servei (F9). El títol va amb la variant `display`
|
// Font del menú de servei (F9). El títol va amb la variant `display`
|
||||||
// i els items amb `condensed-display` (més compacte perquè caben més
|
// i els items amb `condensed-display` (més compacte perquè caben més
|
||||||
// files). Els dos noms i kernings viuen a 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) {
|
auto load_menu_font = [&](const UiConfig::Font& font) {
|
||||||
const std::string KEY = font.name + ".gif";
|
const std::string KEY = font.name + ".gif";
|
||||||
Resource::Cache::get().loadSurface(KEY, font.gif);
|
Resource::Cache::get().loadSurface(KEY, font.gif);
|
||||||
@@ -233,7 +249,7 @@ Director::Director() {
|
|||||||
// sobreescrivim amb la "ui" perquè el GIF ja porta els colors finals
|
// sobreescrivim amb la "ui" perquè el GIF ja porta els colors finals
|
||||||
// del logo).
|
// del logo).
|
||||||
constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif";
|
constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif";
|
||||||
Resource::Cache::get().loadSurface(MINI_LOGO_KEY, "data/logo/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);
|
auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY);
|
||||||
mini_logo_surface_ = mini_logo_surface;
|
mini_logo_surface_ = mini_logo_surface;
|
||||||
|
|
||||||
@@ -241,20 +257,20 @@ Director::Director() {
|
|||||||
// conserva la seua paleta nadiua: els colors finals del logo viuen
|
// conserva la seua paleta nadiua: els colors finals del logo viuen
|
||||||
// dins del propi GIF i no han de passar per la paleta UI.
|
// dins del propi GIF i no han de passar per la paleta UI.
|
||||||
constexpr const char* LOGO_FULL_KEY = "logo_jailgames.gif";
|
constexpr const char* LOGO_FULL_KEY = "logo_jailgames.gif";
|
||||||
Resource::Cache::get().loadSurface(LOGO_FULL_KEY, "data/logo/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);
|
logo_full_surface_ = Resource::Cache::get().getSurface(LOGO_FULL_KEY);
|
||||||
|
|
||||||
// Logotip del joc per a la TITLE (substitueix el text "Aventures en
|
// Logotip del joc per a la TITLE (substitueix el text "Aventures en
|
||||||
// Egipte ARCADE"). Igual que la resta de logos, paleta nadiua —
|
// Egipte ARCADE"). Igual que la resta de logos, paleta nadiua —
|
||||||
// l'art ja porta els colors finals i no passa per la UI.
|
// l'art ja porta els colors finals i no passa per la UI.
|
||||||
constexpr const char* GAME_LOGO_KEY = "game_logo.gif";
|
constexpr const char* GAME_LOGO_KEY = "game_logo.gif";
|
||||||
Resource::Cache::get().loadSurface(GAME_LOGO_KEY, "data/title/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);
|
game_logo_surface_ = Resource::Cache::get().getSurface(GAME_LOGO_KEY);
|
||||||
|
|
||||||
// Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva
|
// Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva
|
||||||
// la seua paleta nadiua perquè el GIF ja porta els colors finals.
|
// la seua paleta nadiua perquè el GIF ja porta els colors finals.
|
||||||
constexpr const char* TITLE_BG_KEY = "title_bg.gif";
|
constexpr const char* TITLE_BG_KEY = "title_bg.gif";
|
||||||
Resource::Cache::get().loadSurface(TITLE_BG_KEY, "data/title/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);
|
auto title_bg_surface = Resource::Cache::get().getSurface(TITLE_BG_KEY);
|
||||||
title_bg_surface_ = title_bg_surface;
|
title_bg_surface_ = title_bg_surface;
|
||||||
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
|
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
|
||||||
@@ -288,8 +304,7 @@ Director::Director() {
|
|||||||
const auto START = DebugConfig::StartScene::LOGO;
|
const auto START = DebugConfig::StartScene::LOGO;
|
||||||
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
||||||
#endif
|
#endif
|
||||||
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
Log::info("Director", "start_scene={} starting_players={}", DebugConfig::toString(START), DebugConfig::toString(STARTING_PLAYERS));
|
||||||
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
|
|
||||||
|
|
||||||
switch (START) {
|
switch (START) {
|
||||||
case DebugConfig::StartScene::LOGO:
|
case DebugConfig::StartScene::LOGO:
|
||||||
@@ -326,25 +341,37 @@ Director::~Director() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Director::createSystemFolder() {
|
void Director::createSystemFolder() {
|
||||||
// Volem la config a $XDG_CONFIG_HOME/jailgames/aee_2026/ (defecte
|
// Carpeta de configuració per plataforma:
|
||||||
// ~/.config/jailgames/aee_2026/). SDL_GetPrefPath apunta a XDG_DATA_HOME
|
// - Windows: %APPDATA%\jailgames\aee_arcade\
|
||||||
// (.local/share), que és el lloc per a dades, no per a configuració.
|
// - macOS: ~/Library/Application Support/jailgames/aee_arcade/
|
||||||
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
// - 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;
|
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') {
|
if (xdg != nullptr && *xdg != '\0') {
|
||||||
base = xdg;
|
base = xdg;
|
||||||
} else {
|
} else {
|
||||||
const char* home = std::getenv("HOME");
|
const char* home = std::getenv("HOME");
|
||||||
base = (home != nullptr) ? std::string(home) + "/.config" : ".";
|
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::error_code error;
|
||||||
std::filesystem::create_directories(system_folder_, error);
|
std::filesystem::create_directories(system_folder_, error);
|
||||||
// Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix.
|
// Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix.
|
||||||
std::filesystem::create_directories(system_folder_ + "shaders/", error);
|
std::filesystem::create_directories(system_folder_ + "shaders/", error);
|
||||||
if (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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +453,7 @@ void Director::switchSceneIfRequested() {
|
|||||||
// l'escena en aquest mateix frame — l'usuari ha cancel·lat l'estat
|
// 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.
|
// de joc explícitament i no ens interessa mantenir-ne la sortida.
|
||||||
if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) {
|
if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) {
|
||||||
std::cout << "Director: scene transition (menu) → " << toString(menu_req->next) << '\n';
|
Log::info("Director", "scene transition (menu) → {}", toString(menu_req->next));
|
||||||
current_scene_ = buildScene(menu_req->next, menu_req->params);
|
current_scene_ = buildScene(menu_req->next, menu_req->params);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -438,10 +465,10 @@ void Director::switchSceneIfRequested() {
|
|||||||
if (!request.has_value()) { return; }
|
if (!request.has_value()) { return; }
|
||||||
try {
|
try {
|
||||||
const auto& req = std::any_cast<const ExitRequest&>(request);
|
const auto& req = std::any_cast<const ExitRequest&>(request);
|
||||||
std::cout << "Director: scene transition → " << toString(req.next) << '\n';
|
Log::info("Director", "scene transition → {}", toString(req.next));
|
||||||
current_scene_ = buildScene(req.next, req.params);
|
current_scene_ = buildScene(req.next, req.params);
|
||||||
} catch (const std::bad_any_cast&) {
|
} catch (const std::bad_any_cast&) {
|
||||||
std::cerr << "Director: exit_request_ amb tipus inesperat — ignorant\n";
|
Log::error("Director", "exit_request_ amb tipus inesperat — ignorant");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -9,6 +8,7 @@
|
|||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/easing.hpp"
|
#include "utils/easing.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ namespace {
|
|||||||
if (!node.contains(key)) { return; }
|
if (!node.contains(key)) { return; }
|
||||||
const int VALUE = node[key].get_value<int>();
|
const int VALUE = node[key].get_value<int>();
|
||||||
if (VALUE < 0 || VALUE > 255) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
dst = static_cast<Uint8>(VALUE);
|
dst = static_cast<Uint8>(VALUE);
|
||||||
@@ -183,7 +183,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
|
|||||||
const auto& reg = curveRegistry();
|
const auto& reg = curveRegistry();
|
||||||
const auto IT = reg.find(name);
|
const auto IT = reg.find(name);
|
||||||
if (IT == reg.end()) {
|
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 &Easing::linear;
|
||||||
}
|
}
|
||||||
return IT->second;
|
return IT->second;
|
||||||
@@ -192,7 +192,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
|
|||||||
void UiConfig::load(const std::string& file_path) {
|
void UiConfig::load(const std::string& file_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(file_path);
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
if (bytes.empty()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,8 +345,8 @@ void UiConfig::load(const std::string& file_path) {
|
|||||||
readCurve(n, "slide_out_curve", notifier_.slide_out_curve);
|
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) {
|
} 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>
|
#include <string>
|
||||||
|
|
||||||
// Configuració del tema UI (índexs de paleta, timings, fonts i flags)
|
// 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
|
// de Defaults::UI com a fallback: si el fitxer falta o li manca una
|
||||||
// clau, el joc s'executa amb el tema compilat.
|
// 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].
|
// 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`.
|
// el punter a la corresponent funció de `utils/easing.hpp`.
|
||||||
using CurveFn = auto (*)(float) -> float;
|
using CurveFn = auto (*)(float) -> float;
|
||||||
|
|
||||||
// Geometria del Notifier (popup superior). Carregada des de
|
// 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
|
// 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.
|
// velocitat, perquè una caixa més gran no triga més a entrar/eixir.
|
||||||
struct Notifier {
|
struct Notifier {
|
||||||
@@ -170,7 +170,7 @@ class UiConfig {
|
|||||||
[[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn;
|
[[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn;
|
||||||
|
|
||||||
// Sons del menú de servei. Claus de Resource::Cache::loadSound (els
|
// 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 menuSelectSound() const -> const std::string& { return menu_select_sound_; }
|
||||||
[[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_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 playerStartSound() const -> const std::string& { return player_start_sound_; }
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
#include <exception>
|
#include <exception>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace DebugConfig {
|
namespace DebugConfig {
|
||||||
|
|
||||||
@@ -53,12 +53,12 @@ namespace DebugConfig {
|
|||||||
if (!yaml.contains("start_scene")) { return StartScene::GAME; }
|
if (!yaml.contains("start_scene")) { return StartScene::GAME; }
|
||||||
const auto RAW = yaml["start_scene"].get_value<std::string>();
|
const auto RAW = yaml["start_scene"].get_value<std::string>();
|
||||||
if (!isKnown(RAW)) {
|
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 StartScene::GAME;
|
||||||
}
|
}
|
||||||
return fromName(RAW);
|
return fromName(RAW);
|
||||||
} catch (const std::exception& e) {
|
} 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;
|
return StartScene::GAME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ namespace DebugConfig {
|
|||||||
}
|
}
|
||||||
const auto CONTENT = readFile(file_path);
|
const auto CONTENT = readFile(file_path);
|
||||||
if (CONTENT.empty()) {
|
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 StartScene::GAME;
|
||||||
}
|
}
|
||||||
return parseStartScene(CONTENT);
|
return parseStartScene(CONTENT);
|
||||||
@@ -85,12 +85,12 @@ namespace DebugConfig {
|
|||||||
if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; }
|
if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; }
|
||||||
const auto RAW = yaml["starting_players"].get_value<std::string>();
|
const auto RAW = yaml["starting_players"].get_value<std::string>();
|
||||||
if (!isKnownPlayers(RAW)) {
|
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 StartingPlayers::BOTH;
|
||||||
}
|
}
|
||||||
return playersFromName(RAW);
|
return playersFromName(RAW);
|
||||||
} catch (const std::exception& e) {
|
} 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;
|
return StartingPlayers::BOTH;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ namespace DebugConfig {
|
|||||||
if (!yaml.contains("starting_level")) { return std::nullopt; }
|
if (!yaml.contains("starting_level")) { return std::nullopt; }
|
||||||
const auto& sl = yaml["starting_level"];
|
const auto& sl = yaml["starting_level"];
|
||||||
if (!sl.contains("pyramid") || !sl.contains("room")) {
|
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 std::nullopt;
|
||||||
}
|
}
|
||||||
return StartingLevel{
|
return StartingLevel{
|
||||||
@@ -118,7 +118,7 @@ namespace DebugConfig {
|
|||||||
.room = sl["room"].get_value<int>(),
|
.room = sl["room"].get_value<int>(),
|
||||||
};
|
};
|
||||||
} catch (const std::exception& e) {
|
} 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;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ namespace DebugConfig {
|
|||||||
if (std::filesystem::exists(file_path, error)) { return; }
|
if (std::filesystem::exists(file_path, error)) { return; }
|
||||||
std::ofstream file(file_path);
|
std::ofstream file(file_path);
|
||||||
if (!file) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
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 << "# clau, comença a la primera (P1-R1).\n";
|
||||||
file << "start_scene: GAME\n";
|
file << "start_scene: GAME\n";
|
||||||
file << "starting_players: BOTH\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
|
} // namespace DebugConfig
|
||||||
@@ -5,9 +5,10 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
|
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
|
||||||
// carpeta de sistema (`$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml`). No es
|
// carpeta de sistema del joc (vegeu `Director::createSystemFolder` per la
|
||||||
// versiona: cada dev el crea a la seua màquina. Si no existeix, s'aplica el
|
// ubicació real per plataforma). No es versiona: cada dev el crea a la seua
|
||||||
// valor per defecte (GAME) sense warning de nivell error.
|
// màquina. Si no existeix, s'aplica el valor per defecte (GAME) sense warning
|
||||||
|
// de nivell error.
|
||||||
namespace DebugConfig {
|
namespace DebugConfig {
|
||||||
|
|
||||||
enum class StartScene : std::uint8_t { LOGO,
|
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.
|
// llança si no troba — provem primer i, en cas de miss, carreguem.
|
||||||
const auto REL = sp["path"].get_value<std::string>();
|
const auto REL = sp["path"].get_value<std::string>();
|
||||||
const std::string KEY = std::filesystem::path(REL).filename().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 += name;
|
||||||
full += '/';
|
full += '/';
|
||||||
full += REL;
|
full += REL;
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ namespace {
|
|||||||
if (!s.empty() && s.front() == '#') { s.erase(0, 1); }
|
if (!s.empty() && s.front() == '#') { s.erase(0, 1); }
|
||||||
dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0));
|
dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0));
|
||||||
} catch (const std::exception& e) {
|
} 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) {
|
void EditorConfig::load(const std::string& file_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(file_path);
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
if (bytes.empty()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -76,9 +76,9 @@ void EditorConfig::load(const std::string& file_path) {
|
|||||||
readArgb(c, "picker_hover", picker_hover_argb_);
|
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) {
|
} 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>
|
#include <string>
|
||||||
|
|
||||||
// Configuració visual de l'editor de tilemap (Debug-only). Carregada des
|
// 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).
|
// a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load).
|
||||||
class EditorConfig {
|
class EditorConfig {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -8,11 +8,14 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -133,6 +136,14 @@ Mummy::Mummy(const fkyaml::node& yaml)
|
|||||||
if (h.contains("whippable")) {
|
if (h.contains("whippable")) {
|
||||||
whippable_ = h["whippable"].get_value<bool>();
|
whippable_ = h["whippable"].get_value<bool>();
|
||||||
}
|
}
|
||||||
|
// So opcional: el basename val tant de clau al Resource::Cache com de
|
||||||
|
// path (`Defaults::Paths::AUDIO_SOUNDS_DIR + key`). Mateix patró que
|
||||||
|
// els sons del Player. Si el camp manca, la clau queda buida i
|
||||||
|
// `enterHurt` no dispara playSound.
|
||||||
|
if (h.contains("sound")) {
|
||||||
|
hurt_config_.sound_key = h["sound"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadSound(hurt_config_.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + hurt_config_.sound_key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,9 +222,24 @@ void Mummy::step(float delta_time) {
|
|||||||
const float DX = dir_x_ * speed_ * delta_time;
|
const float DX = dir_x_ * speed_ * delta_time;
|
||||||
const float DY = dir_y_ * speed_ * delta_time;
|
const float DY = dir_y_ * speed_ * delta_time;
|
||||||
|
|
||||||
// Si no té direcció (p.ex. YAML sense `initial_direction`), el frame és
|
// Sense direcció: pot ser un setup degenerat (YAML sense
|
||||||
// no-op fins que algú la injecte amb `setDirection`.
|
// `initial_direction`) o el residu d'un HURT que va sortir amb
|
||||||
if (DX == 0.0F && DY == 0.0F) { return; }
|
// `chooseHurtDirection() == NONE` (totes les direccions provades
|
||||||
|
// bloquejades). En aquest segon cas la mòmia quedaria eternament quieta
|
||||||
|
// — `setDirection(0,0)` a `enterHurt` no toca `anim_`, així que es
|
||||||
|
// veuria animant-se sense moure's. Re-decidim com a una intersecció:
|
||||||
|
// si trobem alguna perpendicular vàlida, la prenem; si no, mitja volta.
|
||||||
|
// Si tampoc, mantenim (0,0) i tornem a provar el proper frame —
|
||||||
|
// pràcticament impossible amb la geometria actual, però no enganxa.
|
||||||
|
if (DX == 0.0F && DY == 0.0F) {
|
||||||
|
auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_blocked=*/true);
|
||||||
|
if (!pools.perp.empty()) {
|
||||||
|
applyChoice(chooseDirection(pools.perp));
|
||||||
|
} else if (!pools.uturn.empty()) {
|
||||||
|
applyChoice(chooseDirection(pools.uturn));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Branca creuament: si aquest frame travessa la línia central d'un
|
// Branca creuament: si aquest frame travessa la línia central d'un
|
||||||
// corredor perpendicular, amarra la caixa al centre del creuament i
|
// corredor perpendicular, amarra la caixa al centre del creuament i
|
||||||
@@ -511,6 +537,9 @@ void Mummy::enterHurt(Direction flee_dir) {
|
|||||||
state_ = State::HURT;
|
state_ = State::HURT;
|
||||||
hurt_remaining_s_ = hurt_config_.duration_s;
|
hurt_remaining_s_ = hurt_config_.duration_s;
|
||||||
hurt_speed_ = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_;
|
hurt_speed_ = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_;
|
||||||
|
if (!hurt_config_.sound_key.empty() && Audio::get() != nullptr) {
|
||||||
|
Audio::get()->playSound(hurt_config_.sound_key, Audio::Group::GAME);
|
||||||
|
}
|
||||||
|
|
||||||
const Direction CHOSEN = chooseHurtDirection(flee_dir);
|
const Direction CHOSEN = chooseHurtDirection(flee_dir);
|
||||||
if (CHOSEN == Direction::NONE) {
|
if (CHOSEN == Direction::NONE) {
|
||||||
@@ -598,8 +627,20 @@ void Mummy::hurtStep(float delta_time) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Topa amb mur o sarc: surt del HURT immediatament. La IA normal
|
// Topa amb mur del playfield: amarra la BOX a la vora exacta abans de
|
||||||
// re-decideix al pròxim step (no fem U-turn ni re-orientació aquí).
|
// transitar a ROAMING. Sense aquest snap, el drift sub-pixel acumulat a
|
||||||
|
// la cursa (DX = -hurt_speed·dt rarament és enter al joc real, on dt ve
|
||||||
|
// de SDL_GetTicks) deixaria la BOX a un offset de 0.x px dins de
|
||||||
|
// l'última cel·la. Com que la BOX és 16×16 i els passadissos també ho
|
||||||
|
// són, qualsevol drift fa que la cantonada sobresurti cap als sarcs
|
||||||
|
// adjacents — i el `chooseHurtDirection` del proper fueteig classifica
|
||||||
|
// les perpendiculars com a bloquejades per micro-overlap, retornant
|
||||||
|
// NONE i deixant la mòmia eternament quieta. Sarc-collision durant HURT
|
||||||
|
// és un cas molt menys habitual (la cursa va sempre per corredor recte);
|
||||||
|
// si arriba a passar, l'snap al playfield no l'arregla però la branca
|
||||||
|
// de recovery a `step()` el cobreix.
|
||||||
|
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
|
||||||
|
|
||||||
state_ = State::ROAMING;
|
state_ = State::ROAMING;
|
||||||
hurt_remaining_s_ = 0.0F;
|
hurt_remaining_s_ = 0.0F;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <climits>
|
#include <climits>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
@@ -35,6 +36,7 @@ class Mummy : public DynamicHazard {
|
|||||||
struct HurtConfig {
|
struct HurtConfig {
|
||||||
float duration_s{0.0F};
|
float duration_s{0.0F};
|
||||||
float speed_px_per_s{0.0F};
|
float speed_px_per_s{0.0F};
|
||||||
|
std::string sound_key; // Buit = sense so. El fitxer viu a Defaults::Paths::AUDIO_SOUNDS_DIR.
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit Mummy(const fkyaml::node& yaml);
|
explicit Mummy(const fkyaml::node& yaml);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
|
#include "game/gameplay_config.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -174,9 +175,16 @@ void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y,
|
|||||||
// d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`.
|
// d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`.
|
||||||
// L'amplada perpendicular és `tile_size` (els passadissos del joc
|
// L'amplada perpendicular és `tile_size` (els passadissos del joc
|
||||||
// tenen exactament aquesta mida i l'atac és lateral).
|
// tenen exactament aquesta mida i l'atac és lateral).
|
||||||
|
//
|
||||||
|
// Si el tweak `whip.damage_pierces_walls` està actiu, la hitbox
|
||||||
|
// ignora els sarcòfags (passa a través del bloc complet) però
|
||||||
|
// continua respectant el `insidePlayfield`: fora de la sala no hi
|
||||||
|
// ha res a colpejar i ampliar-ho generaria visuals/edge cases
|
||||||
|
// estranys. El mur extern, doncs, sempre retalla.
|
||||||
const auto TILE = static_cast<float>(Map::tile_size);
|
const auto TILE = static_cast<float>(Map::tile_size);
|
||||||
const int MAX_PX = static_cast<int>(std::max(0.0F, range_px));
|
const int MAX_PX = static_cast<int>(std::max(0.0F, range_px));
|
||||||
const auto MAX = static_cast<float>(MAX_PX);
|
const auto MAX = static_cast<float>(MAX_PX);
|
||||||
|
const bool PIERCES = GameplayConfig::get().whipDamagePiercesWalls();
|
||||||
float length = 0.0F;
|
float length = 0.0F;
|
||||||
|
|
||||||
auto try_probe = [&](float l) -> bool {
|
auto try_probe = [&](float l) -> bool {
|
||||||
@@ -198,7 +206,9 @@ void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y,
|
|||||||
probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l};
|
probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return Map::insidePlayfield(probe) && !Map::collides(probe);
|
if (!Map::insidePlayfield(probe)) { return false; }
|
||||||
|
if (PIERCES) { return true; }
|
||||||
|
return !Map::collides(probe);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 1; i <= MAX_PX; ++i) {
|
for (int i = 1; i <= MAX_PX; ++i) {
|
||||||
@@ -311,6 +321,10 @@ namespace {
|
|||||||
WhipAttack::Direction dir,
|
WhipAttack::Direction dir,
|
||||||
std::unordered_set<void*>& already_hit) {
|
std::unordered_set<void*>& already_hit) {
|
||||||
if (p == owner || !playerIsTargetable(p)) { return; }
|
if (p == owner || !playerIsTargetable(p)) { return; }
|
||||||
|
// Tweak `combat.friendly_fire`: si està desactivat, els altres
|
||||||
|
// jugadors són immunes al látigo aliat. L'owner ja queda exclòs
|
||||||
|
// per la primera guarda; aquest filtre cobreix només cross-player.
|
||||||
|
if (!GameplayConfig::get().friendlyFire()) { return; }
|
||||||
if (already_hit.contains(static_cast<void*>(p))) { return; }
|
if (already_hit.contains(static_cast<void*>(p))) { return; }
|
||||||
const auto BOX = p->getBaseBox();
|
const auto BOX = p->getBaseBox();
|
||||||
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
|
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "game/entities/entity.hpp"
|
#include "game/entities/entity.hpp"
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
namespace EntityLoader {
|
namespace EntityLoader {
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ namespace EntityLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto templatePath(const std::string& name) -> std::string {
|
auto templatePath(const std::string& name) -> std::string {
|
||||||
return "data/entities/" + name + "/" + name + ".yaml";
|
return std::string{Defaults::Paths::ENTITIES_DIR} + name + "/" + name + ".yaml";
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
|
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iostream>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -21,6 +20,8 @@
|
|||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
// Render centralitzat al manager per confinar la dependència cap a
|
// Render centralitzat al manager per confinar la dependència cap a
|
||||||
// `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el
|
// `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el
|
||||||
@@ -124,7 +125,7 @@ namespace {
|
|||||||
if (s.contains("path")) {
|
if (s.contains("path")) {
|
||||||
const auto REL = s["path"].get_value<std::string>();
|
const auto REL = s["path"].get_value<std::string>();
|
||||||
info.key = std::filesystem::path(REL).filename().string();
|
info.key = std::filesystem::path(REL).filename().string();
|
||||||
info.full_path = "data/entities/" + type + "/" + REL;
|
info.full_path = std::string{Defaults::Paths::ENTITIES_DIR} + type + "/" + REL;
|
||||||
info.valid = true;
|
info.valid = true;
|
||||||
}
|
}
|
||||||
if (s.contains("size")) {
|
if (s.contains("size")) {
|
||||||
@@ -236,12 +237,12 @@ namespace {
|
|||||||
try {
|
try {
|
||||||
tmpl = EntityLoader::loadTemplate(type);
|
tmpl = EntityLoader::loadTemplate(type);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "EntityManager: " << e.what() << '\n';
|
Log::error("EntityManager", "{}", e.what());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
auto entity = EntityFactory::instance().create(type, tmpl);
|
auto entity = EntityFactory::instance().create(type, tmpl);
|
||||||
if (entity == nullptr) {
|
if (entity == nullptr) {
|
||||||
std::cerr << "EntityManager: tipus desconegut '" << type << "'\n";
|
Log::error("EntityManager", "tipus desconegut '{}'", type);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
sprite_out = readSpriteInfo(tmpl, type);
|
sprite_out = readSpriteInfo(tmpl, type);
|
||||||
@@ -266,7 +267,7 @@ namespace {
|
|||||||
namespace {
|
namespace {
|
||||||
auto placeSarcophagus(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool {
|
auto placeSarcophagus(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool {
|
||||||
if (!spawn.contains("sarc_position")) {
|
if (!spawn.contains("sarc_position")) {
|
||||||
std::cerr << "EntityManager: sarcophagus sense sarc_position\n";
|
Log::error("EntityManager", "sarcophagus sense sarc_position");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto& sp = spawn["sarc_position"];
|
const auto& sp = spawn["sarc_position"];
|
||||||
@@ -303,7 +304,7 @@ namespace {
|
|||||||
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
|
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
|
||||||
// `unique_ptr` evitem el lint `bugprone-unused-return-value`.
|
// `unique_ptr` evitem el lint `bugprone-unused-return-value`.
|
||||||
if (dynamic_cast<Sarcophagus*>(entity.get()) == nullptr) {
|
if (dynamic_cast<Sarcophagus*>(entity.get()) == nullptr) {
|
||||||
std::cerr << "EntityManager: la factory no ha creat un Sarcophagus\n";
|
Log::error("EntityManager", "la factory no ha creat un Sarcophagus");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::unique_ptr<Sarcophagus> sarc(static_cast<Sarcophagus*>(entity.release()));
|
std::unique_ptr<Sarcophagus> sarc(static_cast<Sarcophagus*>(entity.release()));
|
||||||
@@ -322,7 +323,7 @@ namespace {
|
|||||||
// queden tapats — comportament intencional.
|
// queden tapats — comportament intencional.
|
||||||
auto placeDoor(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite) -> bool {
|
auto placeDoor(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite) -> bool {
|
||||||
if (Map::door_col <= 0 || Map::tile_size <= 0) {
|
if (Map::door_col <= 0 || Map::tile_size <= 0) {
|
||||||
std::cerr << "EntityManager: door requereix `geometry.door_col` definit al mapa\n";
|
Log::error("EntityManager", "door requereix `geometry.door_col` definit al mapa");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const float CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
|
const float CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
|
||||||
@@ -342,7 +343,7 @@ namespace {
|
|||||||
|
|
||||||
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const std::string& anchor, const fkyaml::node& spawn) -> bool {
|
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const std::string& anchor, const fkyaml::node& spawn) -> bool {
|
||||||
if (!spawn.contains("position")) {
|
if (!spawn.contains("position")) {
|
||||||
std::cerr << "EntityManager: " << type << " sense position\n";
|
Log::error("EntityManager", "{} sense position", type);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto& p = spawn["position"];
|
const auto& p = spawn["position"];
|
||||||
@@ -443,10 +444,8 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
|||||||
++decal_broadcast;
|
++decal_broadcast;
|
||||||
}
|
}
|
||||||
if (decal_broadcast > 0) {
|
if (decal_broadcast > 0) {
|
||||||
std::cout << "EntityManager: " << decal_broadcast
|
Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast);
|
||||||
<< " tile_painted broadcast inicial (decals de sòl)\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "EntityManager: " << LOADED << " entitats carregades des de "
|
Log::info("EntityManager", "{} entitats carregades des de {}", LOADED, level_yaml_path);
|
||||||
<< level_yaml_path << '\n';
|
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "game/entities/entity.hpp"
|
#include "game/entities/entity.hpp"
|
||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/gameplay_config.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
@@ -179,16 +180,16 @@ void Player::readJumpBlock(const fkyaml::node& yaml) {
|
|||||||
if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
|
if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
|
||||||
if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
|
if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
|
||||||
if (j.contains("shadow_y_offset")) { jump_config.shadow_y_offset = j["shadow_y_offset"].get_value<int>(); }
|
if (j.contains("shadow_y_offset")) { jump_config.shadow_y_offset = j["shadow_y_offset"].get_value<int>(); }
|
||||||
// Sons opcionals: el nom del fitxer (relatiu a data/sounds/) val tant
|
// Sons opcionals: el nom del fitxer (relatiu a Defaults::Paths::AUDIO_SOUNDS_DIR)
|
||||||
// de clau al Resource::Cache com de basename per a la càrrega. Si el
|
// val tant de clau al Resource::Cache com de basename per a la càrrega.
|
||||||
// camp manca, la clau queda buida i `playSound` no es crida.
|
// Si el camp manca, la clau queda buida i `playSound` no es crida.
|
||||||
if (j.contains("sound")) {
|
if (j.contains("sound")) {
|
||||||
jump_config.sound_key = j["sound"].get_value<std::string>();
|
jump_config.sound_key = j["sound"].get_value<std::string>();
|
||||||
Resource::Cache::get().loadSound(jump_config.sound_key, "data/sounds/" + jump_config.sound_key);
|
Resource::Cache::get().loadSound(jump_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + jump_config.sound_key);
|
||||||
}
|
}
|
||||||
if (j.contains("land_sound")) {
|
if (j.contains("land_sound")) {
|
||||||
jump_config.land_sound_key = j["land_sound"].get_value<std::string>();
|
jump_config.land_sound_key = j["land_sound"].get_value<std::string>();
|
||||||
Resource::Cache::get().loadSound(jump_config.land_sound_key, "data/sounds/" + jump_config.land_sound_key);
|
Resource::Cache::get().loadSound(jump_config.land_sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + jump_config.land_sound_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +202,7 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
|
|||||||
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
|
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
|
||||||
if (h.contains("sound")) {
|
if (h.contains("sound")) {
|
||||||
hurt_config.sound_key = h["sound"].get_value<std::string>();
|
hurt_config.sound_key = h["sound"].get_value<std::string>();
|
||||||
Resource::Cache::get().loadSound(hurt_config.sound_key, "data/sounds/" + hurt_config.sound_key);
|
Resource::Cache::get().loadSound(hurt_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + hurt_config.sound_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,10 +221,11 @@ void Player::readImmunityBlock(const fkyaml::node& yaml) {
|
|||||||
if (im.contains("duration_ms")) { immunity_config.duration_ms = im["duration_ms"].get_value<float>(); }
|
if (im.contains("duration_ms")) { immunity_config.duration_ms = im["duration_ms"].get_value<float>(); }
|
||||||
if (im.contains("blink_period_ms")) { immunity_config.blink_period_ms = im["blink_period_ms"].get_value<int>(); }
|
if (im.contains("blink_period_ms")) { immunity_config.blink_period_ms = im["blink_period_ms"].get_value<int>(); }
|
||||||
// Paleta del blink: el basename val tant de clau al Resource::Cache
|
// Paleta del blink: el basename val tant de clau al Resource::Cache
|
||||||
// com de path (`data/player/<key>.pal`). Mateix patró que els sons.
|
// com de path (`Defaults::Paths::PLAYER_DIR + key + .pal`). Mateix patró
|
||||||
|
// que els sons.
|
||||||
if (im.contains("palette")) {
|
if (im.contains("palette")) {
|
||||||
immunity_config.palette_key = im["palette"].get_value<std::string>();
|
immunity_config.palette_key = im["palette"].get_value<std::string>();
|
||||||
Resource::Cache::get().loadPalette(immunity_config.palette_key, "data/player/" + immunity_config.palette_key + ".pal");
|
Resource::Cache::get().loadPalette(immunity_config.palette_key, std::string{Defaults::Paths::PLAYER_DIR} + immunity_config.palette_key + ".pal");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +241,7 @@ void Player::readWhipBlock(const fkyaml::node& yaml) {
|
|||||||
// clau al cache i de path. Si manca, queda buit i no es dispara.
|
// clau al cache i de path. Si manca, queda buit i no es dispara.
|
||||||
if (w.contains("sound")) {
|
if (w.contains("sound")) {
|
||||||
whip_config.sound_key = w["sound"].get_value<std::string>();
|
whip_config.sound_key = w["sound"].get_value<std::string>();
|
||||||
Resource::Cache::get().loadSound(whip_config.sound_key, "data/sounds/" + whip_config.sound_key);
|
Resource::Cache::get().loadSound(whip_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + whip_config.sound_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,6 +415,10 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
|||||||
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
|
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
|
||||||
if (isOutOfPlay()) { return false; }
|
if (isOutOfPlay()) { return false; }
|
||||||
if (map_.collides(box)) { return true; }
|
if (map_.collides(box)) { return true; }
|
||||||
|
// Tweak `players.collide_with_each_other`: si està desactivat, els
|
||||||
|
// jugadors es travessen lliurement. La col·lisió contra el món
|
||||||
|
// (murs/sarcs) ja s'ha avaluat dalt; saltem només la part actor↔actor.
|
||||||
|
if (!GameplayConfig::get().playersCollide()) { return false; }
|
||||||
if (all_players_ == nullptr) { return false; }
|
if (all_players_ == nullptr) { return false; }
|
||||||
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
||||||
// projectem la hit-box des de la posició base proposada i la comparem
|
// projectem la hit-box des de la posició base proposada i la comparem
|
||||||
@@ -1026,6 +1032,12 @@ void Player::resolveLandingOverlap() {
|
|||||||
// canvi, qualsevol jugador amb el qual solape entra en HURT i
|
// canvi, qualsevol jugador amb el qual solape entra en HURT i
|
||||||
// surt projectat. Així el solapament inicial es resol per moviment
|
// surt projectat. Així el solapament inicial es resol per moviment
|
||||||
// del solapat, no per desplaçament forçat al lloc.
|
// del solapat, no per desplaçament forçat al lloc.
|
||||||
|
//
|
||||||
|
// Tweak `players.collide_with_each_other`: si està desactivat, el
|
||||||
|
// saltador travessa l'altre net en lloc de projectar-lo. Aquest
|
||||||
|
// camí és independent de `collidesWithWorld`, així que cal aturar-lo
|
||||||
|
// explícitament aquí.
|
||||||
|
if (!GameplayConfig::get().playersCollide()) { return; }
|
||||||
if (all_players_ == nullptr) { return; }
|
if (all_players_ == nullptr) { return; }
|
||||||
for (Player* other : *all_players_) {
|
for (Player* other : *all_players_) {
|
||||||
if (other == nullptr || other == this) { continue; }
|
if (other == nullptr || other == this) { continue; }
|
||||||
@@ -1258,11 +1270,30 @@ void Player::applyDyingMovement(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Player::resetForJoin() {
|
||||||
|
lives_remaining_ = death_config.initial_lives;
|
||||||
|
}
|
||||||
|
|
||||||
void Player::respawnFromDying() {
|
void Player::respawnFromDying() {
|
||||||
// Vides: decreix sempre. Pot anar a negatius — no hi ha game over.
|
// Vides: decreix una unitat. Si el comptador arriba a 0 el jugador
|
||||||
|
// surt de partida (DISABLED) i la GameScene/HUD reaccionen via bus
|
||||||
|
// — això habilita el flux de "join in progress" (l'altre jugador
|
||||||
|
// pot pulsar START per entrar) i obrirà el camí al CONTINUE? +
|
||||||
|
// GAME OVER en una fase posterior.
|
||||||
--lives_remaining_;
|
--lives_remaining_;
|
||||||
death_animation_complete_ = false;
|
death_animation_complete_ = false;
|
||||||
respawn_remaining_ms_ = 0.0F;
|
respawn_remaining_ms_ = 0.0F;
|
||||||
|
if (lives_remaining_ <= 0) {
|
||||||
|
disable();
|
||||||
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_disabled",
|
||||||
|
.payload = std::monostate{},
|
||||||
|
.player_id = id_,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Cinemàtica idèntica a la d'INTRO de sala: el jugador cau des de
|
// Cinemàtica idèntica a la d'INTRO de sala: el jugador cau des de
|
||||||
// dalt fins al passadís d'spawn (`spawnFromAir` reseteja primer i
|
// dalt fins al passadís d'spawn (`spawnFromAir` reseteja primer i
|
||||||
// després posa AIRBORNE amb l'altura derivada de la geometria).
|
// després posa AIRBORNE amb l'altura derivada de la geometria).
|
||||||
@@ -1736,6 +1767,10 @@ auto Player::tryPushSnapBlockers(
|
|||||||
float (Player::*get_pos)() const,
|
float (Player::*get_pos)() const,
|
||||||
void (Player::*set_pos)(float)) -> bool {
|
void (Player::*set_pos)(float)) -> bool {
|
||||||
if (all_players_ == nullptr) { return false; }
|
if (all_players_ == nullptr) { return false; }
|
||||||
|
// Si el tweak de col·lisió entre jugadors està desactivat, mai hi
|
||||||
|
// ha res a empentar — els jugadors són intangibles entre si i el
|
||||||
|
// snap pot procedir lliurement sense desplaçar ningú.
|
||||||
|
if (!GameplayConfig::get().playersCollide()) { return false; }
|
||||||
|
|
||||||
// Blockers = jugadors que ara mateix solapen la nostra hit-box (ja
|
// Blockers = jugadors que ara mateix solapen la nostra hit-box (ja
|
||||||
// amb el snap aplicat). Mateix criteri actor-actor que `collidesWithWorld`.
|
// amb el snap aplicat). Mateix criteri actor-actor que `collidesWithWorld`.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class Player : public AnimatedSprite {
|
|||||||
DYING,
|
DYING,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
// Geometria carregada des del YAML del player (p.ex. `data/entities/player/sam.yaml`).
|
||||||
// `sprite_*` són
|
// `sprite_*` són
|
||||||
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
|
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
|
||||||
// 12 píxels superiors són el cap i els 12 inferiors la base).
|
// 12 píxels superiors són el cap i els 12 inferiors la base).
|
||||||
@@ -127,6 +127,11 @@ class Player : public AnimatedSprite {
|
|||||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||||
static void loadFrom(const std::string& yaml_path);
|
static void loadFrom(const std::string& yaml_path);
|
||||||
|
|
||||||
|
// Vides amb què arrenca un jugador (camp `lives:` del YAML del
|
||||||
|
// jugador). El consumeix el HUD per inicialitzar el comptador del
|
||||||
|
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
|
||||||
|
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
|
||||||
|
|
||||||
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
||||||
|
|
||||||
// Update en una sola crida: manté l'API anterior per als tests i
|
// Update en una sola crida: manté l'API anterior per als tests i
|
||||||
@@ -296,8 +301,11 @@ class Player : public AnimatedSprite {
|
|||||||
// `death_spinning`, posició congelada, input ignorat. Idempotent —
|
// `death_spinning`, posició congelada, input ignorat. Idempotent —
|
||||||
// si ja està DYING, EXITED o DISABLED, és no-op (un segon toc de
|
// si ja està DYING, EXITED o DISABLED, és no-op (un segon toc de
|
||||||
// momia mentre la roda no la reinicia). Quan l'animació acaba i
|
// momia mentre la roda no la reinicia). Quan l'animació acaba i
|
||||||
// `respawn_delay_ms` expira, `respawn(spawn_x_, spawn_y_)` torna a
|
// `respawn_delay_ms` expira, `respawnFromDying()` decrementa
|
||||||
// GROUNDED i `lives_remaining_` decreix en 1 (pot anar a negatius).
|
// `lives_remaining_`: si encara queden vides, fa `spawnFromAir`
|
||||||
|
// al spawn point; si arriba a 0, fa `disable()` i publica al bus
|
||||||
|
// `player_disabled` perquè GameScene/HUD reaccionin (slot fora de
|
||||||
|
// partida, esperant un nou START per re-entrar via "join in progress").
|
||||||
void enterDying(DeathCause cause = DeathCause::GENERIC);
|
void enterDying(DeathCause cause = DeathCause::GENERIC);
|
||||||
|
|
||||||
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
||||||
@@ -309,9 +317,11 @@ class Player : public AnimatedSprite {
|
|||||||
spawn_y_ = y;
|
spawn_y_ = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comptador d'intents (estil Mario, NO HP). Decreix a cada respawn;
|
// Comptador d'intents (estil Mario, NO HP). Decreix a cada respawn
|
||||||
// no es clampa a 0 — permet anar a negatius perquè avui no hi ha
|
// post-mort. En arribar a 0 el jugador passa a DISABLED i publica
|
||||||
// game over. Públic per a HUD i tests.
|
// `player_disabled` al bus; pot tornar a partida via "join in
|
||||||
|
// progress" (GameScene::joinPlayer crida `resetForJoin`). Públic
|
||||||
|
// per a HUD i tests.
|
||||||
[[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; }
|
[[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; }
|
||||||
|
|
||||||
// Activa la immunitat per `duration_ms` ms. Mentre `isImmune()` és
|
// Activa la immunitat per `duration_ms` ms. Mentre `isImmune()` és
|
||||||
@@ -342,6 +352,14 @@ class Player : public AnimatedSprite {
|
|||||||
// sala usa `disable()` + `spawnFromAir()`.
|
// sala usa `disable()` + `spawnFromAir()`.
|
||||||
void respawn(float x, float y);
|
void respawn(float x, float y);
|
||||||
|
|
||||||
|
// Restaura el comptador de vides al valor inicial configurat
|
||||||
|
// (`death_config.initial_lives`). Cridat per la GameScene quan un
|
||||||
|
// slot inactiu pulsa START per entrar mid-partida (regla 6 de la
|
||||||
|
// FSM de joc): el jugador entra amb les vides plenes. La resta de
|
||||||
|
// l'estat (posició, animació, flags) la reseteja `spawnFromAir`
|
||||||
|
// que GameScene crida tot seguit.
|
||||||
|
void resetForJoin();
|
||||||
|
|
||||||
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
|
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
|
||||||
// segona crida és no-op. Mateix patró que `exit()` per simetria.
|
// segona crida és no-op. Mateix patró que `exit()` per simetria.
|
||||||
// No publica res al bus — el GameScene decideix quan disparar el
|
// No publica res al bus — el GameScene decideix quan disparar el
|
||||||
@@ -362,7 +380,7 @@ class Player : public AnimatedSprite {
|
|||||||
// al respawn post-mort: mateixa entrada cinemàtica.
|
// al respawn post-mort: mateixa entrada cinemàtica.
|
||||||
void spawnFromAir(float x, float y);
|
void spawnFromAir(float x, float y);
|
||||||
|
|
||||||
// Arma un preset de reverb (catàleg a `data/sounds.yaml`) per al
|
// Arma un preset de reverb (catàleg a `data/config/sounds.yaml`) per al
|
||||||
// pròxim aterratge d'aquest jugador. Pensat per a la cinemàtica
|
// pròxim aterratge d'aquest jugador. Pensat per a la cinemàtica
|
||||||
// d'INTRO de sala: GameScene crida aquesta funció just després de
|
// d'INTRO de sala: GameScene crida aquesta funció just després de
|
||||||
// `spawnFromAir`, i el primer LAND consumeix el preset (queda buit
|
// `spawnFromAir`, i el primer LAND consumeix el preset (queda buit
|
||||||
@@ -529,7 +547,7 @@ class Player : public AnimatedSprite {
|
|||||||
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
||||||
bool being_pushed_{false};
|
bool being_pushed_{false};
|
||||||
|
|
||||||
// Preset de reverb (catàleg de `data/sounds.yaml`) que el pròxim
|
// Preset de reverb (catàleg de `data/config/sounds.yaml`) que el pròxim
|
||||||
// LAND ha de fer servir; buit = so sec. L'arma `armNextLandReverb`
|
// LAND ha de fer servir; buit = so sec. L'arma `armNextLandReverb`
|
||||||
// (cridada per GameScene al spawn aeri d'INTRO) i el consumeix
|
// (cridada per GameScene al spawn aeri d'INTRO) i el consumeix
|
||||||
// `integrateJump` quan el jugador toca terra. Cap altre punt
|
// `integrateJump` quan el jugador toca terra. Cap altre punt
|
||||||
@@ -636,7 +654,7 @@ class Player : public AnimatedSprite {
|
|||||||
struct HurtConfig {
|
struct HurtConfig {
|
||||||
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
||||||
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
||||||
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/sounds/`).
|
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/audio/sounds/`).
|
||||||
// Mateixa convenció que la resta de sons del jugador: el basename val
|
// Mateixa convenció que la resta de sons del jugador: el basename val
|
||||||
// de clau al Resource::Cache i de path. Buit o absent = entrada
|
// de clau al Resource::Cache i de path. Buit o absent = entrada
|
||||||
// silenciosa.
|
// silenciosa.
|
||||||
@@ -660,7 +678,7 @@ class Player : public AnimatedSprite {
|
|||||||
int cooldown_ms{0};
|
int cooldown_ms{0};
|
||||||
int damage{0};
|
int damage{0};
|
||||||
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
|
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
|
||||||
// `data/sounds/`). Convenció: el basename val tant de clau al
|
// `data/audio/sounds/`). Convenció: el basename val tant de clau al
|
||||||
// Resource::Cache com de path. Buit o absent = swing silenciós.
|
// Resource::Cache com de path. Buit o absent = swing silenciós.
|
||||||
std::string sound_key;
|
std::string sound_key;
|
||||||
};
|
};
|
||||||
@@ -697,7 +715,7 @@ class Player : public AnimatedSprite {
|
|||||||
float duration_ms{0.0F};
|
float duration_ms{0.0F};
|
||||||
int blink_period_ms{0};
|
int blink_period_ms{0};
|
||||||
// Clau al Resource::Cache de la paleta plana usada al blink (basename
|
// Clau al Resource::Cache de la paleta plana usada al blink (basename
|
||||||
// del .pal a `data/player/`). Buida = no hi ha alternança visual,
|
// del .pal a `data/entities/player/`). Buida = no hi ha alternança visual,
|
||||||
// només la finestra lògica d'immunitat (que sense feedback visual
|
// només la finestra lògica d'immunitat (que sense feedback visual
|
||||||
// pot ser confusa, però no és incorrecte).
|
// pot ser confusa, però no és incorrecte).
|
||||||
std::string palette_key;
|
std::string palette_key;
|
||||||
@@ -706,7 +724,7 @@ class Player : public AnimatedSprite {
|
|||||||
|
|
||||||
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
|
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
|
||||||
// hi ha `palette`, registra la paleta corresponent al Resource::Cache
|
// hi ha `palette`, registra la paleta corresponent al Resource::Cache
|
||||||
// (path: `data/player/<palette_key>.pal`). Mateix patró que els altres
|
// (path: `data/entities/player/<palette_key>.pal`). Mateix patró que els altres
|
||||||
// readers — `loadFrom` el crida al final.
|
// readers — `loadFrom` el crida al final.
|
||||||
static void readImmunityBlock(const fkyaml::node& yaml);
|
static void readImmunityBlock(const fkyaml::node& yaml);
|
||||||
|
|
||||||
@@ -829,8 +847,10 @@ class Player : public AnimatedSprite {
|
|||||||
|
|
||||||
// Comptador d'intents (estil Mario). Inicialitzat a `death_config.
|
// Comptador d'intents (estil Mario). Inicialitzat a `death_config.
|
||||||
// initial_lives` al ctor; decreix una vegada per cada respawn
|
// initial_lives` al ctor; decreix una vegada per cada respawn
|
||||||
// post-mort (no per respawns de canvi de sala). Pot anar a negatius:
|
// post-mort (no per respawns de canvi de sala). En arribar a 0,
|
||||||
// avui no hi ha game over.
|
// `respawnFromDying` posa el jugador a DISABLED i publica
|
||||||
|
// `player_disabled` al bus. `resetForJoin()` el restaura al valor
|
||||||
|
// inicial quan el jugador re-entra mid-partida.
|
||||||
int lives_remaining_{0};
|
int lives_remaining_{0};
|
||||||
|
|
||||||
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#include "game/gameplay_config.hpp"
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto readFile(const std::string& path) -> std::string {
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file) { return {}; }
|
||||||
|
std::stringstream buffer;
|
||||||
|
buffer << file.rdbuf();
|
||||||
|
return buffer.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llig una subclau booleana obligatòria (`section.key`). Retorna
|
||||||
|
// false si la secció o la clau no hi són o el tipus no és bool —
|
||||||
|
// qualsevol d'aquests casos és una irregularitat que dispara
|
||||||
|
// reescriptura del fitxer.
|
||||||
|
auto readRequiredBool(const fkyaml::node& root, const char* section, const char* key, bool& out)
|
||||||
|
-> bool {
|
||||||
|
if (!root.contains(section)) { return false; }
|
||||||
|
const auto& sec = root[section];
|
||||||
|
if (!sec.contains(key)) { return false; }
|
||||||
|
try {
|
||||||
|
out = sec[key].get_value<bool>();
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escriu el fitxer amb els valors actuals + comentaris d'ajuda.
|
||||||
|
// Format estable perquè l'usuari pot editar-lo a mà.
|
||||||
|
void writeFile(const std::string& path,
|
||||||
|
int version,
|
||||||
|
bool whip_pierces,
|
||||||
|
bool friendly_fire,
|
||||||
|
bool players_collide) {
|
||||||
|
std::ofstream file(path);
|
||||||
|
if (!file) {
|
||||||
|
Log::error("gameplay.yaml", "no s'ha pogut escriure {}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file << "# gameplay.yaml — tweaks de jugabilitat persistents.\n";
|
||||||
|
file << "# Edita des del menú de servei (F9 → Tweaks) o a mà.\n";
|
||||||
|
file << "# Si esborres el fitxer (o aquesta capçalera de versió), es regenerarà\n";
|
||||||
|
file << "# amb els defaults a la propera arrencada.\n";
|
||||||
|
file << "\n";
|
||||||
|
file << "version: " << version << "\n";
|
||||||
|
file << "\n";
|
||||||
|
file << "whip:\n";
|
||||||
|
file << " damage_pierces_walls: " << (whip_pierces ? "true" : "false") << "\n";
|
||||||
|
file << "\n";
|
||||||
|
file << "combat:\n";
|
||||||
|
file << " friendly_fire: " << (friendly_fire ? "true" : "false") << "\n";
|
||||||
|
file << "\n";
|
||||||
|
file << "players:\n";
|
||||||
|
file << " collide_with_each_other: " << (players_collide ? "true" : "false") << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto GameplayConfig::get() -> GameplayConfig& {
|
||||||
|
static GameplayConfig instance_;
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameplayConfig::load(const std::string& file_path) {
|
||||||
|
file_path_ = file_path;
|
||||||
|
|
||||||
|
std::error_code error;
|
||||||
|
const bool EXISTS = std::filesystem::exists(file_path, error);
|
||||||
|
const auto CONTENT = EXISTS ? readFile(file_path) : std::string{};
|
||||||
|
|
||||||
|
// Tots els valors van a temporaris; només ens els quedem si la
|
||||||
|
// validació completa passa. Així una validació parcial (p. ex. dues
|
||||||
|
// claus bones i una de dolenta) no deixa l'estat a mig camí.
|
||||||
|
bool whip_pierces = whip_damage_pierces_walls_;
|
||||||
|
bool ff = friendly_fire_;
|
||||||
|
bool players_collide = players_collide_;
|
||||||
|
|
||||||
|
auto rewrite_with_defaults = [&](const char* reason) {
|
||||||
|
// Resetem els camps als defaults compilats abans de reescriure.
|
||||||
|
// Necessari si load() es crida diverses vegades amb fitxers
|
||||||
|
// distints; el primer pot haver mutat l'estat.
|
||||||
|
whip_damage_pierces_walls_ = true;
|
||||||
|
friendly_fire_ = true;
|
||||||
|
players_collide_ = true;
|
||||||
|
writeFile(file_path,
|
||||||
|
CURRENT_VERSION,
|
||||||
|
whip_damage_pierces_walls_,
|
||||||
|
friendly_fire_,
|
||||||
|
players_collide_);
|
||||||
|
Log::info("gameplay.yaml", "{} — reescrit a {} amb defaults", reason, file_path);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!EXISTS) {
|
||||||
|
rewrite_with_defaults("fitxer absent");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (CONTENT.empty()) {
|
||||||
|
rewrite_with_defaults("fitxer buit");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fkyaml::node yaml;
|
||||||
|
try {
|
||||||
|
yaml = fkyaml::node::deserialize(CONTENT);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
rewrite_with_defaults((std::string("parse error: ") + e.what()).c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `version` ha de ser un enter present i igual a CURRENT_VERSION.
|
||||||
|
// Qualsevol altra cosa (absent, tipus no enter, valor diferent) és
|
||||||
|
// una irregularitat: el fitxer ve d'una versió incompatible o ha
|
||||||
|
// estat manipulat de manera inesperada.
|
||||||
|
if (!yaml.contains("version")) {
|
||||||
|
rewrite_with_defaults("version absent");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int version = 0;
|
||||||
|
try {
|
||||||
|
version = yaml["version"].get_value<int>();
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
rewrite_with_defaults("version no és enter");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (version != CURRENT_VERSION) {
|
||||||
|
rewrite_with_defaults("version desconeguda");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readRequiredBool(yaml, "whip", "damage_pierces_walls", whip_pierces) || !readRequiredBool(yaml, "combat", "friendly_fire", ff) || !readRequiredBool(yaml, "players", "collide_with_each_other", players_collide)) {
|
||||||
|
rewrite_with_defaults("clau requerida absent o de tipus incorrecte");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tot validat: ens quedem els valors llegits.
|
||||||
|
whip_damage_pierces_walls_ = whip_pierces;
|
||||||
|
friendly_fire_ = ff;
|
||||||
|
players_collide_ = players_collide;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameplayConfig::save() const {
|
||||||
|
if (file_path_.empty()) {
|
||||||
|
// Sense path establert: cap consumidor s'ha registrat encara.
|
||||||
|
// Pot passar en tests que no carreguen el fitxer; silenciem el cas.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeFile(file_path_,
|
||||||
|
CURRENT_VERSION,
|
||||||
|
whip_damage_pierces_walls_,
|
||||||
|
friendly_fire_,
|
||||||
|
players_collide_);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Tweaks de jugabilitat persistents per usuari. Carregat de
|
||||||
|
// `gameplay.yaml` dins la carpeta de sistema (vegeu
|
||||||
|
// `Director::createSystemFolder` per la ubicació real per plataforma).
|
||||||
|
//
|
||||||
|
// Diferent d'UiConfig (data/config/ui.yaml, només lectura): aquest fitxer
|
||||||
|
// és escrivible i el ServiceMenu en modifica els valors. Els defaults
|
||||||
|
// compilats són els que veuen els nous usuaris fins que toquen el menú.
|
||||||
|
//
|
||||||
|
// Política d'integritat: davant qualsevol irregularitat (fitxer absent,
|
||||||
|
// parse error, `version` absent o diferent, clau requerida absent o de
|
||||||
|
// tipus incorrecte), `load()` reseteja a defaults i reescriu el fitxer.
|
||||||
|
// No es preserven fitxers parcialment vàlids — així evitem dragar
|
||||||
|
// versions antigues incoherents quan l'esquema evolucioni.
|
||||||
|
class GameplayConfig {
|
||||||
|
public:
|
||||||
|
// Esquema actual del fitxer. Incrementar quan canviï l'estructura
|
||||||
|
// (renomenar/treure claus o canviar semàntica). Els fitxers d'usuari
|
||||||
|
// amb una `version` distinta es reescriuen amb els defaults nous.
|
||||||
|
// Afegir noves claus opcionals no requereix bump si els defaults
|
||||||
|
// compilats són equivalents al comportament anterior.
|
||||||
|
static constexpr int CURRENT_VERSION = 1;
|
||||||
|
|
||||||
|
static auto get() -> GameplayConfig&;
|
||||||
|
|
||||||
|
GameplayConfig(const GameplayConfig&) = delete;
|
||||||
|
GameplayConfig(GameplayConfig&&) = delete;
|
||||||
|
auto operator=(const GameplayConfig&) -> GameplayConfig& = delete;
|
||||||
|
auto operator=(GameplayConfig&&) -> GameplayConfig& = delete;
|
||||||
|
|
||||||
|
// Llig el fitxer i guarda la ruta per a `save()`. Si no existeix,
|
||||||
|
// l'escriu amb els defaults compilats. Idempotent: pot cridar-se
|
||||||
|
// múltiples vegades (l'última crida fixa el path actiu).
|
||||||
|
void load(const std::string& file_path);
|
||||||
|
|
||||||
|
// Persisteix l'estat actual al path passat a `load()`. Cridat pel
|
||||||
|
// ServiceMenu a cada canvi de toggle. No-op si `load()` no s'ha
|
||||||
|
// cridat encara (sense path no podem escriure enlloc útil).
|
||||||
|
void save() const;
|
||||||
|
|
||||||
|
// Tweaks. Defaults: tots true.
|
||||||
|
//
|
||||||
|
// - whip_damage_pierces_walls: el dany del látigo travessa sarcòfags
|
||||||
|
// (NO el mur extern del playfield, que retalla igualment per
|
||||||
|
// evitar que el rect surti de la sala visible).
|
||||||
|
// - friendly_fire: un latigazo pot impactar l'altre jugador.
|
||||||
|
// - players_collide: els jugadors col·lisionen físicament entre si
|
||||||
|
// en lloc de travessar-se.
|
||||||
|
[[nodiscard]] auto whipDamagePiercesWalls() const -> bool { return whip_damage_pierces_walls_; }
|
||||||
|
[[nodiscard]] auto friendlyFire() const -> bool { return friendly_fire_; }
|
||||||
|
[[nodiscard]] auto playersCollide() const -> bool { return players_collide_; }
|
||||||
|
|
||||||
|
void setWhipDamagePiercesWalls(bool v) { whip_damage_pierces_walls_ = v; }
|
||||||
|
void setFriendlyFire(bool v) { friendly_fire_ = v; }
|
||||||
|
void setPlayersCollide(bool v) { players_collide_ = v; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
GameplayConfig() = default;
|
||||||
|
~GameplayConfig() = default;
|
||||||
|
|
||||||
|
// Ruta absoluta al fitxer persistit. Buida fins que algú crida
|
||||||
|
// `load()` per primera vegada; mentre estigui buida, `save()` és no-op.
|
||||||
|
std::string file_path_;
|
||||||
|
|
||||||
|
bool whip_damage_pierces_walls_{true};
|
||||||
|
bool friendly_fire_{true};
|
||||||
|
bool players_collide_{true};
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
#include "game/levels/level_manager.hpp"
|
#include "game/levels/level_manager.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace Levels {
|
namespace Levels {
|
||||||
|
|
||||||
@@ -46,8 +46,7 @@ namespace Levels {
|
|||||||
throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida");
|
throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida");
|
||||||
}
|
}
|
||||||
index_ = 0;
|
index_ = 0;
|
||||||
std::cout << "LevelManager: " << entries_.size() << " sales carregades de "
|
Log::info("LevelManager", "{} sales carregades de {} (començant a {})", entries_.size(), PATH, currentLabel());
|
||||||
<< PATH << " (començant a " << currentLabel() << ")\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto LevelManager::currentPath() const -> const std::string& {
|
auto LevelManager::currentPath() const -> const std::string& {
|
||||||
@@ -67,22 +66,20 @@ namespace Levels {
|
|||||||
auto LevelManager::advance() -> bool {
|
auto LevelManager::advance() -> bool {
|
||||||
if (index_ + 1 >= count()) { return false; }
|
if (index_ + 1 >= count()) { return false; }
|
||||||
++index_;
|
++index_;
|
||||||
std::cout << "LevelManager: avança a " << currentLabel()
|
Log::info("LevelManager", "avança a {} ({})", currentLabel(), currentPath());
|
||||||
<< " (" << currentPath() << ")\n";
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LevelManager::restart() {
|
void LevelManager::restart() {
|
||||||
index_ = 0;
|
index_ = 0;
|
||||||
std::cout << "LevelManager: restart a " << currentLabel() << '\n';
|
Log::info("LevelManager", "restart a {}", currentLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
void LevelManager::setIndex(int new_index) {
|
void LevelManager::setIndex(int new_index) {
|
||||||
const int CLAMPED = std::clamp(new_index, 0, count() - 1);
|
const int CLAMPED = std::clamp(new_index, 0, count() - 1);
|
||||||
if (CLAMPED != index_) {
|
if (CLAMPED != index_) {
|
||||||
index_ = CLAMPED;
|
index_ = CLAMPED;
|
||||||
std::cout << "LevelManager: setIndex(" << new_index << ") → "
|
Log::info("LevelManager", "setIndex({}) → {} ({})", new_index, currentLabel(), currentPath());
|
||||||
<< currentLabel() << " (" << currentPath() << ")\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,14 +87,11 @@ namespace Levels {
|
|||||||
for (std::size_t i = 0; i < entries_.size(); ++i) {
|
for (std::size_t i = 0; i < entries_.size(); ++i) {
|
||||||
if (entries_[i].pyramid == pyramid && entries_[i].room == room) {
|
if (entries_[i].pyramid == pyramid && entries_[i].room == room) {
|
||||||
index_ = static_cast<int>(i);
|
index_ = static_cast<int>(i);
|
||||||
std::cout << "LevelManager: starting_point P" << pyramid << "-R" << room
|
Log::info("LevelManager", "starting_point P{}-R{} trobat a l'índex {} ({})", pyramid, room, i, currentPath());
|
||||||
<< " trobat a l'índex " << i << " (" << currentPath() << ")\n";
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::cerr << "LevelManager: starting_point P" << pyramid << "-R" << room
|
Log::warn("LevelManager", "starting_point P{}-R{} no trobat a levels.yaml — es manté l'índex actual ({})", pyramid, room, currentLabel());
|
||||||
<< " no trobat a levels.yaml — es manté l'índex actual ("
|
|
||||||
<< currentLabel() << ")\n";
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -90,11 +90,7 @@ namespace Map {
|
|||||||
// canviarà el flag a true quan transiti a OPEN.
|
// canviarà el flag a true quan transiti a OPEN.
|
||||||
door_open = false;
|
door_open = false;
|
||||||
|
|
||||||
std::cout << "Map: geometria " << grid_cols << "x" << grid_rows
|
Log::info("Map", "geometria {}x{} sarcs {}x{} pitch {}x{} door_col {} des de {}", grid_cols, grid_rows, sarc_w, sarc_h, pitch_x, pitch_y, door_col, yaml_path);
|
||||||
<< " sarcs " << sarc_w << "x" << sarc_h
|
|
||||||
<< " pitch " << pitch_x << "x" << pitch_y
|
|
||||||
<< " door_col " << door_col
|
|
||||||
<< " des de " << yaml_path << '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto getSarcRect(int col, int row) -> SDL_FRect {
|
auto getSarcRect(int col, int row) -> SDL_FRect {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <iostream>
|
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
@@ -11,6 +10,7 @@
|
|||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
#include "game/map/tilemap.hpp"
|
#include "game/map/tilemap.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
// CONTRACTE BUS DE PINTAT
|
// CONTRACTE BUS DE PINTAT
|
||||||
// -----------------------
|
// -----------------------
|
||||||
@@ -89,9 +89,7 @@ PaintLayer::PaintLayer(const Tilemap& tilemap, EventBus* bus)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "PaintLayer: " << cols_ << "x" << rows_ << " tiles ("
|
Log::info("PaintLayer", "{}x{} tiles ({}x{} px), tileset {}", cols_, rows_, width_px_, height_px_, tileset_path_);
|
||||||
<< width_px_ << "x" << height_px_ << " px), tileset "
|
|
||||||
<< tileset_path_ << '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PaintLayer::~PaintLayer() {
|
PaintLayer::~PaintLayer() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -12,6 +11,7 @@
|
|||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
#include "game/map/tilemap_writer.hpp"
|
#include "game/map/tilemap_writer.hpp"
|
||||||
@@ -97,8 +97,7 @@ Tilemap::Tilemap(const std::string& yaml_path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Tilemap: " << cols_ << "x" << rows_ << " tiles ("
|
Log::info("Tilemap", "{}x{} tiles ({}x{} px) des de {}", cols_, rows_, width_px_, height_px_, yaml_path);
|
||||||
<< width_px_ << "x" << height_px_ << " px) des de " << yaml_path << '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@@ -12,6 +11,7 @@
|
|||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ namespace {
|
|||||||
std::ofstream dst(path + ".bak", std::ios::binary);
|
std::ofstream dst(path + ".bak", std::ios::binary);
|
||||||
if (!dst) { return; }
|
if (!dst) { return; }
|
||||||
dst << src.rdbuf();
|
dst << src.rdbuf();
|
||||||
std::cout << "Options: còpia de seguretat a " << path << ".bak\n";
|
Log::info("Options", "còpia de seguretat a {}.bak", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseWindow(const fkyaml::node& yaml) {
|
void parseWindow(const fkyaml::node& yaml) {
|
||||||
@@ -463,7 +463,7 @@ namespace {
|
|||||||
try {
|
try {
|
||||||
auto yaml = fkyaml::node::deserialize(CONTENT);
|
auto yaml = fkyaml::node::deserialize(CONTENT);
|
||||||
if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) {
|
if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) {
|
||||||
std::cout << "Options: " << kind_label << " manca `presets:`; reescrivint amb defaults.\n";
|
Log::warn("Options", "{} manca `presets:`; reescrivint amb defaults.", kind_label);
|
||||||
backupFile(path);
|
backupFile(path);
|
||||||
presets = defaults_fn();
|
presets = defaults_fn();
|
||||||
return save_fn();
|
return save_fn();
|
||||||
@@ -475,15 +475,15 @@ namespace {
|
|||||||
presets.push_back(preset);
|
presets.push_back(preset);
|
||||||
}
|
}
|
||||||
if (presets.empty()) {
|
if (presets.empty()) {
|
||||||
std::cout << "Options: " << kind_label << " sense presets; reescrivint amb defaults.\n";
|
Log::warn("Options", "{} sense presets; reescrivint amb defaults.", kind_label);
|
||||||
backupFile(path);
|
backupFile(path);
|
||||||
presets = defaults_fn();
|
presets = defaults_fn();
|
||||||
return save_fn();
|
return save_fn();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "Options: error parsejant " << kind_label << ": " << e.what() << '\n';
|
Log::error("Options", "error parsejant {}: {}", kind_label, e.what());
|
||||||
std::cout << "Options: reescrivint el fitxer amb presets per defecte.\n";
|
Log::info("Options", "reescrivint el fitxer amb presets per defecte.");
|
||||||
backupFile(path);
|
backupFile(path);
|
||||||
presets = defaults_fn();
|
presets = defaults_fn();
|
||||||
return save_fn();
|
return save_fn();
|
||||||
@@ -655,8 +655,7 @@ namespace Options {
|
|||||||
// amb els defaults de la versió nova. El fitxer original queda
|
// amb els defaults de la versió nova. El fitxer original queda
|
||||||
// a `<path>.bak` per si l'usuari volia preservar tuning manual.
|
// a `<path>.bak` per si l'usuari volia preservar tuning manual.
|
||||||
if (file_version != Texts::VERSION) {
|
if (file_version != Texts::VERSION) {
|
||||||
std::cout << "Options: config.yaml versió \"" << file_version
|
Log::warn("Options", "config.yaml versió \"{}\" != \"{}\"; reescrivint amb defaults.", file_version, Texts::VERSION);
|
||||||
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
|
|
||||||
backupFile(g_config_file_path);
|
backupFile(g_config_file_path);
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
@@ -679,8 +678,8 @@ namespace Options {
|
|||||||
if (CHANGED) { saveToFile(); }
|
if (CHANGED) { saveToFile(); }
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
|
Log::error("Options", "error parsejant config.yaml: {}", e.what());
|
||||||
std::cout << "Options: reescrivint el fitxer amb defaults.\n";
|
Log::info("Options", "reescrivint el fitxer amb defaults.");
|
||||||
backupFile(g_config_file_path);
|
backupFile(g_config_file_path);
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
@@ -692,7 +691,7 @@ namespace Options {
|
|||||||
g_config_version = Texts::VERSION;
|
g_config_version = Texts::VERSION;
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n";
|
out << "# Configuració d'AEE ARCADE. Generat automàticament; editable a mà.\n";
|
||||||
out << "version: \"" << g_config_version << "\"\n";
|
out << "version: \"" << g_config_version << "\"\n";
|
||||||
out << "locale: \"" << g_locale << "\"\n\n";
|
out << "locale: \"" << g_locale << "\"\n\n";
|
||||||
|
|
||||||
@@ -780,7 +779,7 @@ namespace Options {
|
|||||||
g_postfx_file_path,
|
g_postfx_file_path,
|
||||||
g_postfx_presets,
|
g_postfx_presets,
|
||||||
&writePostFXPreset,
|
&writePostFXPreset,
|
||||||
"Presets PostFX per a AEE 2026. Editable a mà.",
|
"Presets PostFX per a AEE ARCADE. Editable a mà.",
|
||||||
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
|
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,7 +802,7 @@ namespace Options {
|
|||||||
g_crtpi_file_path,
|
g_crtpi_file_path,
|
||||||
g_crtpi_presets,
|
g_crtpi_presets,
|
||||||
&writeCrtPiPreset,
|
&writeCrtPiPreset,
|
||||||
"Presets CRT-Pi per a AEE 2026. Editable a mà.",
|
"Presets CRT-Pi per a AEE ARCADE. Editable a mà.",
|
||||||
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
|
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,7 +842,7 @@ namespace Options {
|
|||||||
const auto IT = std::ranges::find_if(g_crtpi_presets,
|
const auto IT = std::ranges::find_if(g_crtpi_presets,
|
||||||
[](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
|
[](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
|
||||||
if (IT == g_crtpi_presets.end()) {
|
if (IT == g_crtpi_presets.end()) {
|
||||||
std::cout << "Options: preset CrtPi \"" << g_video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
|
Log::warn("Options", "preset CrtPi \"{}\" no trobat; aplicant defaults.", g_video.shader.current_crtpi_preset);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -871,7 +870,7 @@ namespace Options {
|
|||||||
const auto IT = std::ranges::find_if(g_postfx_presets,
|
const auto IT = std::ranges::find_if(g_postfx_presets,
|
||||||
[](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
|
[](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
|
||||||
if (IT == g_postfx_presets.end()) {
|
if (IT == g_postfx_presets.end()) {
|
||||||
std::cout << "Options: preset PostFX \"" << g_video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
|
Log::warn("Options", "preset PostFX \"{}\" no trobat; aplicant passthrough.", g_video.shader.current_postfx_preset);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#include "game/scenes/game_over_fsm.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Llegeix un sub-camp sencer d'un node YAML. Si manca, retorna el
|
||||||
|
// fallback. Mateix patró que `RoomFsm::loadConfig`.
|
||||||
|
auto readChildInt(const fkyaml::node& parent, const char* key, int fallback) -> int {
|
||||||
|
if (!parent.contains(key)) { return fallback; }
|
||||||
|
return parent[key].get_value<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace GameOverFsm {
|
||||||
|
|
||||||
|
auto loadConfig(const std::string& yaml_path) -> Config {
|
||||||
|
Config cfg{};
|
||||||
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
Log::warn("GameOverFsm", "{} absent — uso fallbacks compilats", yaml_path);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
|
|
||||||
|
if (yaml.contains("game_over")) {
|
||||||
|
const auto& n = yaml["game_over"];
|
||||||
|
cfg.continue_text_enter_ms = readChildInt(n, "continue_text_enter_ms", cfg.continue_text_enter_ms);
|
||||||
|
cfg.continue_text_hold_ms = readChildInt(n, "continue_text_hold_ms", cfg.continue_text_hold_ms);
|
||||||
|
cfg.continue_text_exit_ms = readChildInt(n, "continue_text_exit_ms", cfg.continue_text_exit_ms);
|
||||||
|
cfg.continue_count_start = readChildInt(n, "continue_count_start", cfg.continue_count_start);
|
||||||
|
cfg.game_over_text_hold_ms = readChildInt(n, "game_over_text_hold_ms", cfg.game_over_text_hold_ms);
|
||||||
|
cfg.game_over_text_exit_ms = readChildInt(n, "game_over_text_exit_ms", cfg.game_over_text_exit_ms);
|
||||||
|
cfg.game_over_wait_ms = readChildInt(n, "game_over_wait_ms", cfg.game_over_wait_ms);
|
||||||
|
cfg.fade_out_ms = readChildInt(n, "fade_out_ms", cfg.fade_out_ms);
|
||||||
|
cfg.black_hold_ms = readChildInt(n, "black_hold_ms", cfg.black_hold_ms);
|
||||||
|
if (n.contains("continue_music_speed")) {
|
||||||
|
cfg.continue_music_speed = n["continue_music_speed"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (n.contains("game_over_music_track")) {
|
||||||
|
cfg.game_over_music_track = n["game_over_music_track"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
if (n.contains("countdown_sound_key")) {
|
||||||
|
cfg.countdown_sound_key = n["countdown_sound_key"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
cfg.skip_below_or_equal = readChildInt(n, "skip_below_or_equal", cfg.skip_below_or_equal);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("GameOverFsm", "timings carregats des de {} (continue hold {}ms, fade {}ms)", yaml_path, cfg.continue_text_hold_ms, cfg.fade_out_ms);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State {
|
||||||
|
if (current == State::OFF || current == State::DONE) { return current; }
|
||||||
|
|
||||||
|
t.elapsed_ms += delta_ms;
|
||||||
|
|
||||||
|
if (current == State::CONTINUE_PROMPT) {
|
||||||
|
const std::array<int, 3> DURS = {
|
||||||
|
cfg.continue_text_enter_ms,
|
||||||
|
cfg.continue_text_hold_ms,
|
||||||
|
cfg.continue_text_exit_ms,
|
||||||
|
};
|
||||||
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||||
|
t.elapsed_ms -= DURS[t.phase];
|
||||||
|
// Si acabem la fase 1 (hold) sense cancel·lació, saltem
|
||||||
|
// directament a GAME_OVER — el panel queda amunt i no
|
||||||
|
// es reprodueix l'exit anim. La fase 2 (exit) només
|
||||||
|
// s'executa si el GameScene l'ha forçada amb
|
||||||
|
// `cancelContinuePrompt`.
|
||||||
|
if (t.phase == 1) {
|
||||||
|
return State::GAME_OVER;
|
||||||
|
}
|
||||||
|
++t.phase;
|
||||||
|
}
|
||||||
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::OFF; }
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == State::GAME_OVER) {
|
||||||
|
const std::array<int, 3> DURS = {
|
||||||
|
cfg.game_over_text_hold_ms,
|
||||||
|
cfg.game_over_text_exit_ms,
|
||||||
|
cfg.game_over_wait_ms,
|
||||||
|
};
|
||||||
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||||
|
t.elapsed_ms -= DURS[t.phase];
|
||||||
|
++t.phase;
|
||||||
|
}
|
||||||
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::FADE_OUT; }
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FADE_OUT
|
||||||
|
const std::array<int, 2> DURS = {
|
||||||
|
cfg.fade_out_ms,
|
||||||
|
cfg.black_hold_ms,
|
||||||
|
};
|
||||||
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||||
|
t.elapsed_ms -= DURS[t.phase];
|
||||||
|
++t.phase;
|
||||||
|
}
|
||||||
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::DONE; }
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelContinuePrompt(State current, Transition& t) {
|
||||||
|
if (current != State::CONTINUE_PROMPT) { return; }
|
||||||
|
if (t.phase >= 2) { return; } // ja en exit anim, no-op
|
||||||
|
t.phase = 2;
|
||||||
|
t.elapsed_ms = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void skipToNextCountdown(State current, Transition& t, const Config& cfg) {
|
||||||
|
if (current != State::CONTINUE_PROMPT) { return; }
|
||||||
|
if (t.phase != 1) { return; }
|
||||||
|
const int START = std::max(0, cfg.continue_count_start);
|
||||||
|
const int STEP_COUNT = START + 1;
|
||||||
|
const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEP_COUNT);
|
||||||
|
const int CONSUMED = t.elapsed_ms / STEP_MS;
|
||||||
|
const int CURRENT = std::clamp(START - CONSUMED, 0, START);
|
||||||
|
// No saltem si ja som al 0: el següent "número" és la transició
|
||||||
|
// a GAME_OVER, que `advance` ja gestiona quan el hold s'esgota.
|
||||||
|
if (CURRENT <= 0) { return; }
|
||||||
|
t.elapsed_ms = (CONSUMED + 1) * STEP_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int {
|
||||||
|
if (current != State::CONTINUE_PROMPT) { return 0; }
|
||||||
|
if (t.phase != 1) { return 0; }
|
||||||
|
// La sèrie és [start, start-1, ..., 1, 0]: en total `start+1`
|
||||||
|
// graons. El primer número visible és `start` i l'últim 0 es
|
||||||
|
// manté durant el seu graó abans que la fase 1 acabi i la FSM
|
||||||
|
// salti a GAME_OVER. Si `start == 0` només es mostra "0"
|
||||||
|
// durant tot el hold.
|
||||||
|
const int START = std::max(0, cfg.continue_count_start);
|
||||||
|
const int STEP_COUNT = START + 1;
|
||||||
|
const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEP_COUNT);
|
||||||
|
const int CONSUMED = t.elapsed_ms / STEP_MS;
|
||||||
|
return std::clamp(START - CONSUMED, 0, START);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stateName(State s) -> const char* {
|
||||||
|
switch (s) {
|
||||||
|
case State::OFF:
|
||||||
|
return "OFF";
|
||||||
|
case State::CONTINUE_PROMPT:
|
||||||
|
return "CONTINUE_PROMPT";
|
||||||
|
case State::GAME_OVER:
|
||||||
|
return "GAME_OVER";
|
||||||
|
case State::FADE_OUT:
|
||||||
|
return "FADE_OUT";
|
||||||
|
case State::DONE:
|
||||||
|
return "DONE";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace GameOverFsm
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// FSM secundària de la GameScene que governa el flux "tots els jugadors
|
||||||
|
// fora de partida" — CONTINUE? amb countdown → GAME OVER → fade-to-black
|
||||||
|
// → transició a LOGO. Viu separada de la `RoomFsm` (que gestiona
|
||||||
|
// intro/outro de cada sala) perquè els seus disparadors són ortogonals
|
||||||
|
// i la composició entre les dues queda al GameScene.
|
||||||
|
//
|
||||||
|
// Lògica pura: no toca SDL ni Audio, només temporitzadors. La part
|
||||||
|
// visual (text + panel + fade) viu al GameScene reaprofitant la mateixa
|
||||||
|
// infraestructura del room text.
|
||||||
|
namespace GameOverFsm {
|
||||||
|
|
||||||
|
// Estats canònics. Phase indices per estat:
|
||||||
|
// OFF — joc normal; el GameScene transita a
|
||||||
|
// CONTINUE_PROMPT quan detecta 0 jugadors actius.
|
||||||
|
// CONTINUE_PROMPT — phases: 0=text_enter, 1=text_hold (countdown
|
||||||
|
// visible), 2=text_exit (només si l'usuari
|
||||||
|
// cancel·la pulsant START). Si la fase 1 acaba
|
||||||
|
// sense cancel·lació → salt a GAME_OVER amb el
|
||||||
|
// panel encara amunt (sense exit anim).
|
||||||
|
// GAME_OVER — phases: 0=text_hold (apareix sense enter
|
||||||
|
// anim, panel ja amunt), 1=text_exit, 2=wait
|
||||||
|
// (otro tiempo más abans del fade).
|
||||||
|
// FADE_OUT — phases: 0=fade, 1=black_hold (perquè el
|
||||||
|
// negre quedi un instant abans de cedir el
|
||||||
|
// pas a la LogoScene).
|
||||||
|
// DONE — sentinel: el caller (GameScene) ha de
|
||||||
|
// demanar la transició cap a LOGO.
|
||||||
|
enum class State : std::uint8_t {
|
||||||
|
OFF,
|
||||||
|
CONTINUE_PROMPT,
|
||||||
|
GAME_OVER,
|
||||||
|
FADE_OUT,
|
||||||
|
DONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sub-fase i temps acumulat dins de l'estat actual. Es reseteja a
|
||||||
|
// {0, 0} quan es canvia d'estat (`GameScene::setGameOverState`).
|
||||||
|
struct Transition {
|
||||||
|
int phase{0};
|
||||||
|
int elapsed_ms{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Timings llegits de `data/scenes/game/game.yaml` (secció
|
||||||
|
// `game_over:`). Si el fitxer no porta cap d'aquests camps, els
|
||||||
|
// valors per defecte coincideixen amb els del .yaml versionat.
|
||||||
|
struct Config {
|
||||||
|
// CONTINUE? — animació d'entrada/sortida idèntica al room text.
|
||||||
|
int continue_text_enter_ms{500};
|
||||||
|
// Hold = duració total del countdown. Es divideix en
|
||||||
|
// (continue_count_start + 1) graons iguals; el cas canònic és
|
||||||
|
// 10000 ms / 10 graons d'1 s cadascun.
|
||||||
|
int continue_text_hold_ms{10000};
|
||||||
|
int continue_text_exit_ms{500};
|
||||||
|
// Primer número visible al countdown. La sèrie és
|
||||||
|
// [continue_count_start, ..., 1, 0] — sempre acaba en 0 i el
|
||||||
|
// valor 0 es manté visible durant l'últim graó abans del salt
|
||||||
|
// a GAME_OVER. 9 → "9, 8, ..., 0" (10 graons).
|
||||||
|
int continue_count_start{9};
|
||||||
|
|
||||||
|
// GAME OVER — apareix sense enter anim (el panel ja és amunt
|
||||||
|
// del CONTINUE?) i s'esvaeix amb la mateixa exit anim que el
|
||||||
|
// room text. `wait_ms` és el "otro tiempo más" abans del fade.
|
||||||
|
int game_over_text_hold_ms{2000};
|
||||||
|
int game_over_text_exit_ms{500};
|
||||||
|
int game_over_wait_ms{500};
|
||||||
|
|
||||||
|
// Fade final cap a LOGO. `black_hold_ms` és el temps amb el
|
||||||
|
// canvas negre opac abans que el GameScene demani la transició.
|
||||||
|
int fade_out_ms{500};
|
||||||
|
int black_hold_ms{500};
|
||||||
|
|
||||||
|
// Multiplicador de velocitat de la música mentre dura el
|
||||||
|
// CONTINUE_PROMPT. 1.0 = sense canvi, >1 = més ràpid (i amb el
|
||||||
|
// to més agut: és l'efecte arcade clàssic dels comptes enrere).
|
||||||
|
// Es restableix a 1.0 al sortir del prompt (revive o GAME_OVER).
|
||||||
|
float continue_music_speed{1.5F};
|
||||||
|
|
||||||
|
// Pista de música que sona durant l'estat GAME_OVER (sense loop).
|
||||||
|
// Si la pista no està carregada al Resource::Cache, s'omet la
|
||||||
|
// reproducció i la fase manté els timings configurats. La durada
|
||||||
|
// de la pista determina implícitament `game_over_text_hold_ms`
|
||||||
|
// (vegeu `GameScene::setGameOverState`).
|
||||||
|
std::string 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 el `start`, cada
|
||||||
|
// canvi de número, i quan apareix el 0). NO sona en la
|
||||||
|
// transició posterior cap a GAME_OVER. Buit = sense so.
|
||||||
|
std::string countdown_sound_key{"countdown.wav"};
|
||||||
|
|
||||||
|
// Llindar a partir del qual qualsevol botó de salt o atac (de
|
||||||
|
// qualsevol jugador) salta al següent número del countdown.
|
||||||
|
// Exemple: 7 → mentre el número visible és 7, 6, 5, ..., 1
|
||||||
|
// (no a 0, que ja no té "següent número") un botó d'acció
|
||||||
|
// avança al següent. Si el valor és més gran que `start` el
|
||||||
|
// skip queda desactivat efectivament. -1 = mai.
|
||||||
|
int skip_below_or_equal{7};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Llegeix la configuració des del YAML. Si el fitxer manca o falta
|
||||||
|
// alguna clau, els camps no presents conserven els seus fallbacks.
|
||||||
|
[[nodiscard]] auto loadConfig(const std::string& yaml_path) -> Config;
|
||||||
|
|
||||||
|
// Avança la transició consumint `delta_ms` des del temps real.
|
||||||
|
// Retorna l'estat resultant. Si retorna un estat diferent, el
|
||||||
|
// caller ha de resetejar la `Transition` a {0, 0} abans del següent
|
||||||
|
// frame. Funció pura sobre l'argument.
|
||||||
|
//
|
||||||
|
// Particularitats:
|
||||||
|
// - CONTINUE_PROMPT phase 1 (hold) arribar a la fi → GAME_OVER
|
||||||
|
// (NO reprodueix exit anim; el caller ja té el panel amunt).
|
||||||
|
// - CONTINUE_PROMPT phase 2 (exit) arribar a la fi → OFF (cas
|
||||||
|
// "cancel·lació via START": el GameScene ha forçat la fase 2
|
||||||
|
// amb `cancelContinuePrompt`).
|
||||||
|
// - GAME_OVER phase 2 (wait) arribar a la fi → FADE_OUT.
|
||||||
|
// - FADE_OUT phase 1 (black_hold) arribar a la fi → DONE.
|
||||||
|
// - DONE i OFF no avancen ni canvien d'estat (sumiders).
|
||||||
|
[[nodiscard]] auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State;
|
||||||
|
|
||||||
|
// Salta a la fase 2 (text_exit) del CONTINUE_PROMPT. No fa res si
|
||||||
|
// l'estat no és CONTINUE_PROMPT o ja som a la fase 2 (idempotent).
|
||||||
|
// Disparat pel GameScene quan un jugador pulsa START dins del
|
||||||
|
// countdown — el join real també el fa GameScene; aquesta funció
|
||||||
|
// només dispara la sortida visual del rètol.
|
||||||
|
void cancelContinuePrompt(State current, Transition& t);
|
||||||
|
|
||||||
|
// Quants segons "queden" del countdown de CONTINUE?. Retorna un
|
||||||
|
// enter en el rang [0, continue_count_steps] perquè es renderitzi
|
||||||
|
// com a segona línia del rètol durant la fase 1 (hold). Fora de
|
||||||
|
// CONTINUE_PROMPT phase 1, retorna 0 (no s'hauria de pintar).
|
||||||
|
[[nodiscard]] auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int;
|
||||||
|
|
||||||
|
// Salta al següent número del countdown (decrementa el valor
|
||||||
|
// visible en una unitat). Només actua dins de CONTINUE_PROMPT
|
||||||
|
// phase 1 i quan el valor actual és > 0 — al 0 ja no hi ha
|
||||||
|
// "següent número", la fase ha d'esgotar-se de manera natural i
|
||||||
|
// el caller es recolza en `advance` per al salt a GAME_OVER. La
|
||||||
|
// GameScene comprova també el llindar `skip_below_or_equal`
|
||||||
|
// abans de cridar; aquesta funció, per simplicitat, no en sap.
|
||||||
|
void skipToNextCountdown(State current, Transition& t, const Config& cfg);
|
||||||
|
|
||||||
|
// Nom textual per a logs i tests.
|
||||||
|
[[nodiscard]] auto stateName(State s) -> const char*;
|
||||||
|
|
||||||
|
} // namespace GameOverFsm
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iostream>
|
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
@@ -33,14 +32,18 @@
|
|||||||
#include "game/entities/entity_loader.hpp"
|
#include "game/entities/entity_loader.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
|
#include "game/gameplay_config.hpp"
|
||||||
#include "game/levels/level_manager.hpp"
|
#include "game/levels/level_manager.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
#include "game/scenes/scene_flow.hpp"
|
||||||
|
#include "game/scenes/start_input.hpp"
|
||||||
#include "game/ui/hud.hpp"
|
#include "game/ui/hud.hpp"
|
||||||
#include "game/ui/service_menu.hpp"
|
#include "game/ui/service_menu.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
#include "utils/easing.hpp"
|
#include "utils/easing.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -108,6 +111,11 @@ namespace {
|
|||||||
auto* a = ps[0];
|
auto* a = ps[0];
|
||||||
auto* b = ps[1];
|
auto* b = ps[1];
|
||||||
if (a == nullptr || b == nullptr) { return; }
|
if (a == nullptr || b == nullptr) { return; }
|
||||||
|
// Tweak `players.collide_with_each_other`: amb el flag desactivat
|
||||||
|
// saltem TOT el resolver — head-on, push i bounce aeri queden
|
||||||
|
// anul·lats. La col·lisió contra el món continua intacta perquè
|
||||||
|
// viu a `Player::collidesWithWorld`/`tryMove`, no aquí.
|
||||||
|
if (!GameplayConfig::get().playersCollide()) { return; }
|
||||||
|
|
||||||
const bool A_AIR = a->isAirborne();
|
const bool A_AIR = a->isAirborne();
|
||||||
const bool B_AIR = b->isAirborne();
|
const bool B_AIR = b->isAirborne();
|
||||||
@@ -229,10 +237,10 @@ auto GameScene::defaultConfig() -> Config {
|
|||||||
// concret de sala — només cap a on apunta la progressió ara mateix.
|
// concret de sala — només cap a on apunta la progressió ara mateix.
|
||||||
return {
|
return {
|
||||||
.map_yaml = Levels::LevelManager::get().currentPath(),
|
.map_yaml = Levels::LevelManager::get().currentPath(),
|
||||||
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE,
|
.player_sprite = Defaults::Pyramid::PLAYER_SAM_GIF,
|
||||||
.player_yaml = Defaults::Pyramid::PLAYER_YAML,
|
.player_yaml = Defaults::Pyramid::PLAYER_SAM_YAML,
|
||||||
.player2_sprite = Defaults::Pyramid::PLAYER_SPRITE_P2,
|
.player2_sprite = Defaults::Pyramid::PLAYER_MORCUS_GIF,
|
||||||
.player2_yaml = Defaults::Pyramid::PLAYER_YAML_P2,
|
.player2_yaml = Defaults::Pyramid::PLAYER_MORCUS_YAML,
|
||||||
.music_track = Defaults::Pyramid::MUSIC_TRACK,
|
.music_track = Defaults::Pyramid::MUSIC_TRACK,
|
||||||
.starting_players = DebugConfig::StartingPlayers::BOTH,
|
.starting_players = DebugConfig::StartingPlayers::BOTH,
|
||||||
};
|
};
|
||||||
@@ -262,7 +270,7 @@ GameScene::GameScene(Config config)
|
|||||||
|
|
||||||
const bool USE_P1 = (config_.starting_players == DebugConfig::StartingPlayers::P1 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
const bool USE_P1 = (config_.starting_players == DebugConfig::StartingPlayers::P1 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
||||||
const bool USE_P2 = (config_.starting_players == DebugConfig::StartingPlayers::P2 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
const bool USE_P2 = (config_.starting_players == DebugConfig::StartingPlayers::P2 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
||||||
players_starts_active_ = {USE_P1, USE_P2};
|
players_active_ = {USE_P1, USE_P2};
|
||||||
|
|
||||||
// Carreguem assets per als DOS jugadors sempre, encara que algun no
|
// Carreguem assets per als DOS jugadors sempre, encara que algun no
|
||||||
// arranque actiu: l'slot existeix igualment al vector com a DISABLED
|
// arranque actiu: l'slot existeix igualment al vector com a DISABLED
|
||||||
@@ -290,13 +298,12 @@ GameScene::GameScene(Config config)
|
|||||||
// re-emissions es filtren per estat per no re-entrar mai des d'una
|
// re-emissions es filtren per estat per no re-entrar mai des d'una
|
||||||
// transició ja en marxa.
|
// transició ja en marxa.
|
||||||
entity_manager_.getBus().subscribe("level_complete", [this](const Event& ev) {
|
entity_manager_.getBus().subscribe("level_complete", [this](const Event& ev) {
|
||||||
std::cout << "[LEVEL COMPLETE] player_id=" << ev.player_id << '\n';
|
Log::info("LEVEL COMPLETE", "player_id={}", ev.player_id);
|
||||||
if (state_ == RoomFsm::State::PLAYING) { setState(RoomFsm::State::ROOM_OUTRO); }
|
if (state_ == RoomFsm::State::PLAYING) { setState(RoomFsm::State::ROOM_OUTRO); }
|
||||||
});
|
});
|
||||||
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
|
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
|
||||||
const auto* dmg = std::get_if<int>(&ev.payload);
|
const auto* dmg = std::get_if<int>(&ev.payload);
|
||||||
std::cout << "[PLAYER HIT] player_id=" << ev.player_id
|
Log::info("PLAYER HIT", "player_id={} dmg={}", ev.player_id, dmg != nullptr ? *dmg : 0);
|
||||||
<< " dmg=" << (dmg != nullptr ? *dmg : 0) << '\n';
|
|
||||||
});
|
});
|
||||||
// `player_killed` el publica un hazard letal (mòmia amb
|
// `player_killed` el publica un hazard letal (mòmia amb
|
||||||
// `kills_outright`) després del `player_hit`. Aquí resolem el
|
// `kills_outright`) després del `player_hit`. Aquí resolem el
|
||||||
@@ -306,13 +313,20 @@ GameScene::GameScene(Config config)
|
|||||||
entity_manager_.getBus().subscribe("player_killed", [this](const Event& ev) {
|
entity_manager_.getBus().subscribe("player_killed", [this](const Event& ev) {
|
||||||
dispatchPlayerKilled(ev.player_id);
|
dispatchPlayerKilled(ev.player_id);
|
||||||
});
|
});
|
||||||
|
// `player_disabled` el publica el Player al final del cicle DYING
|
||||||
|
// quan `lives_remaining_` arriba a 0. La feina viu a
|
||||||
|
// `onPlayerDisabled` per evitar pujar la complexitat del ctor; el
|
||||||
|
// HUD ja s'apunta al mateix event per banda.
|
||||||
|
entity_manager_.getBus().subscribe("player_disabled", [this](const Event& ev) {
|
||||||
|
onPlayerDisabled(ev.player_id);
|
||||||
|
});
|
||||||
|
|
||||||
// Polvet: precarreguem el surface del Dust una sola vegada perquè
|
// Polvet: precarreguem el surface del Dust una sola vegada perquè
|
||||||
// els spawns runtime de l'entitat no facin disc al primer salt.
|
// els spawns runtime de l'entitat no facin disc al primer salt.
|
||||||
// També ens subscrivim a `player_jumped` i `player_landed` per
|
// També ens subscrivim a `player_jumped` i `player_landed` per
|
||||||
// spawnejar el polvet de despegue / aterratge a la posició dels
|
// spawnejar el polvet de despegue / aterratge a la posició dels
|
||||||
// peus que el Player ens dóna al payload.
|
// peus que el Player ens dóna al payload.
|
||||||
Resource::Cache::get().loadSurface("dust.gif", "data/entities/dust/dust.gif");
|
Resource::Cache::get().loadSurface("dust.gif", Defaults::Paths::DUST_GIF);
|
||||||
entity_manager_.getBus().subscribe("player_jumped", [this](const Event& ev) {
|
entity_manager_.getBus().subscribe("player_jumped", [this](const Event& ev) {
|
||||||
const auto* pos = std::get_if<WorldPos>(&ev.payload);
|
const auto* pos = std::get_if<WorldPos>(&ev.payload);
|
||||||
if (pos != nullptr) { spawnDust(pos->x, pos->y, false); }
|
if (pos != nullptr) { spawnDust(pos->x, pos->y, false); }
|
||||||
@@ -325,10 +339,10 @@ GameScene::GameScene(Config config)
|
|||||||
// Whip: precarreguem les 4 surfaces (una per cardinal) abans del
|
// Whip: precarreguem les 4 surfaces (una per cardinal) abans del
|
||||||
// primer spawn perquè el Player no facte disc al primer atac.
|
// primer spawn perquè el Player no facte disc al primer atac.
|
||||||
// Subscriu `whip_spawn` per crear el `WhipAttack` corresponent.
|
// Subscriu `whip_spawn` per crear el `WhipAttack` corresponent.
|
||||||
Resource::Cache::get().loadSurface("whip_east.gif", "data/entities/whip_attack/whip_east.gif");
|
Resource::Cache::get().loadSurface("whip_east.gif", Defaults::Paths::WHIP_EAST_GIF);
|
||||||
Resource::Cache::get().loadSurface("whip_west.gif", "data/entities/whip_attack/whip_west.gif");
|
Resource::Cache::get().loadSurface("whip_west.gif", Defaults::Paths::WHIP_WEST_GIF);
|
||||||
Resource::Cache::get().loadSurface("whip_north.gif", "data/entities/whip_attack/whip_north.gif");
|
Resource::Cache::get().loadSurface("whip_north.gif", Defaults::Paths::WHIP_NORTH_GIF);
|
||||||
Resource::Cache::get().loadSurface("whip_south.gif", "data/entities/whip_attack/whip_south.gif");
|
Resource::Cache::get().loadSurface("whip_south.gif", Defaults::Paths::WHIP_SOUTH_GIF);
|
||||||
entity_manager_.getBus().subscribe("whip_spawn", [this](const Event& ev) {
|
entity_manager_.getBus().subscribe("whip_spawn", [this](const Event& ev) {
|
||||||
const auto* p = std::get_if<WhipSpawnPayload>(&ev.payload);
|
const auto* p = std::get_if<WhipSpawnPayload>(&ev.payload);
|
||||||
if (p != nullptr) { spawnWhip(*p); }
|
if (p != nullptr) { spawnWhip(*p); }
|
||||||
@@ -428,7 +442,7 @@ GameScene::GameScene(Config config)
|
|||||||
// tots els jugadors *actius* han travessat la porta. Els slots DISABLED
|
// tots els jugadors *actius* han travessat la porta. Els slots DISABLED
|
||||||
// permanents (single-player) NO compten — d'altra manera la sala mai
|
// permanents (single-player) NO compten — d'altra manera la sala mai
|
||||||
// es completaria, perquè un slot DISABLED no pot eixir per la porta.
|
// es completaria, perquè un slot DISABLED no pot eixir per la porta.
|
||||||
const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);
|
const int ACTIVE_COUNT = std::ranges::count(players_active_, true);
|
||||||
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
||||||
// El snap també empeny lateralment si l'altre jugador ocupa el carril:
|
// El snap també empeny lateralment si l'altre jugador ocupa el carril:
|
||||||
// passem el mateix factor perquè vaja al mateix ritme que el resolver.
|
// passem el mateix factor perquè vaja al mateix ritme que el resolver.
|
||||||
@@ -442,10 +456,17 @@ GameScene::GameScene(Config config)
|
|||||||
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||||||
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||||||
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||||||
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font);
|
// Vides inicials del HUD = `initial_lives` del YAML del jugador
|
||||||
// Si hi ha P2 actiu, activem el seu bloc (per defecte el HUD naix amb
|
// (`Player::loadFrom` ja ha omplert `death_config` abans d'aquí).
|
||||||
// només P1). Mentre els events del bus no porten `player_id`, els hits
|
// Sense això, el marcador es queda fixe a 3 i el YAML perd efecte.
|
||||||
// segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit).
|
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, Player::getInitialLives());
|
||||||
|
// Activem el bloc HUD per als slots que arranquen en partida. Els
|
||||||
|
// inactius queden mostrant el prompt "POLSA START / PER JUGAR" via
|
||||||
|
// `refreshJoinPrompt` cada frame. Tots dos s'haurien de poder
|
||||||
|
// activar (cas BOTH) o un dels dos (cas P1 o P2 sols) — el HUD
|
||||||
|
// naix amb tots dos slots inactius i és aquesta crida la que dóna
|
||||||
|
// l'estat real.
|
||||||
|
if (USE_P1) { hud_->activatePlayer(1); }
|
||||||
if (USE_P2) { hud_->activatePlayer(2); }
|
if (USE_P2) { hud_->activatePlayer(2); }
|
||||||
// Sincronització inicial dels comptadors de piràmide/sala del HUD amb
|
// Sincronització inicial dels comptadors de piràmide/sala del HUD amb
|
||||||
// el LevelManager (després de cada `loadRoom` també es refresca; ací
|
// el LevelManager (després de cada `loadRoom` també es refresca; ací
|
||||||
@@ -479,7 +500,13 @@ GameScene::GameScene(Config config)
|
|||||||
|
|
||||||
// Timings i corbes de la FSM de sala. Llegits del YAML una sola
|
// Timings i corbes de la FSM de sala. Llegits del YAML una sola
|
||||||
// vegada; canvis requereixen reiniciar el joc.
|
// vegada; canvis requereixen reiniciar el joc.
|
||||||
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::CONFIG_YAML);
|
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::GAME_YAML);
|
||||||
|
// Timings de la sub-FSM de game over (CONTINUE? + GAME OVER + fade
|
||||||
|
// final). Comparteix YAML amb la RoomFsm però viu en una secció
|
||||||
|
// pròpia (`game_over:`).
|
||||||
|
game_over_config_ = GameOverFsm::loadConfig(Defaults::GameScene::GAME_YAML);
|
||||||
|
|
||||||
|
preloadGameOverAssets();
|
||||||
|
|
||||||
// Cinemàtica d'entrada de la primera sala: arrenca la pista
|
// Cinemàtica d'entrada de la primera sala: arrenca la pista
|
||||||
// `room_intro.ogg` i arma el callback de fi que farà spawn + salt
|
// `room_intro.ogg` i arma el callback de fi que farà spawn + salt
|
||||||
@@ -489,7 +516,18 @@ GameScene::GameScene(Config config)
|
|||||||
enterRoomIntro();
|
enterRoomIntro();
|
||||||
}
|
}
|
||||||
|
|
||||||
GameScene::~GameScene() = default;
|
GameScene::~GameScene() {
|
||||||
|
// Aturem qualsevol música que estigués sonant al sortir de la
|
||||||
|
// GameScene. Això cobreix tres camins: (a) transició natural a
|
||||||
|
// LOGO al final del game over (l'única audio activa pot ser la
|
||||||
|
// pista `game_over.ogg`, que en aquell moment ja s'ha drenat);
|
||||||
|
// (b) tancament directe via service menu o ALT+F4; (c) qualsevol
|
||||||
|
// futura sortida no-natural. La LogoScene té la seva pròpia
|
||||||
|
// música i no s'ha de mesclar amb la del joc.
|
||||||
|
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||||
|
audio->stopMusic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto GameScene::computeSpawn() -> PlayerSpawn {
|
auto GameScene::computeSpawn() -> PlayerSpawn {
|
||||||
// Centre de la columna del passadís en píxels canvas. La caixa de
|
// Centre de la columna del passadís en píxels canvas. La caixa de
|
||||||
@@ -537,7 +575,7 @@ void GameScene::loadRoom(const std::string& yaml_path) {
|
|||||||
for (auto& p : players_) { p->setMapInfo(INFO); }
|
for (auto& p : players_) { p->setMapInfo(INFO); }
|
||||||
// Tornem a fixar el comptador de "esperats" amb només els slots
|
// Tornem a fixar el comptador de "esperats" amb només els slots
|
||||||
// actius — un slot DISABLED permanent (single-player) no compta.
|
// actius — un slot DISABLED permanent (single-player) no compta.
|
||||||
const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);
|
const int ACTIVE_COUNT = std::ranges::count(players_active_, true);
|
||||||
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
||||||
|
|
||||||
// Tornem tots els slots a DISABLED. Els actius els reactivarà el
|
// Tornem tots els slots a DISABLED. Els actius els reactivarà el
|
||||||
@@ -556,8 +594,7 @@ void GameScene::loadRoom(const std::string& yaml_path) {
|
|||||||
hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom());
|
hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "[ROOM] load " << yaml_path << ": entities reloaded, paint cleared, "
|
Log::info("ROOM", "load {}: entities reloaded, paint cleared, {} active player(s) disabled (awaiting spawn cinematic)", yaml_path, ACTIVE_COUNT);
|
||||||
<< ACTIVE_COUNT << " active player(s) disabled (awaiting spawn cinematic)\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::advanceToNextRoom() {
|
void GameScene::advanceToNextRoom() {
|
||||||
@@ -565,7 +602,7 @@ void GameScene::advanceToNextRoom() {
|
|||||||
if (!mgr.advance()) {
|
if (!mgr.advance()) {
|
||||||
// Política temporal mentre no hi haja pantalla de "Fi de joc":
|
// Política temporal mentre no hi haja pantalla de "Fi de joc":
|
||||||
// arribar al final del levels.yaml fa loop a la primera sala.
|
// arribar al final del levels.yaml fa loop a la primera sala.
|
||||||
std::cout << "[ROOM] final del levels.yaml — bucle a la primera sala\n";
|
Log::info("ROOM", "final del levels.yaml — bucle a la primera sala");
|
||||||
mgr.restart();
|
mgr.restart();
|
||||||
}
|
}
|
||||||
loadRoom(mgr.currentPath());
|
loadRoom(mgr.currentPath());
|
||||||
@@ -576,9 +613,9 @@ void GameScene::enterRoomIntro() {
|
|||||||
// Anotem la transició al log si veníem d'un altre estat; si ja
|
// Anotem la transició al log si veníem d'un altre estat; si ja
|
||||||
// estàvem a INTRO (cas típic d'un salt debug) també queda ben clar.
|
// estàvem a INTRO (cas típic d'un salt debug) també queda ben clar.
|
||||||
if (state_ != RoomFsm::State::ROOM_INTRO) {
|
if (state_ != RoomFsm::State::ROOM_INTRO) {
|
||||||
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> ROOM_INTRO\n";
|
Log::info("ROOM", "{} -> ROOM_INTRO", RoomFsm::stateName(state_));
|
||||||
} else {
|
} else {
|
||||||
std::cout << "[ROOM] ROOM_INTRO restart (cinemàtica reiniciada)\n";
|
Log::info("ROOM", "ROOM_INTRO restart (cinemàtica reiniciada)");
|
||||||
}
|
}
|
||||||
state_ = RoomFsm::State::ROOM_INTRO;
|
state_ = RoomFsm::State::ROOM_INTRO;
|
||||||
transition_ = {};
|
transition_ = {};
|
||||||
@@ -602,7 +639,7 @@ void GameScene::enterRoomIntro() {
|
|||||||
|
|
||||||
auto* audio = Audio::get();
|
auto* audio = Audio::get();
|
||||||
if (audio == nullptr) {
|
if (audio == nullptr) {
|
||||||
std::cout << "[ROOM] INTRO: Audio absent — sense música, timeline només contra text\n";
|
Log::warn("ROOM", "INTRO: Audio absent — sense música, timeline només contra text");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
audio->stopMusic();
|
audio->stopMusic();
|
||||||
@@ -611,10 +648,9 @@ void GameScene::enterRoomIntro() {
|
|||||||
if (Audio::getMusicState() == Audio::MusicState::PLAYING) {
|
if (Audio::getMusicState() == Audio::MusicState::PLAYING) {
|
||||||
const int DUR_MS = Audio::getMusicDurationMs("room_intro.ogg");
|
const int DUR_MS = Audio::getMusicDurationMs("room_intro.ogg");
|
||||||
transition_config_.intro_music_tail_ms = std::max(0, DUR_MS - TEXT_TOTAL_MS);
|
transition_config_.intro_music_tail_ms = std::max(0, DUR_MS - TEXT_TOTAL_MS);
|
||||||
std::cout << "[ROOM] INTRO: música " << DUR_MS << " ms, music_tail "
|
Log::info("ROOM", "INTRO: música {} ms, music_tail {} ms", DUR_MS, transition_config_.intro_music_tail_ms);
|
||||||
<< transition_config_.intro_music_tail_ms << " ms\n";
|
|
||||||
} else {
|
} else {
|
||||||
std::cout << "[ROOM] INTRO: música no arrencada — timeline només contra text\n";
|
Log::warn("ROOM", "INTRO: música no arrencada — timeline només contra text");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,10 +662,10 @@ void GameScene::respawnPlayersInstant() {
|
|||||||
// mateix patró de `triggerPlayerSpawn` però cridem `respawn(x, y)`
|
// mateix patró de `triggerPlayerSpawn` però cridem `respawn(x, y)`
|
||||||
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
|
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
|
||||||
const auto SPAWN = computeSpawn();
|
const auto SPAWN = computeSpawn();
|
||||||
const bool BOTH_ACTIVE = (players_starts_active_[0] && players_starts_active_[1]);
|
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
||||||
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
||||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||||
if (!players_starts_active_[i]) { continue; }
|
if (!players_active_[i]) { continue; }
|
||||||
float x = SINGLE_X;
|
float x = SINGLE_X;
|
||||||
if (BOTH_ACTIVE) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
if (BOTH_ACTIVE) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
||||||
players_[i]->setSpawnPoint(x, SPAWN.y);
|
players_[i]->setSpawnPoint(x, SPAWN.y);
|
||||||
@@ -638,20 +674,20 @@ void GameScene::respawnPlayersInstant() {
|
|||||||
state_ = RoomFsm::State::PLAYING;
|
state_ = RoomFsm::State::PLAYING;
|
||||||
intro_completed_ = true;
|
intro_completed_ = true;
|
||||||
transition_ = {};
|
transition_ = {};
|
||||||
std::cout << "[ROOM] post-editor: spawn instantani, estat -> PLAYING\n";
|
Log::info("ROOM", "post-editor: spawn instantani, estat -> PLAYING");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
||||||
if (slot_index >= players_.size()) { return; }
|
if (slot_index >= players_.size()) { return; }
|
||||||
if (!players_starts_active_[slot_index]) { return; }
|
if (!players_active_[slot_index]) { return; }
|
||||||
if (players_spawned_in_intro_[slot_index]) { return; }
|
if (players_spawned_in_intro_[slot_index]) { return; }
|
||||||
players_spawned_in_intro_[slot_index] = true;
|
players_spawned_in_intro_[slot_index] = true;
|
||||||
|
|
||||||
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
||||||
// la lògica ja queda preparada per a l'itinerari).
|
// la lògica ja queda preparada per a l'itinerari).
|
||||||
const auto SPAWN = computeSpawn();
|
const auto SPAWN = computeSpawn();
|
||||||
const bool BOTH_ACTIVE = (players_starts_active_[0] && players_starts_active_[1]);
|
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
||||||
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
|
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
|
||||||
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
||||||
// amb un sol actiu, el reposicionem al centre exacte.
|
// amb un sol actiu, el reposicionem al centre exacte.
|
||||||
@@ -671,17 +707,114 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
|||||||
// al YAML, l'arma queda buida i el LAND és sec igualment.
|
// al YAML, l'arma queda buida i el LAND és sec igualment.
|
||||||
players_[slot_index]->armNextLandReverb(transition_config_.intro_land_reverb_preset);
|
players_[slot_index]->armNextLandReverb(transition_config_.intro_land_reverb_preset);
|
||||||
|
|
||||||
std::cout << "[ROOM] spawn cinematic P" << (slot_index + 1) << ": airborne entry\n";
|
Log::info("ROOM", "spawn cinematic P{}: airborne entry", slot_index + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::dispatchPlayerKilled(int player_id) {
|
void GameScene::dispatchPlayerKilled(int player_id) {
|
||||||
std::cout << "[PLAYER KILLED] player_id=" << player_id << '\n';
|
Log::info("PLAYER KILLED", "player_id={}", player_id);
|
||||||
const auto IT = std::ranges::find_if(players_, [player_id](const auto& p) {
|
const auto IT = std::ranges::find_if(players_, [player_id](const auto& p) {
|
||||||
return p && p->getId() == player_id;
|
return p && p->getId() == player_id;
|
||||||
});
|
});
|
||||||
if (IT != players_.end()) { (*IT)->enterDying(); }
|
if (IT != players_.end()) { (*IT)->enterDying(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::onPlayerDisabled(int player_id) {
|
||||||
|
Log::info("PLAYER DISABLED", "player_id={}", player_id);
|
||||||
|
if (player_id < 0 || player_id >= static_cast<int>(players_active_.size())) { return; }
|
||||||
|
players_active_[player_id] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::preloadGameOverAssets() const {
|
||||||
|
// Pre-load del so del countdown: el dispara `updateGameOverFlow`
|
||||||
|
// cada vegada que canvia el número visible de CONTINUE? (incloent
|
||||||
|
// l'aparició del 0 — però NO la transició a GAME_OVER). Sense
|
||||||
|
// pre-load, la primera reproducció faria disc amb el countdown ja
|
||||||
|
// en marxa.
|
||||||
|
const auto& key = game_over_config_.countdown_sound_key;
|
||||||
|
if (key.empty()) { return; }
|
||||||
|
Resource::Cache::get().loadSound(
|
||||||
|
key,
|
||||||
|
std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::handleJoinInput() {
|
||||||
|
// El "join in progress" es processa durant PLAYING (regla 1) i,
|
||||||
|
// com a cas especial, durant CONTINUE_PROMPT — ahí un START reviu
|
||||||
|
// el jugador i cancel·la el countdown. A partir de GAME_OVER el
|
||||||
|
// START queda bloquejat (regla 5: ja no es pot pulsar start).
|
||||||
|
const bool IN_PLAYING = (state_ == RoomFsm::State::PLAYING);
|
||||||
|
const bool IN_CONTINUE = (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT);
|
||||||
|
const bool IN_GAME_OVER = (game_over_state_ != GameOverFsm::State::OFF && game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT);
|
||||||
|
if (IN_GAME_OVER) { return; }
|
||||||
|
if (!IN_PLAYING && !IN_CONTINUE) { return; }
|
||||||
|
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||||
|
if (players_active_[i]) { continue; }
|
||||||
|
if (!players_[i]->isDisabled()) { continue; }
|
||||||
|
if (!Input::Detect::pressedStart(static_cast<int>(i))) { continue; }
|
||||||
|
joinPlayer(i);
|
||||||
|
// Si veníem del countdown de CONTINUE?, disparem la sortida
|
||||||
|
// visual del rètol. El jugador ja apareix en escena per la
|
||||||
|
// crida prèvia a `joinPlayer`; el text marxa amb la mateixa
|
||||||
|
// exit anim que el room text.
|
||||||
|
if (IN_CONTINUE) {
|
||||||
|
GameOverFsm::cancelContinuePrompt(game_over_state_, game_over_transition_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::joinPlayer(std::size_t slot_index) {
|
||||||
|
if (slot_index >= players_.size()) { return; }
|
||||||
|
// Spawn lateral SEMPRE: cada jugador entra pel seu costat de la
|
||||||
|
// porta de sortida. Mai el centre (en single-player la cinemàtica
|
||||||
|
// inicial sí que centra el slot actiu, però en un join mid-partida
|
||||||
|
// volem el costat per coherència visual amb la regla 2).
|
||||||
|
const auto SPAWN = computeSpawn();
|
||||||
|
const float X = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x;
|
||||||
|
players_[slot_index]->setSpawnPoint(X, SPAWN.y);
|
||||||
|
players_[slot_index]->resetForJoin();
|
||||||
|
players_[slot_index]->spawnFromAir(X, SPAWN.y);
|
||||||
|
players_active_[slot_index] = true;
|
||||||
|
if (hud_) { hud_->activatePlayer(static_cast<int>(slot_index) + 1); }
|
||||||
|
Log::info("ROOM", "join mid-game P{} -> spawn aerial at ({}, {})", slot_index + 1, X, SPAWN.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GameScene::activePlayerCount() const -> int {
|
||||||
|
int n = 0;
|
||||||
|
for (const auto& p : players_) {
|
||||||
|
if (!p) { continue; }
|
||||||
|
if (!p->isOutOfPlay() || p->isDying()) { ++n; }
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::refreshJoinPrompt() {
|
||||||
|
if (!hud_) { return; }
|
||||||
|
// El game over substitueix la decisió purament basada en RoomFsm:
|
||||||
|
// durant CONTINUE_PROMPT els jugadors encara poden entrar (revive),
|
||||||
|
// a partir de GAME_OVER ja no, així que mostrem WAIT (o NONE) per
|
||||||
|
// indicar que el START ja no farà res fins a la transició a LOGO.
|
||||||
|
Hud::JoinPromptMode mode_inactive = Hud::JoinPromptMode::NONE;
|
||||||
|
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
||||||
|
mode_inactive = Hud::JoinPromptMode::CAN_JOIN;
|
||||||
|
} else if (game_over_state_ != GameOverFsm::State::OFF) {
|
||||||
|
mode_inactive = Hud::JoinPromptMode::WAIT;
|
||||||
|
} else {
|
||||||
|
switch (state_) {
|
||||||
|
case RoomFsm::State::PLAYING:
|
||||||
|
mode_inactive = Hud::JoinPromptMode::CAN_JOIN;
|
||||||
|
break;
|
||||||
|
case RoomFsm::State::ROOM_INTRO:
|
||||||
|
case RoomFsm::State::ROOM_OUTRO:
|
||||||
|
mode_inactive = Hud::JoinPromptMode::WAIT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||||
|
const auto MODE = players_active_[i] ? Hud::JoinPromptMode::NONE : mode_inactive;
|
||||||
|
hud_->setJoinPrompt(static_cast<int>(i) + 1, MODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::completeIntro() {
|
void GameScene::completeIntro() {
|
||||||
if (intro_completed_) { return; }
|
if (intro_completed_) { return; }
|
||||||
intro_completed_ = true;
|
intro_completed_ = true;
|
||||||
@@ -697,9 +830,9 @@ void GameScene::completeIntro() {
|
|||||||
|
|
||||||
void GameScene::spawnDuePlayers() {
|
void GameScene::spawnDuePlayers() {
|
||||||
if (!intro_completed_) { return; }
|
if (!intro_completed_) { return; }
|
||||||
const bool BOTH_ACTIVE = players_starts_active_[0] && players_starts_active_[1];
|
const bool BOTH_ACTIVE = players_active_[0] && players_active_[1];
|
||||||
for (std::size_t i = 0; i < players_starts_active_.size(); ++i) {
|
for (std::size_t i = 0; i < players_active_.size(); ++i) {
|
||||||
if (!players_starts_active_[i]) { continue; }
|
if (!players_active_[i]) { continue; }
|
||||||
if (players_spawned_in_intro_[i]) { continue; }
|
if (players_spawned_in_intro_[i]) { continue; }
|
||||||
// Single-player: el slot actiu (P1 o P2) sempre fa servir
|
// Single-player: el slot actiu (P1 o P2) sempre fa servir
|
||||||
// `player_spawn_delay_ms`. Mode BOTH: P1 fa player_spawn_delay_ms,
|
// `player_spawn_delay_ms`. Mode BOTH: P1 fa player_spawn_delay_ms,
|
||||||
@@ -713,7 +846,7 @@ void GameScene::spawnDuePlayers() {
|
|||||||
|
|
||||||
void GameScene::setState(RoomFsm::State new_state) {
|
void GameScene::setState(RoomFsm::State new_state) {
|
||||||
if (new_state == state_) { return; }
|
if (new_state == state_) { return; }
|
||||||
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n';
|
Log::info("ROOM", "{} -> {}", RoomFsm::stateName(state_), RoomFsm::stateName(new_state));
|
||||||
state_ = new_state;
|
state_ = new_state;
|
||||||
transition_ = {};
|
transition_ = {};
|
||||||
// Reset defensiu dels flags de cicle quan tornem a INTRO via aquest
|
// Reset defensiu dels flags de cicle quan tornem a INTRO via aquest
|
||||||
@@ -742,7 +875,7 @@ void GameScene::setState(RoomFsm::State new_state) {
|
|||||||
const int TEXT_TOTAL_MS = transition_config_.outro_text_enter_ms + transition_config_.outro_text_hold_ms + transition_config_.outro_text_exit_ms;
|
const int TEXT_TOTAL_MS = transition_config_.outro_text_enter_ms + transition_config_.outro_text_hold_ms + transition_config_.outro_text_exit_ms;
|
||||||
transition_config_.outro_music_tail_ms = 0;
|
transition_config_.outro_music_tail_ms = 0;
|
||||||
if (audio == nullptr) {
|
if (audio == nullptr) {
|
||||||
std::cout << "[ROOM] OUTRO: Audio absent — sense música, timeline només contra text\n";
|
Log::warn("ROOM", "OUTRO: Audio absent — sense música, timeline només contra text");
|
||||||
} else {
|
} else {
|
||||||
audio->stopMusic();
|
audio->stopMusic();
|
||||||
audio->setOnMusicEnded({});
|
audio->setOnMusicEnded({});
|
||||||
@@ -750,10 +883,9 @@ void GameScene::setState(RoomFsm::State new_state) {
|
|||||||
if (Audio::getMusicState() == Audio::MusicState::PLAYING) {
|
if (Audio::getMusicState() == Audio::MusicState::PLAYING) {
|
||||||
const int DUR_MS = Audio::getMusicDurationMs("room_outro.ogg");
|
const int DUR_MS = Audio::getMusicDurationMs("room_outro.ogg");
|
||||||
transition_config_.outro_music_tail_ms = std::max(0, DUR_MS - TEXT_TOTAL_MS);
|
transition_config_.outro_music_tail_ms = std::max(0, DUR_MS - TEXT_TOTAL_MS);
|
||||||
std::cout << "[ROOM] OUTRO: música " << DUR_MS << " ms, music_tail "
|
Log::info("ROOM", "OUTRO: música {} ms, music_tail {} ms", DUR_MS, transition_config_.outro_music_tail_ms);
|
||||||
<< transition_config_.outro_music_tail_ms << " ms\n";
|
|
||||||
} else {
|
} else {
|
||||||
std::cout << "[ROOM] OUTRO: música no arrencada — timeline només contra text\n";
|
Log::warn("ROOM", "OUTRO: música no arrencada — timeline només contra text");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -856,18 +988,20 @@ void GameScene::renderRoomText() {
|
|||||||
// l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador
|
// l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador
|
||||||
// i tenir el rètol equilibrat sobre el playfield.
|
// i tenir el rètol equilibrat sobre el playfield.
|
||||||
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
||||||
// Colors del rètol via UiConfig (`data/ui.yaml > colors.room_text` i
|
// Colors del rètol via UiConfig (`data/config/ui.yaml > colors.room_text` i
|
||||||
// `room_text_outline`). Independents dels colors de notificació.
|
// `room_text_outline`). Independents dels colors de notificació.
|
||||||
const Uint8 FILL = UiConfig::get().roomText();
|
const Uint8 FILL = UiConfig::get().roomText();
|
||||||
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
||||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
||||||
|
|
||||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) {
|
const int PHASE = transition_.phase;
|
||||||
|
const int ELAPSED = transition_.elapsed_ms;
|
||||||
|
if (state_ == RoomFsm::State::ROOM_INTRO && PHASE >= 2 && PHASE <= 4) {
|
||||||
const int DUR_ENTER = transition_config_.intro_text_enter_ms;
|
const int DUR_ENTER = transition_config_.intro_text_enter_ms;
|
||||||
const int DUR_EXIT = transition_config_.intro_text_exit_ms;
|
const int DUR_EXIT = transition_config_.intro_text_exit_ms;
|
||||||
// Panel primer (queda sota el text gràcies a l'ordre de blits).
|
// Panel primer (queda sota el text gràcies a l'ordre de blits).
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||||
computeRoomTextPanelHeight(2, 3, 4, DUR_ENTER, DUR_EXIT));
|
computeRoomTextPanelHeight(PHASE, ELAPSED, 2, 3, 4, DUR_ENTER, DUR_EXIT));
|
||||||
|
|
||||||
const auto& mgr = Levels::LevelManager::get();
|
const auto& mgr = Levels::LevelManager::get();
|
||||||
const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid());
|
const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid());
|
||||||
@@ -875,12 +1009,16 @@ void GameScene::renderRoomText() {
|
|||||||
// Direccions oposades: piràmide entra per l'esquerra i surt per
|
// Direccions oposades: piràmide entra per l'esquerra i surt per
|
||||||
// la dreta; sala fa el contrari.
|
// la dreta; sala fa el contrari.
|
||||||
const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING),
|
const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
2,
|
2,
|
||||||
4,
|
4,
|
||||||
DUR_ENTER,
|
DUR_ENTER,
|
||||||
DUR_EXIT,
|
DUR_EXIT,
|
||||||
/*from_right=*/false);
|
/*from_right=*/false);
|
||||||
const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING),
|
const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
2,
|
2,
|
||||||
4,
|
4,
|
||||||
DUR_ENTER,
|
DUR_ENTER,
|
||||||
@@ -892,21 +1030,25 @@ void GameScene::renderRoomText() {
|
|||||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
const int GAP = transition_config_.intro_text_line_gap_px;
|
||||||
drawRoomTextLine(LINE_PYRAMID, X_PYRAMID, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
drawRoomTextLine(LINE_PYRAMID, X_PYRAMID, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||||
drawRoomTextLine(LINE_ROOM, X_ROOM, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
drawRoomTextLine(LINE_ROOM, X_ROOM, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
||||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase >= 0 && transition_.phase <= 2) {
|
} else if (state_ == RoomFsm::State::ROOM_OUTRO && PHASE >= 0 && PHASE <= 2) {
|
||||||
const int DUR_ENTER = transition_config_.outro_text_enter_ms;
|
const int DUR_ENTER = transition_config_.outro_text_enter_ms;
|
||||||
const int DUR_EXIT = transition_config_.outro_text_exit_ms;
|
const int DUR_EXIT = transition_config_.outro_text_exit_ms;
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||||
computeRoomTextPanelHeight(0, 1, 2, DUR_ENTER, DUR_EXIT));
|
computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT));
|
||||||
|
|
||||||
const std::string LINE_1 = Locale::get().text("game.room_complete_line1");
|
const std::string LINE_1 = Locale::get().text("game.room_complete_line1");
|
||||||
const std::string LINE_2 = Locale::get().text("game.room_complete_line2");
|
const std::string LINE_2 = Locale::get().text("game.room_complete_line2");
|
||||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
0,
|
0,
|
||||||
2,
|
2,
|
||||||
DUR_ENTER,
|
DUR_ENTER,
|
||||||
DUR_EXIT,
|
DUR_EXIT,
|
||||||
/*from_right=*/false);
|
/*from_right=*/false);
|
||||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
0,
|
0,
|
||||||
2,
|
2,
|
||||||
DUR_ENTER,
|
DUR_ENTER,
|
||||||
@@ -918,7 +1060,7 @@ void GameScene::renderRoomText() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto GameScene::computeRoomTextX(int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int {
|
auto GameScene::computeRoomTextX(int width, int phase, int elapsed_ms, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int {
|
||||||
// X animada d'una línia: entra des de fora de pantalla, s'atura al
|
// X animada d'una línia: entra des de fora de pantalla, s'atura al
|
||||||
// CENTER_X durant el hold (fora del rang d'aquesta funció — el caller
|
// CENTER_X durant el hold (fora del rang d'aquesta funció — el caller
|
||||||
// garanteix que cau a la branca return final) i surt pel costat
|
// garanteix que cau a la branca return final) i surt pel costat
|
||||||
@@ -928,16 +1070,16 @@ auto GameScene::computeRoomTextX(int width, int phase_enter, int phase_exit, int
|
|||||||
const int OFF_RIGHT = GameCanvas::WIDTH;
|
const int OFF_RIGHT = GameCanvas::WIDTH;
|
||||||
const int OFF_ENTER = from_right ? OFF_RIGHT : OFF_LEFT;
|
const int OFF_ENTER = from_right ? OFF_RIGHT : OFF_LEFT;
|
||||||
const int OFF_EXIT = from_right ? OFF_LEFT : OFF_RIGHT;
|
const int OFF_EXIT = from_right ? OFF_LEFT : OFF_RIGHT;
|
||||||
if (transition_.phase == phase_enter) {
|
if (phase == phase_enter) {
|
||||||
const float T = (dur_enter > 0)
|
const float T = (dur_enter > 0)
|
||||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
? std::clamp(static_cast<float>(elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||||
: 1.0F;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||||
return Easing::lerpInt(OFF_ENTER, GameCanvas::CENTER_X, CURVE(T));
|
return Easing::lerpInt(OFF_ENTER, GameCanvas::CENTER_X, CURVE(T));
|
||||||
}
|
}
|
||||||
if (transition_.phase == phase_exit) {
|
if (phase == phase_exit) {
|
||||||
const float T = (dur_exit > 0)
|
const float T = (dur_exit > 0)
|
||||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
? std::clamp(static_cast<float>(elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||||
: 1.0F;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||||
return Easing::lerpInt(GameCanvas::CENTER_X, OFF_EXIT, CURVE(T));
|
return Easing::lerpInt(GameCanvas::CENTER_X, OFF_EXIT, CURVE(T));
|
||||||
@@ -945,23 +1087,23 @@ auto GameScene::computeRoomTextX(int width, int phase_enter, int phase_exit, int
|
|||||||
return GameCanvas::CENTER_X;
|
return GameCanvas::CENTER_X;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto GameScene::computeRoomTextPanelHeight(int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int {
|
auto GameScene::computeRoomTextPanelHeight(int phase, int elapsed_ms, int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int {
|
||||||
// Altura animada del panel translúcid sota el rètol. Creix de 0 a
|
// Altura animada del panel translúcid sota el rètol. Creix de 0 a
|
||||||
// `text_panel_height_px` amb `text_enter_curve` durant l'enter, es
|
// `text_panel_height_px` amb `text_enter_curve` durant l'enter, es
|
||||||
// manté plena durant el hold i decreix amb `text_exit_curve`
|
// manté plena durant el hold i decreix amb `text_exit_curve`
|
||||||
// durant l'exit. Fora d'aquestes tres fases retorna 0.
|
// durant l'exit. Fora d'aquestes tres fases retorna 0.
|
||||||
const int MAX = transition_config_.text_panel_height_px;
|
const int MAX = transition_config_.text_panel_height_px;
|
||||||
if (transition_.phase == phase_enter) {
|
if (phase == phase_enter) {
|
||||||
const float T = (dur_enter > 0)
|
const float T = (dur_enter > 0)
|
||||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
? std::clamp(static_cast<float>(elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||||
: 1.0F;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||||
return Easing::lerpInt(0, MAX, CURVE(T));
|
return Easing::lerpInt(0, MAX, CURVE(T));
|
||||||
}
|
}
|
||||||
if (transition_.phase == phase_hold) { return MAX; }
|
if (phase == phase_hold) { return MAX; }
|
||||||
if (transition_.phase == phase_exit) {
|
if (phase == phase_exit) {
|
||||||
const float T = (dur_exit > 0)
|
const float T = (dur_exit > 0)
|
||||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
? std::clamp(static_cast<float>(elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||||
: 1.0F;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||||
return Easing::lerpInt(MAX, 0, CURVE(T));
|
return Easing::lerpInt(MAX, 0, CURVE(T));
|
||||||
@@ -994,6 +1136,250 @@ void GameScene::drawRoomTextLine(const std::string& text, int x, int y, std::uin
|
|||||||
outline_color);
|
outline_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::setGameOverState(GameOverFsm::State new_state) {
|
||||||
|
if (new_state == game_over_state_) { return; }
|
||||||
|
const auto PREV = game_over_state_;
|
||||||
|
Log::info("GAMEOVER", "{} -> {}", GameOverFsm::stateName(PREV), GameOverFsm::stateName(new_state));
|
||||||
|
game_over_state_ = new_state;
|
||||||
|
game_over_transition_ = {};
|
||||||
|
|
||||||
|
auto* audio = Audio::get();
|
||||||
|
|
||||||
|
// Sortida de CONTINUE_PROMPT cap a OFF (revive): reinici de la
|
||||||
|
// música de joc des del principi a velocitat normal. Reaprofitem
|
||||||
|
// el track gravat a `config_.music_track` (sempre `playing.ogg`
|
||||||
|
// amb loop infinit). Si la sortida és cap a GAME_OVER, la branca
|
||||||
|
// de més avall ja talla la música i posa `game_over.ogg` — aquí
|
||||||
|
// saltem perquè el stop+play seria redundant.
|
||||||
|
if (PREV == GameOverFsm::State::CONTINUE_PROMPT && new_state == GameOverFsm::State::OFF && audio != nullptr) {
|
||||||
|
audio->stopMusic();
|
||||||
|
audio->playMusic(config_.music_track, -1);
|
||||||
|
audio->setMusicSpeed(1.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_state == GameOverFsm::State::CONTINUE_PROMPT && audio != nullptr) {
|
||||||
|
// Acceleració arcade: stop+play perquè la pista arrenqui des
|
||||||
|
// del principi al ritme accelerat (sense aquest reinici, el
|
||||||
|
// canvi de velocitat sobre la posició actual queda poc
|
||||||
|
// perceptible amb ratios subtils com 1.05). El to també puja:
|
||||||
|
// és l'efecte clàssic dels comptes enrere.
|
||||||
|
audio->stopMusic();
|
||||||
|
audio->playMusic(config_.music_track, -1);
|
||||||
|
audio->setMusicSpeed(game_over_config_.continue_music_speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset del tracker del so del countdown cada vegada que entrem
|
||||||
|
// a CONTINUE_PROMPT, perquè la primera aparició del valor inicial
|
||||||
|
// (`continue_count_start`) dispari el "tick". Fora de
|
||||||
|
// CONTINUE_PROMPT no s'usa.
|
||||||
|
if (new_state == GameOverFsm::State::CONTINUE_PROMPT) {
|
||||||
|
last_countdown_value_ = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_state == GameOverFsm::State::GAME_OVER && audio != nullptr) {
|
||||||
|
// Talla la música actual i arrenca la pista de GAME OVER sense
|
||||||
|
// loop. La fase de hold es redimensiona a la durada real de la
|
||||||
|
// pista perquè el text es mantingui visible mentre dura la
|
||||||
|
// música; l'exit anim, el wait i el fade arrenquen quan la
|
||||||
|
// música ja s'ha drenat.
|
||||||
|
audio->stopMusic();
|
||||||
|
const auto& track = game_over_config_.game_over_music_track;
|
||||||
|
if (!track.empty()) {
|
||||||
|
audio->playMusic(track, 0);
|
||||||
|
const int DUR_MS = Audio::getMusicDurationMs(track);
|
||||||
|
if (DUR_MS > 0) {
|
||||||
|
game_over_config_.game_over_text_hold_ms = DUR_MS;
|
||||||
|
Log::info("GAMEOVER", "{}: durada {} ms (text_hold sincronitzat)", track, DUR_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_state == GameOverFsm::State::FADE_OUT) {
|
||||||
|
// Arma el fade en el moment que entrem a la fase de fade-out.
|
||||||
|
// Mateix patró que `outro_fade_started_` per a OUTRO de sala.
|
||||||
|
game_over_fade_.begin(game_over_config_.fade_out_ms,
|
||||||
|
Transitions::FadeToBlack::Direction::FADE_OUT);
|
||||||
|
game_over_fade_started_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::updateGameOverFlow(int delta_ms) {
|
||||||
|
// Tick del fade visual: és no-op si encara no s'ha armat (begin
|
||||||
|
// sense duration → tick=no-op gràcies al clamp intern).
|
||||||
|
game_over_fade_.tick(delta_ms);
|
||||||
|
|
||||||
|
// Detecció d'entrada a CONTINUE_PROMPT: tots els jugadors fora de
|
||||||
|
// partida durant PLAYING. Només es dispara una vegada — `setGameOverState`
|
||||||
|
// és idempotent davant del mateix estat. Es vol que la transició
|
||||||
|
// sigui exclusiva de PLAYING perquè un join mid-INTRO/OUTRO ja no
|
||||||
|
// és possible (vegeu `handleJoinInput`); permetent CONTINUE? fora
|
||||||
|
// de PLAYING obriríem races amb la cinemàtica de sala.
|
||||||
|
if (game_over_state_ == GameOverFsm::State::OFF && state_ == RoomFsm::State::PLAYING && activePlayerCount() == 0) {
|
||||||
|
setGameOverState(GameOverFsm::State::CONTINUE_PROMPT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (game_over_state_ == GameOverFsm::State::OFF) { return; }
|
||||||
|
|
||||||
|
const auto NEXT = GameOverFsm::advance(game_over_state_, game_over_transition_, game_over_config_, delta_ms);
|
||||||
|
if (NEXT != game_over_state_) {
|
||||||
|
setGameOverState(NEXT);
|
||||||
|
// En arribar a DONE, demanem la transició a LOGO. La construcció
|
||||||
|
// de la nova escena la fa el Director llegint `exit_request_`.
|
||||||
|
if (NEXT == GameOverFsm::State::DONE) {
|
||||||
|
exit_request_ = ExitRequest{.next = SceneId::LOGO, .params = LogoParams{}};
|
||||||
|
Log::info("GAMEOVER", "DONE -> requesting LOGO scene");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip i tick del so del countdown viuen en helpers per acotar
|
||||||
|
// la complexitat cognitiva d'aquesta funció. Skip primer perquè
|
||||||
|
// el tick del so soni amb el valor nou si la pulsació ha avançat.
|
||||||
|
handleCountdownSkip();
|
||||||
|
emitCountdownTickIfChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::handleCountdownSkip() {
|
||||||
|
if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT) { return; }
|
||||||
|
if (game_over_transition_.phase != 1) { return; }
|
||||||
|
const int CURRENT = GameOverFsm::countdownSecondsRemaining(
|
||||||
|
game_over_state_,
|
||||||
|
game_over_transition_,
|
||||||
|
game_over_config_);
|
||||||
|
if (CURRENT <= 0) { return; }
|
||||||
|
if (CURRENT > game_over_config_.skip_below_or_equal) { return; }
|
||||||
|
const bool ANY_ACTION = Input::Detect::pressedActionButton(0) || Input::Detect::pressedActionButton(1);
|
||||||
|
if (!ANY_ACTION) { return; }
|
||||||
|
GameOverFsm::skipToNextCountdown(
|
||||||
|
game_over_state_,
|
||||||
|
game_over_transition_,
|
||||||
|
game_over_config_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::emitCountdownTickIfChanged() {
|
||||||
|
if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT) { return; }
|
||||||
|
if (game_over_transition_.phase != 1) { return; }
|
||||||
|
const int CURRENT = GameOverFsm::countdownSecondsRemaining(
|
||||||
|
game_over_state_,
|
||||||
|
game_over_transition_,
|
||||||
|
game_over_config_);
|
||||||
|
if (CURRENT == last_countdown_value_) { return; }
|
||||||
|
last_countdown_value_ = CURRENT;
|
||||||
|
const auto& key = game_over_config_.countdown_sound_key;
|
||||||
|
if (key.empty()) { return; }
|
||||||
|
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||||
|
audio->playSound(key, Audio::Group::INTERFACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::renderGameOverText() {
|
||||||
|
if (!room_text_font_) { return; }
|
||||||
|
if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT && game_over_state_ != GameOverFsm::State::GAME_OVER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int KERNING = 0;
|
||||||
|
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
||||||
|
const Uint8 FILL = UiConfig::get().roomText();
|
||||||
|
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
||||||
|
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
||||||
|
const int PHASE = game_over_transition_.phase;
|
||||||
|
const int ELAPSED = game_over_transition_.elapsed_ms;
|
||||||
|
|
||||||
|
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
||||||
|
const int DUR_ENTER = game_over_config_.continue_text_enter_ms;
|
||||||
|
const int DUR_EXIT = game_over_config_.continue_text_exit_ms;
|
||||||
|
// Phases del CONTINUE_PROMPT: 0=enter, 1=hold, 2=exit. El panel
|
||||||
|
// segueix la mateixa corba enter/hold/exit que el room text.
|
||||||
|
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||||
|
computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT));
|
||||||
|
|
||||||
|
const std::string LINE_1 = Locale::get().text("game.continue_prompt");
|
||||||
|
const int SECONDS = GameOverFsm::countdownSecondsRemaining(
|
||||||
|
game_over_state_,
|
||||||
|
game_over_transition_,
|
||||||
|
game_over_config_);
|
||||||
|
// Línia 2 només té text durant la fase de hold (countdown). En
|
||||||
|
// les fases d'enter i exit deixem només el "CONTINUE?" perquè
|
||||||
|
// l'animació dels números no quedi rara.
|
||||||
|
const std::string LINE_2 = (PHASE == 1) ? std::to_string(SECONDS) : std::string{};
|
||||||
|
|
||||||
|
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
DUR_ENTER,
|
||||||
|
DUR_EXIT,
|
||||||
|
/*from_right=*/false);
|
||||||
|
const int X_2 = computeRoomTextX(LINE_2.empty() ? 0 : room_text_font_->length(LINE_2, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
DUR_ENTER,
|
||||||
|
DUR_EXIT,
|
||||||
|
/*from_right=*/true);
|
||||||
|
const int GAP = transition_config_.intro_text_line_gap_px;
|
||||||
|
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||||
|
if (!LINE_2.empty()) {
|
||||||
|
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GAME_OVER: panel sempre amunt durant phase 0/1, decreix en phase 1
|
||||||
|
// (exit). Phase 2 (wait) sense panel ni text — només temps mort
|
||||||
|
// abans del fade.
|
||||||
|
const int DUR_HOLD = game_over_config_.game_over_text_hold_ms;
|
||||||
|
const int DUR_EXIT = game_over_config_.game_over_text_exit_ms;
|
||||||
|
if (PHASE > 1) { return; }
|
||||||
|
|
||||||
|
// Panel: phase 0 = ple, phase 1 = decreix. Reaprofitem
|
||||||
|
// computeRoomTextPanelHeight passant un phase_enter "fora de rang"
|
||||||
|
// (-1) perquè la branca d'enter no s'activi mai.
|
||||||
|
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||||
|
computeRoomTextPanelHeight(PHASE, ELAPSED, /*phase_enter=*/-1,
|
||||||
|
/*phase_hold=*/0,
|
||||||
|
/*phase_exit=*/1,
|
||||||
|
/*dur_enter=*/0,
|
||||||
|
DUR_EXIT));
|
||||||
|
|
||||||
|
const std::string LINE_1 = Locale::get().text("game.game_over_line1");
|
||||||
|
const std::string LINE_2 = Locale::get().text("game.game_over_line2");
|
||||||
|
// Phase 0 (hold): text quiet al CENTER_X. Phase 1 (exit): el text
|
||||||
|
// marxa pels costats com el room outro. Reaprofitem computeRoomTextX
|
||||||
|
// amb phase_enter=-1 (no enter anim) i phase_exit=1.
|
||||||
|
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
|
/*phase_enter=*/-1,
|
||||||
|
/*phase_exit=*/1,
|
||||||
|
/*dur_enter=*/0,
|
||||||
|
DUR_EXIT,
|
||||||
|
/*from_right=*/false);
|
||||||
|
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
|
/*phase_enter=*/-1,
|
||||||
|
/*phase_exit=*/1,
|
||||||
|
/*dur_enter=*/0,
|
||||||
|
DUR_EXIT,
|
||||||
|
/*from_right=*/true);
|
||||||
|
const int GAP = transition_config_.outro_text_line_gap_px;
|
||||||
|
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||||
|
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
||||||
|
|
||||||
|
(void)DUR_HOLD; // Reservat per a futurs efectes de hold (pulse, etc.).
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::renderGameOverFade() {
|
||||||
|
// Sense `begin` previ, `tick` és no-op i `render` pinta alpha=0
|
||||||
|
// (vegeu `Transitions::FadeToBlack`). Així el callsite és
|
||||||
|
// incondicional sense malbaratar pixel ops fora del flux.
|
||||||
|
if (!game_over_fade_started_) { return; }
|
||||||
|
game_over_fade_.render();
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
||||||
// Mida del sprite de Dust llegida del seu YAML. Hardcoded per
|
// Mida del sprite de Dust llegida del seu YAML. Hardcoded per
|
||||||
// simplificar — si canvia, només cal tocar dust.yaml + aquesta
|
// simplificar — si canvia, només cal tocar dust.yaml + aquesta
|
||||||
@@ -1075,7 +1461,7 @@ void GameScene::handlePauseInput() {
|
|||||||
pause_.curve = GOING_TO_PAUSE ? UiConfig::get().pauseEnterCurve() : UiConfig::get().pauseExitCurve();
|
pause_.curve = GOING_TO_PAUSE ? UiConfig::get().pauseEnterCurve() : UiConfig::get().pauseExitCurve();
|
||||||
pause_.state = GOING_TO_PAUSE ? PauseState::ENTERING : PauseState::EXITING;
|
pause_.state = GOING_TO_PAUSE ? PauseState::ENTERING : PauseState::EXITING;
|
||||||
|
|
||||||
std::cout << "[PAUSE] " << (GOING_TO_PAUSE ? "entering" : "exiting") << " from_scale=" << pause_.from_scale << " duration_ms=" << pause_.duration_ms << '\n';
|
Log::info("PAUSE", "{} from_scale={} duration_ms={}", GOING_TO_PAUSE ? "entering" : "exiting", pause_.from_scale, pause_.duration_ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,6 +1552,12 @@ void GameScene::update(int delta_ms) {
|
|||||||
// arrenque ja sigui efectiu en el mateix frame.
|
// arrenque ja sigui efectiu en el mateix frame.
|
||||||
updateRoomTransition(delta_ms);
|
updateRoomTransition(delta_ms);
|
||||||
|
|
||||||
|
// Sub-FSM de game over (CONTINUE? + GAME OVER + fade-to-LOGO).
|
||||||
|
// També en temps real: el countdown de CONTINUE? no es congela
|
||||||
|
// amb la pausa de gameplay. La detecció de "tots fora de partida"
|
||||||
|
// s'arma dins quan estem a PLAYING.
|
||||||
|
updateGameOverFlow(delta_ms);
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
// Toggle de l'editor: només si no hi ha cap transició de pausa ni
|
// Toggle de l'editor: només si no hi ha cap transició de pausa ni
|
||||||
// pausa consolidada. L'editor mateix no consumeix la tecla — la
|
// pausa consolidada. L'editor mateix no consumeix la tecla — la
|
||||||
@@ -1212,11 +1604,25 @@ void GameScene::update(int delta_ms) {
|
|||||||
// (creuar → player_exited → level_complete → OUTRO) es manté.
|
// (creuar → player_exited → level_complete → OUTRO) es manté.
|
||||||
// Mateixos gates que el salt entre sales.
|
// Mateixos gates que el salt entre sales.
|
||||||
if (pause_.state == PauseState::IDLE && !editor_.active() && detectOpenDoor()) {
|
if (pause_.state == PauseState::IDLE && !editor_.active() && detectOpenDoor()) {
|
||||||
std::cout << "[DEBUG] força obertura de la porta\n";
|
Log::info("DEBUG", "força obertura de la porta");
|
||||||
entity_manager_.forceOpenDoor();
|
entity_manager_.forceOpenDoor();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Join in progress (regla 1): un slot DISABLED pot entrar a partida
|
||||||
|
// pulsant START durant PLAYING. Es processa abans del bloc de món
|
||||||
|
// per que el spawn aeri ja sigui efectiu en aquest mateix frame.
|
||||||
|
// Gateat per pausa/menú/editor: no es vol que un START accidental
|
||||||
|
// mentre l'usuari navega per un menú trigueri un join.
|
||||||
|
#ifdef _DEBUG
|
||||||
|
const bool EDITOR_OPEN = editor_.active();
|
||||||
|
#else
|
||||||
|
constexpr bool EDITOR_OPEN = false;
|
||||||
|
#endif
|
||||||
|
if (pause_.state == PauseState::IDLE && !MENU_FROZEN && !EDITOR_OPEN) {
|
||||||
|
handleJoinInput();
|
||||||
|
}
|
||||||
|
|
||||||
GlobalInputs::update(delta_ms);
|
GlobalInputs::update(delta_ms);
|
||||||
if (GlobalInputs::wantsQuit()) { wants_quit_ = true; }
|
if (GlobalInputs::wantsQuit()) { wants_quit_ = true; }
|
||||||
|
|
||||||
@@ -1277,6 +1683,11 @@ void GameScene::update(int delta_ms) {
|
|||||||
|
|
||||||
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
|
|
||||||
|
// Refresc del prompt al HUD per a slots inactius. Es fa al final per
|
||||||
|
// recollir els canvis d'aquest frame (joins, deaths, transició de
|
||||||
|
// RoomFsm) abans del proper render.
|
||||||
|
refreshJoinPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::render() {
|
void GameScene::render() {
|
||||||
@@ -1375,6 +1786,14 @@ void GameScene::render() {
|
|||||||
// Text de la transició de sala (entrada o sortida): per damunt del
|
// Text de la transició de sala (entrada o sortida): per damunt del
|
||||||
// HUD perquè és UI de transició i ha de ser sempre llegible.
|
// HUD perquè és UI de transició i ha de ser sempre llegible.
|
||||||
renderRoomText();
|
renderRoomText();
|
||||||
|
|
||||||
|
// Rètol de game over (CONTINUE? + countdown / GAME OVER) — comparteix
|
||||||
|
// panel i font amb el room text però viu en una sub-FSM independent.
|
||||||
|
renderGameOverText();
|
||||||
|
|
||||||
|
// Fade-to-black final del game over: tapeix tot el canvas (HUD
|
||||||
|
// inclòs) durant la fase FADE_OUT i el black hold posterior.
|
||||||
|
renderGameOverFade();
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
|
|||||||