Compare commits

..
32 Commits
Author SHA1 Message Date
JailDesigner fd513f3bdb nou icon temporal 2026-06-01 18:02:00 +02:00
JailDesigner ebb15d32be merge fix/neteja-warnings: neteja de warnings 2026-05-31 00:26:53 +02:00
JailDesigner 53174f11f9 fix: silencia -Wtautological-compare de stb_vorbis al cmake 2026-05-31 00:26:53 +02:00
JailDesigner da923ae421 merge docs/arquitectura: guia d'arquitectura del projecte 2026-05-29 13:07:44 +02:00
JailDesigner ef496bd971 docs(arquitectura): afig guia d'arquitectura del projecte 2026-05-29 13:07:23 +02:00
JailDesigner 3db0ed4457 merge reinici-real-servei: reinici real via execv + neteja fake reset 2026-05-25 17:36:17 +02:00
JailDesigner 0c6bebe8dc refactor(servei): elimina infra del fake reset 2026-05-25 17:34:32 +02:00
JailDesigner a8ccb3fae1 feat(servei): reinici real via execv 2026-05-25 17:29:42 +02:00
JailDesigner 774874deb0 merge cleanup-includes: neteja includes no utilitzats 2026-05-25 17:14:16 +02:00
JailDesigner 24afdef7cd chore(includes): neteja includes no utilitzats (clang-include-cleaner) 2026-05-25 17:07:24 +02:00
JailDesigner f29cc7318a merge audita-nolint: 30→9 NOLINT (useStlAlgorithm refactors + neteja obsolets) 2026-05-18 17:21:45 +02:00
JailDesigner 7a859c1e44 neteja NOLINT obsolets (audio 14 + player + ajusta justificacions) 2026-05-18 16:41:15 +02:00
JailDesigner 822bed0748 useStlAlgorithm: ranges algorithms + justifica returnDanglingLifetime 2026-05-18 16:28:36 +02:00
JailDesigner d7db02bafd treu include <ranges> no utilitzat (std::ranges::fill és a <algorithm>) 2026-05-17 13:25:57 +02:00
JailDesigner b55473ae0b merge: chroma min/max amb mostreig bilinear 2026-05-17 12:32:53 +02:00
JailDesigner 35d3b359eb chroma min/max amb mostreig bilinear subpíxel 2026-05-17 12:24:22 +02:00
JailDesigner b81a453b32 feat(log): printStatus amb claudàtor acolorit al davant 2026-05-14 19:02:43 +02:00
JailDesigner 7a1261202c targets _*_release en kebab 2026-05-14 17:26:17 +02:00
JailDesigner f0c118e6fa estén la detecció de Ninja a Linux/macOS 2026-05-14 16:23:50 +02:00
JailDesigner bc5a6abf53 feat(menu): teclat amb submenú per jugador 2026-05-14 10:29:34 +02:00
JailDesigner 4daeb0e15c feat(menu): ESC tanca el joc també amb el menú obert 2026-05-14 10:21:33 +02:00
JailDesigner b6403e8de5 feat(ui): outline per slot al text del menú 2026-05-14 10:11:50 +02:00
JailDesigner 0e8a195ddd feat(ui): temes de color del menú amb tema egípci per defecte 2026-05-14 09:39:46 +02:00
JailDesigner 80140434e7 refactor(input): elimina InputState vestigial 2026-05-14 09:08:45 +02:00
JailDesigner fd3da8cb99 feat(menu): F-keys passen a GlobalInputs amb menú obert 2026-05-14 09:02:03 +02:00
JailDesigner 885af944c5 fix(lighting): corregeix il·luminació a p1r2 2026-05-14 07:48:00 +02:00
JailDesigner 2e41815d3c Merge branch 'refactor/pyramid-yaml-base' 2026-05-13 22:44:42 +02:00
JailDesigner 125cdd0c74 refactor(maps): pyramid.yaml per piràmide amb merge a sala 2026-05-13 22:38:42 +02:00
JailDesigner 5f206e03c8 tweak(r1): radi 23 i noise_freq 0 a antorxes 2026-05-13 22:03:39 +02:00
JailDesigner fa77f4cee3 chore(version): bump 0.45.0 -> 0.46.0 2026-05-13 21:43:00 +02:00
JailDesigner a02706ac8b Merge branch 'refactor/player-instance-config' 2026-05-13 21:42:49 +02:00
JailDesigner 2aef925286 refactor(player): config a membres d'instancia (P1/P2 YAMLs independents) 2026-05-13 21:40:02 +02:00
112 changed files with 11004 additions and 9063 deletions

No files matched your search

+5 -2
View File
@@ -65,6 +65,7 @@ set(ENTITY_SOURCES
set(APP_SOURCES set(APP_SOURCES
source/main.cpp source/main.cpp
source/core/system/director.cpp source/core/system/director.cpp
source/core/system/relaunch.cpp
source/core/audio/audio.cpp source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp source/core/audio/audio_adapter.cpp
source/core/audio/audio_effects.cpp source/core/audio/audio_effects.cpp
@@ -74,7 +75,6 @@ set(APP_SOURCES
source/core/ui/ui_config.cpp source/core/ui/ui_config.cpp
source/core/input/key_config.cpp source/core/input/key_config.cpp
source/core/input/global_inputs.cpp source/core/input/global_inputs.cpp
source/core/input/input_state.cpp
source/core/input/gamepad_manager.cpp source/core/input/gamepad_manager.cpp
source/core/input/gamepad_bindings.cpp source/core/input/gamepad_bindings.cpp
source/core/input/define_buttons.cpp source/core/input/define_buttons.cpp
@@ -131,6 +131,7 @@ set(APP_SOURCES
# al TilemapWriter. Només Debug. # al TilemapWriter. Només Debug.
source/game/map/map_writer.cpp source/game/map/map_writer.cpp
source/game/map/paint_layer.cpp source/game/map/paint_layer.cpp
source/game/map/room_yaml.cpp
source/game/levels/level_manager.cpp source/game/levels/level_manager.cpp
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`). # Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
# En Release els fitxers compilen a object buit i l'enllaçador no # En Release els fitxers compilen a object buit i l'enllaçador no
@@ -204,7 +205,7 @@ add_executable(${PROJECT_NAME} ${APP_SOURCES})
set_source_files_properties( set_source_files_properties(
source/external/stb_image_write_impl.cpp source/external/stb_image_write_impl.cpp
source/external/stb_vorbis_impl.cpp source/external/stb_vorbis_impl.cpp
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations" PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations;-Wno-tautological-compare"
) )
if(NOT APPLE AND GLSLC_EXE) if(NOT APPLE AND GLSLC_EXE)
@@ -374,6 +375,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/game/map/map.cpp source/game/map/map.cpp
source/game/map/room_yaml.cpp
source/utils/log.cpp source/utils/log.cpp
${ENTITY_SOURCES_BASE} ${ENTITY_SOURCES_BASE}
) )
@@ -405,6 +407,7 @@ if(BUILD_TESTS)
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/game/gameplay_config.cpp source/game/gameplay_config.cpp
source/game/map/map.cpp source/game/map/map.cpp
source/game/map/room_yaml.cpp
source/utils/log.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
+15 -8
View File
@@ -27,7 +27,14 @@ ifeq ($(OS),Windows_NT)
else else
NULDEV := /dev/null NULDEV := /dev/null
JOBS ?= $(shell nproc 2>/dev/null || echo 4) JOBS ?= $(shell nproc 2>/dev/null || echo 4)
CMAKE_GEN := # Si Ninja està al PATH, l'usem (més ràpid i paral·lelitza millor).
# Si no, deixem que CMake usi el generador per defecte (Unix Makefiles).
HAS_NINJA := $(shell ninja --version 2>/dev/null)
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN :=
endif
endif endif
# VERSION (única font de veritat: source/utils/defines.hpp). # VERSION (única font de veritat: source/utils/defines.hpp).
@@ -84,7 +91,7 @@ else
MKDIR := mkdir -p MKDIR := mkdir -p
endif endif
.PHONY: all debug release _windows_release _macos_release _linux_release run run-debug clean rebuild show-version pack controllerdb format format-check tidy tidy-fix cppcheck hooks-install help .PHONY: all debug release _windows-release _macos-release _linux-release run run-debug clean rebuild show-version pack controllerdb format format-check tidy tidy-fix cppcheck hooks-install help
# ============================================================================== # ==============================================================================
# COMPILACIÓ # COMPILACIÓ
@@ -115,19 +122,19 @@ run-debug: debug
# resultat a $(DIST_DIR)/. # resultat a $(DIST_DIR)/.
release: release:
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
@"$(MAKE)" _windows_release @"$(MAKE)" _windows-release
else else
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
@$(MAKE) _macos_release @$(MAKE) _macos-release
else else
@$(MAKE) _linux_release @$(MAKE) _linux-release
endif endif
endif endif
# ============================================================================== # ==============================================================================
# RELEASE — Linux (.tar.gz) # RELEASE — Linux (.tar.gz)
# ============================================================================== # ==============================================================================
_linux_release: _linux-release:
@echo "Creando release para Linux - Version: $(VERSION)" @echo "Creando release para Linux - Version: $(VERSION)"
# Compila Release (genera shaders, resources.pack i binari) # Compila Release (genera shaders, resources.pack i binari)
@@ -158,7 +165,7 @@ _linux_release:
# ============================================================================== # ==============================================================================
# RELEASE — Windows (.zip) # RELEASE — Windows (.zip)
# ============================================================================== # ==============================================================================
_windows_release: _windows-release:
@echo off @echo off
@echo Creando release para Windows - Version: $(VERSION) @echo Creando release para Windows - Version: $(VERSION)
@@ -191,7 +198,7 @@ _windows_release:
# ============================================================================== # ==============================================================================
# RELEASE — macOS (.dmg per Intel + Apple Silicon) # RELEASE — macOS (.dmg per Intel + Apple Silicon)
# ============================================================================== # ==============================================================================
_macos_release: _macos-release:
@echo "Creando release para macOS - Version: $(VERSION)" @echo "Creando release para macOS - Version: $(VERSION)"
# Verificar/instal·lar create-dmg si cal # Verificar/instal·lar create-dmg si cal
+43 -7
View File
@@ -9,13 +9,6 @@ colors:
notification_bg: 64 notification_bg: 64
exit_text: 71 exit_text: 71
exit_bg: 2 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_bg: 9
hud_flash: 63 hud_flash: 63
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents # Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
@@ -27,6 +20,49 @@ colors:
room_text: 55 # or brillant (#FFDA00, = hud_center.value) room_text: 55 # or brillant (#FFDA00, = hud_center.value)
room_text_outline: 10 # marró cremat (#482400, = hud_center.shadow) room_text_outline: 10 # marró cremat (#482400, = hud_center.shadow)
# Tema actiu del menú de servei. Ha de coincidir amb una clau de
# `menu_themes`; si manca o no existeix, cada slot cau al default
# compilat a Defaults::UI (= tema "classic"). Canviar de tema requereix
# reiniciar el joc — la càrrega de paleta es fa només en arrencar.
menu_theme: egyptian
# Conjunts de colors del menú de servei. Cada tema defineix els 7 slots
# (sense prefix `menu_` perquè ja som dins del bloc del tema). Si manca
# alguna clau dins d'un tema, cau al default compilat. Per afegir un
# tema nou, només cal una nova entrada aquí i posar-ne el nom a
# `menu_theme`.
menu_themes:
# Tema històric (defaults compilats). Cian + verd llima + magenta —
# paleta arcade clàssica, freda i amb molt contrast.
classic:
bg: 2 # marró-vermell fosc (#480000)
border: 232 # cian brillant (#00B6FF)
title: 237 # lavanda (#B6B6FF)
title_outline: 64 # navy fosc (#0000AA) — halo fred del títol
label: 58 # verd llima (#48FF00)
label_outline: 17 # verd molt fosc (#244800) — bord same-family del label
value: 232 # cian brillant (#00B6FF) — coincideix amb el border
value_outline: 64 # navy fosc (#0000AA) — halo cian
cursor: 71 # magenta-vermell (#FF0055)
cursor_outline: 1 # granat fosc (#240000) — halo càlid del cursor
disabled: 16 # verd fosc (#004800)
# Tema "egípci": família or/ambre/marró coherent amb el HUD i el
# rètol de transició de sala (room_text). El bg comparteix to amb
# `room_text_outline` i `hud_center.shadow` perquè el menú "creixi"
# del mateix món visual que el joc.
egyptian:
bg: 10 # marró cremat (#482400)
border: 55 # or brillant (#FFDA00)
title: 127 # groc llimona viu (#FFFF55)
title_outline: 19 # oliva fosc (#6D4800) — halo càlid del groc
label: 47 # ambre (#FFB600)
label_outline: 11 # marró (#6D2400) — bord càlid del label
value: 191 # crema clar (#FFFFAA)
value_outline: 19 # oliva fosc (#6D4800) — recolza el crema
cursor: 23 # vermilló (#FF4800)
cursor_outline: 9 # marró molt fosc (#240000) — halo profund del cursor
disabled: 19 # oliva fosc (#6D4800)
timings: timings:
notification_ms: 4000 notification_ms: 4000
exit_ms: 4000 exit_ms: 4000
+42
View File
@@ -0,0 +1,42 @@
# Configuració compartida per totes les sales de la piràmide 1. Cada sala
# (r1..r6) hereta aquests camps via el merge recursiu de `RoomYaml::loadMerged`:
# el YAML de sala només declara el que canvia (room, geometry, data, entities,
# i opcionalment overrides puntuals de `lighting`).
pyramid: 1
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Il·luminació per defecte: additive amb halos càlids subtils a les
# antorxes. Les sales que vulguin l'estil "fosc amb forats" (subtractive)
# poden redeclarar `lighting:` sencer; el merge no fusiona "additive"
# amb "subtractive", la sala guanya.
lighting:
mode: additive
level: 1.0
emitters:
torches:
radius: 23
color: [255, 170, 60]
flicker_params:
wave_amplitude_ratio: 0.04
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 0
players: false
mummies: false
-56
View File
@@ -1,60 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1
room: 2 room: 2
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Banc de proves del mode `additive` d'il·luminació. La sala es veu
# "normal" (level 1.0, el GIF intacte fora del cercle de les torxes)
# i només les antorxes hi sumen un halo càlid suau. Player i mummies
# desactivats explícitament perquè aquí no volem cap halo al voltant
# d'actors — el toc cromàtic és només l'antorxa. La corba additiva
# (intensitats baixes a propòsit perquè additive satura ràpid) viu a
# `gradient_additive:` de data/config/lighting.yaml.
lighting:
mode: additive
level: 1.0
emitters:
torches:
radius: 20
color: [255, 170, 60]
# Valors default del flicker explicitats per anar-los ajustant
# in-situ. Coincideixen amb els del global a data/config/lighting.yaml;
# baixar wave_frequency_hz fa el "palpitar" més lent, baixar les
# ratios fa l'oscil·lació més subtil. Qualsevol camp es pot
# eliminar i caurà al valor del global.
flicker_params:
wave_amplitude_ratio: 0.04
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 3.0
players: false
mummies: false
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
+12 -30
View File
@@ -1,19 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1
room: 1 room: 1
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda # Il·luminació de la sala. Bloc opcional; si manca, la sala queda
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els # totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
@@ -37,24 +22,21 @@ origin: [-8, 0]
# (`light_level:` plà a l'arrel) també segueix sent vàlida per # (`light_level:` plà a l'arrel) també segueix sent vàlida per
# retrocompatibilitat. # retrocompatibilitat.
lighting: lighting:
# Sala fosca de proves. La piràmide declara additive per defecte; aquí
# tornem a subtractive perquè volem la dinàmica "negre amb forats de
# llum". `level: 0.15` és l'atenuació base de la sala fora dels emissors.
mode: subtractive
level: 0.15 level: 0.15
emitters: emitters:
players: true players: true
mummies: false torches:
radius: 40
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl color: [255, 170, 60]
# pintable. Un índex per jugador perquè a multijugador local el recorregut flicker_params:
# es distingeix a simple vista. Provisional fins que l'art del pintat wave_amplitude_ratio: 0.05
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp wave_frequency_hz: 3.0
# per desactivar el pintat per a aquest jugador. noise_amplitude_ratio: 0.025
player1_paint_tile: 0 noise_frequency_hz: 7.0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1
room: 3 room: 3
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1
room: 4 room: 4
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1
room: 5 room: 5
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1
room: 6 room: 6
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
+42
View File
@@ -0,0 +1,42 @@
# Configuració compartida per totes les sales de la piràmide 2. Cada sala
# (r1..r6) hereta aquests camps via el merge recursiu de `RoomYaml::loadMerged`:
# el YAML de sala només declara el que canvia (room, geometry, data, entities,
# i opcionalment overrides puntuals de `lighting`).
pyramid: 2
tileset: data/tilesets/pyramid_2.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Il·luminació per defecte: additive amb halos càlids subtils a les
# antorxes. Les sales que vulguin l'estil "fosc amb forats" (subtractive)
# poden redeclarar `lighting:` sencer; el merge no fusiona "additive"
# amb "subtractive", la sala guanya.
lighting:
mode: additive
level: 1.0
emitters:
torches:
radius: 23
color: [255, 170, 60]
flicker_params:
wave_amplitude_ratio: 0.04
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 0
players: false
mummies: false
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 2
room: 1 room: 1
tileset: data/tilesets/pyramid_2.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 2
room: 2 room: 2
tileset: data/tilesets/pyramid_2.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 2
room: 3 room: 3
tileset: data/tilesets/pyramid_2.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 2
room: 4 room: 4
tileset: data/tilesets/pyramid_2.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 2
room: 5 room: 5
tileset: data/tilesets/pyramid_2.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 2
room: 6 room: 6
tileset: data/tilesets/pyramid_2.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
+42
View File
@@ -0,0 +1,42 @@
# Configuració compartida per totes les sales de la piràmide 3. Cada sala
# (r1..r6) hereta aquests camps via el merge recursiu de `RoomYaml::loadMerged`:
# el YAML de sala només declara el que canvia (room, geometry, data, entities,
# i opcionalment overrides puntuals de `lighting`).
pyramid: 3
tileset: data/tilesets/pyramid_3.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Il·luminació per defecte: additive amb halos càlids subtils a les
# antorxes. Les sales que vulguin l'estil "fosc amb forats" (subtractive)
# poden redeclarar `lighting:` sencer; el merge no fusiona "additive"
# amb "subtractive", la sala guanya.
lighting:
mode: additive
level: 1.0
emitters:
torches:
radius: 23
color: [255, 170, 60]
flicker_params:
wave_amplitude_ratio: 0.04
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 0
players: false
mummies: false
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 3
room: 1 room: 1
tileset: data/tilesets/pyramid_3.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 3
room: 2 room: 2
tileset: data/tilesets/pyramid_3.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 3
room: 3 room: 3
tileset: data/tilesets/pyramid_3.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 3
room: 4 room: 4
tileset: data/tilesets/pyramid_3.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 3
room: 5 room: 5
tileset: data/tilesets/pyramid_3.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 3
room: 6 room: 6
tileset: data/tilesets/pyramid_3.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
+42
View File
@@ -0,0 +1,42 @@
# Configuració compartida per totes les sales de la piràmide 4. Cada sala
# (r1..r6) hereta aquests camps via el merge recursiu de `RoomYaml::loadMerged`:
# el YAML de sala només declara el que canvia (room, geometry, data, entities,
# i opcionalment overrides puntuals de `lighting`).
pyramid: 4
tileset: data/tilesets/pyramid_4.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Il·luminació per defecte: additive amb halos càlids subtils a les
# antorxes. Les sales que vulguin l'estil "fosc amb forats" (subtractive)
# poden redeclarar `lighting:` sencer; el merge no fusiona "additive"
# amb "subtractive", la sala guanya.
lighting:
mode: additive
level: 1.0
emitters:
torches:
radius: 23
color: [255, 170, 60]
flicker_params:
wave_amplitude_ratio: 0.04
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 0
players: false
mummies: false
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 4
room: 1 room: 1
tileset: data/tilesets/pyramid_4.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 4
room: 2 room: 2
tileset: data/tilesets/pyramid_4.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 4
room: 3 room: 3
tileset: data/tilesets/pyramid_4.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 4
room: 4 room: 4
tileset: data/tilesets/pyramid_4.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 4
room: 5 room: 5
tileset: data/tilesets/pyramid_4.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 4
room: 6 room: 6
tileset: data/tilesets/pyramid_4.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
+42
View File
@@ -0,0 +1,42 @@
# Configuració compartida per totes les sales de la piràmide 5. Cada sala
# (r1..r6) hereta aquests camps via el merge recursiu de `RoomYaml::loadMerged`:
# el YAML de sala només declara el que canvia (room, geometry, data, entities,
# i opcionalment overrides puntuals de `lighting`).
pyramid: 5
tileset: data/tilesets/pyramid_5.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Il·luminació per defecte: additive amb halos càlids subtils a les
# antorxes. Les sales que vulguin l'estil "fosc amb forats" (subtractive)
# poden redeclarar `lighting:` sencer; el merge no fusiona "additive"
# amb "subtractive", la sala guanya.
lighting:
mode: additive
level: 1.0
emitters:
torches:
radius: 23
color: [255, 170, 60]
flicker_params:
wave_amplitude_ratio: 0.04
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 0
players: false
mummies: false
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 5
room: 1 room: 1
tileset: data/tilesets/pyramid_5.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 5
room: 2 room: 2
tileset: data/tilesets/pyramid_5.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 5
room: 3 room: 3
tileset: data/tilesets/pyramid_5.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 5
room: 4 room: 4
tileset: data/tilesets/pyramid_5.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 5
room: 5 room: 5
tileset: data/tilesets/pyramid_5.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-29
View File
@@ -1,33 +1,4 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 5
room: 6 room: 6
tileset: data/tilesets/pyramid_5.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset). # Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs # La graella de sarcòfags, el pitch entre ells i les amplades dels murs
-138
View File
@@ -1,138 +0,0 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre tomb.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
tileset: data/tilesets/tomb.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
# decideixen els límits del jugador i les seues caixes de col·lisió. Abans
# vivien com a `static constexpr` a map.hpp; ara cada piràmide pot definir
# la seua pròpia forma sense recompilar.
geometry:
grid_cols: 4
grid_rows: 5
sarc_w: 48
sarc_h: 16
pitch_x: 64
pitch_y: 32
hud_h: 24
back_wall_h: 24
side_wall_w: 24
# Columna central del tile transitable de la porta. La porta es renderitza
# com a sprite 48×32 (3 tiles d'amplada) centrat sobre aquesta columna; els
# 16 px centrals (just aquesta columna) són el "forat" del mur trasser per
# on el jugador surt de la sala. Les 2 columnes flanquejants segueixen sent
# mur visible. El spawn dels jugadors també s'alinea amb aquesta columna.
door_col: 10
data:
- [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
- [ 1, 91,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105, 92, 1]
- [ 1, 98,113,113, 95,113, 94,113,113,113,113,113,113,113, 94,113,102,113,113, 96, 1]
- [ 1, 98, 8, 8, 6, 8, 17, 8, 10, 8, 17, 8, 8, 8, 17, 8, 8, 8, 8, 96, 1]
- [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1, 98, 17, 8, 9, 2, 8, 10, 8, 8, 8, 8, 10, 8, 8, 26, 8, 8, 17, 96, 1]
- [ 1, 98, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1, 98, 17, 8, 8, 26, 8, 8, 9, 8, 8, 8, 8, 8, 10, 8, 6, 8, 17, 96, 1]
- [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1, 98, 17, 8, 10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8, 10, 8, 17, 96, 1]
- [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 6, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1, 98, 17, 8, 10, 8, 8, 10, 2, 8, 8, 26, 8, 8, 8, 8, 10, 8, 17, 96, 1]
- [ 1, 98, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 96, 1]
- [ 1, 98, 8, 10, 6, 6, 17, 8, 8, 8, 17, 8, 9, 8, 17, 8, 2, 8, 8, 96, 1]
- [ 1, 99, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89,100, 1]
# Spawns d'entitats per a aquest nivell. Es llegeixen des de
# EntityManager::loadFromLevel al GameScene. Convencions:
# - sarcophagus → `sarc_position: [sarc_col, sarc_row]` en la graella de
# sarcòfags (4×5). EntityManager calcula el rect píxel via
# Map::getSarcRect i l'ancoratge de tile per al perímetre.
# - la resta (torch, door, mummy, rolling_rock, wall_trap, floor_trap)
# → `position: [tile_col, tile_row]` en coordenades del tilemap
# (origen `[-8, 0]` per defecte). Pixel = tile*16 + tilemap_origin.
#
# Dos sarcòfags en cantonades oposades → perímetres aïllats (cap tile en
# comú). La porta va al centre inferior, on el tilemap té els tiles
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
# superior esquerre per verificar render d'una entitat decorativa.
entities:
- type: torch
position: [4, 2]
- type: torch
position: [8, 2]
- type: torch
position: [12, 2]
- type: torch
position: [16, 2]
- type: sarcophagus
sarc_position: [0, 0]
- type: sarcophagus
sarc_position: [1, 0]
- type: sarcophagus
sarc_position: [2, 0]
- type: sarcophagus
sarc_position: [3, 0]
- type: sarcophagus
sarc_position: [0, 1]
- type: sarcophagus
sarc_position: [1, 1]
- type: sarcophagus
sarc_position: [2, 1]
- type: sarcophagus
sarc_position: [3, 1]
- type: sarcophagus
sarc_position: [0, 2]
- type: sarcophagus
sarc_position: [1, 2]
- type: sarcophagus
sarc_position: [2, 2]
- type: sarcophagus
sarc_position: [3, 2]
- type: sarcophagus
sarc_position: [0, 3]
- type: sarcophagus
sarc_position: [1, 3]
- type: sarcophagus
sarc_position: [2, 3]
- type: sarcophagus
sarc_position: [3, 3]
- type: sarcophagus
sarc_position: [0, 4]
- type: sarcophagus
sarc_position: [1, 4]
- type: sarcophagus
sarc_position: [2, 4]
- type: sarcophagus
sarc_position: [3, 4]
- type: door
# Sense `position`: la porta sempre viu al mur trasser. La columna
# transitable la dicta `geometry.door_col`; el render i la caixa
# de col·lisió es deriven d'aquesta dada al loadFromLevel.
- type: mummy
position: [10, 7]
- type: mummy
position: [7, 9]
+23 -6
View File
@@ -17,7 +17,7 @@ layout(set = 2, binding = 0) uniform sampler2D scene;
layout(set = 3, binding = 0) uniform PostFXUniforms { layout(set = 3, binding = 0) uniform PostFXUniforms {
float vignette_strength; float vignette_strength;
float chroma_strength; float chroma_min; // intensitat mínima de l'aberració cromàtica
float scanline_strength; float scanline_strength;
float screen_height; float screen_height;
float mask_strength; float mask_strength;
@@ -27,7 +27,7 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_) float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init float time; // seconds since SDL init
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
float pad2; // padding per tancar vec4 #2 float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML) // vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333) float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
@@ -35,6 +35,19 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
float pad3; // padding per tancar a 64 bytes (4 × vec4) float pad3; // padding per tancar a 64 bytes (4 × vec4)
} u; } u;
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
float sampleBilinearX(vec2 uv_target, int channel) {
vec2 tex_size = vec2(textureSize(scene, 0));
float px = uv_target.x * tex_size.x - 0.5;
float p_floor = floor(px);
float f = px - p_floor;
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
// YCbCr helpers for NTSC bleeding // YCbCr helpers for NTSC bleeding
vec3 rgb_to_ycc(vec3 rgb) { vec3 rgb_to_ycc(vec3 rgb) {
return vec3( return vec3(
@@ -92,10 +105,14 @@ void main() {
colour = base; colour = base;
} }
// Aberración cromática (drift animado con time para efecto NTSC real) // Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3)); // una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
colour.r = texture(scene, uv + vec2(ca, 0.0)).r; // per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
colour.b = texture(scene, uv - vec2(ca, 0.0)).b; if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
}
// Corrección gamma (linealizar antes de scanlines, codificar después) // Corrección gamma (linealizar antes de scanlines, codificar después)
if (u.gamma_strength > 0.0) { if (u.gamma_strength > 0.0) {
+907
View File
@@ -0,0 +1,907 @@
# Arquitectura de **AEE Arcade**
> Guía de orientación para un desarrollador nuevo en el proyecto.
>
> Cada afirmación de este documento está anclada a código real: se cita el
> fichero (y, cuando ayuda, la función o el número de línea) que la respalda.
> Donde no he encontrado algo, lo digo explícitamente en lugar de inventarlo.
>
> El proyecto es *"Aventures en Egipte ARCADE"* (AEE), un juego 2D escrito en
> C++ sobre **SDL3**. Los comentarios del código están en valenciano/catalán;
> este documento está en castellano por petición.
---
## Índice
1. [Visión general](#1-visión-general)
2. [Punto de entrada y bucle principal](#2-punto-de-entrada-y-bucle-principal)
3. [Gestión de escenas](#3-gestión-de-escenas)
4. [Renderizado: de la lógica al píxel](#4-renderizado-de-la-lógica-al-píxel)
5. [Entrada](#5-entrada)
6. [Comunicación entre módulos](#6-comunicación-entre-módulos)
7. [Lógica del juego](#7-lógica-del-juego)
8. [Entidades](#8-entidades)
9. [Mapa y niveles](#9-mapa-y-niveles)
10. [Recursos](#10-recursos)
11. [Configuración y constantes](#11-configuración-y-constantes)
12. [Modo demo / IA](#12-modo-demo--ia)
13. [Convenciones y patrones recurrentes](#13-convenciones-y-patrones-recurrentes)
14. [Guía de navegación: "si quieres tocar X, mira Y"](#14-guía-de-navegación-si-quieres-tocar-x-mira-y)
---
## 1. Visión general
AEE Arcade está organizado en **dos capas claramente separadas** dentro de
`source/`:
- **`source/core/`** — el motor genérico, sin conocimiento del juego concreto:
sistema/arranque, rendering, audio, input, recursos, escena base, locale y
UI base.
- **`source/game/`** — el juego concreto: escenas (LOGO, TITLE, INTRO, GAME),
entidades (momias, sarcófagos, puertas…), mapa, progresión de niveles,
editor (solo Debug) y HUD.
La separación no es solo de carpetas: el código de `core/` nunca incluye
cabeceras de `game/`. La conexión va siempre de `game/` hacia `core/`, salvo
algún *hook* genérico (p.ej. el overlay del menú, ver §6).
Filosofía dominante: **data-driven**. Casi todo el comportamiento (geometría
de salas, plantillas de entidades, timings de transición, controles, paletas,
shaders, idioma…) se carga desde ficheros **YAML** en `data/`, con valores de
*fallback* compilados en `source/utils/defaults.hpp`. El parser es
[fkYAML](https://github.com/fktn-k/fkYAML), vendorizado en
`source/external/fkyaml_node.hpp`.
```mermaid
graph TD
SDL[SDL3 callbacks · main.cpp] --> DIR[Director · core/system]
DIR -->|update / render| SC[Scene activa]
DIR --> EVT[EventBuffer]
EVT --> MENU[ServiceMenu] & GI["GlobalInputs (F1–F12)"] & GP[GamepadManager]
SC --> GAME[GameScene · game/scenes]
GAME --> EM[EntityManager] --> BUS[EventBus]
GAME --> TM[Tilemap] & MAP[Map] & LM[LevelManager]
GAME --> PL[Players] -->|polling teclado/mando| INPUT[core/input]
EM --> ENT["Entity + concrete/*"]
GAME -->|blit ARGB| SURF[Surface]
SURF --> CANVAS["Canvas 320×240 ARGB (CPU)"]
CANVAS --> SCREEN[Screen] --> BACKEND["SDL3 GPU + shaders / fallback SDL_Renderer"]
RES[Resource::Cache] -.-> SURF & AUDIO[Audio]
```
Tamaño aproximado: ~188 ficheros C++ y ~30.000 líneas en `source/` (excluyendo
`external/`).
**Dos ideas-fuerza que conviene interiorizar desde el principio**, porque
condicionan todo lo demás:
1. **El render es 100% por CPU sobre un único canvas ARGB de 320×240.** La GPU
solo escala ese canvas a la ventana y aplica post-procesado (CRT). Ver §4.
2. **La paleta es por-`Surface`, pero el canvas final es ARGB de 24 bits.** No
es un juego de paleta global limitada: cada GIF lleva su propia paleta y se
compone sobre un framebuffer de color completo
(`source/core/rendering/surface.hpp:13`).
---
## 2. Punto de entrada y bucle principal
### 2.1. No hay `while` propio: SDL conduce el bucle
`source/main.cpp` define `SDL_MAIN_USE_CALLBACKS` (línea 1). Eso significa que
**no existe un game loop escrito a mano**: es SDL quien llama a cuatro
funciones de *callback*:
| Callback | Qué hace en este proyecto |
|---|---|
| `SDL_AppInit` | Guarda `argv` (para reinicio en caliente), inicia el logger y **crea el `Director`** (`main.cpp:13`). |
| `SDL_AppEvent` | Empuja cada `SDL_Event` a `EventBuffer::events` (`main.cpp:31`). |
| `SDL_AppIterate` | Ejecuta **un frame**: `director->iterate()` (`main.cpp:37`). |
| `SDL_AppQuit` | Destruye el `Director` y, si se pidió, hace `execv` para reiniciar (`main.cpp:42`). |
El `Director` se entrega a SDL como puntero crudo (`appstate`) y se reabsorbe
en `SDL_AppQuit` con un `unique_ptr` para limpieza RAII ordenada.
### 2.2. El `Director`: orquestador del frame
`source/core/system/director.hpp` / `.cpp` es el corazón del sistema.
**Constructor (`director.cpp:47`)** — inicializa *todo* en un orden cuidadoso y
documentado en comentarios. Resumen del orden:
1. `SDL_Init(0)` (sin subsistemas todavía).
2. Sistema de recursos: localiza `resources.pack` junto al binario; en `_DEBUG`
activa *fallback* a disco apuntando al árbol fuente (`AEE_SOURCE_DIR`) para
editar YAML/GIF sin re-empaquetar; en Release el pack es obligatorio
(`director.cpp:52`).
3. `createSystemFolder()` — carpeta de configuración por plataforma (ver §11).
4. `GamepadManager::get().init()` **antes** de cargar opciones (las opciones
re-vinculan mandos a jugadores; `director.cpp:86`).
5. Carga de `config.yaml`, `gamepad_configs.yaml`, `gameplay.yaml`, presets de
shaders (`postfx.yaml`, `crtpi.yaml`).
6. `Screen::get().configure(...)` — crea ventana + backend GPU.
7. `Locale`, `KeyConfig`, `UiConfig`, `Lighting`, `EditorConfig` (Debug).
8. Pre-carga de recursos de UI: paleta, fuentes, sombra compartida, pistas de
música, sonidos de menú.
9. `Audio::init(...)`.
10. Construcción de los *overlays* compartidos (`Notifier`, `RenderInfo`,
`ServiceMenu`) y de las `Surface`/`Text` compartidas entre escenas.
11. Registro del *overlay hook* del menú en `Screen` (`director.cpp:291`).
12. Elección de la escena inicial (ver §3.3) y `last_ticks_ms_ = SDL_GetTicks()`.
**Destructor (`director.cpp:336`)** — el orden de liberación es crítico y está
fuertemente comentado: primero la escena, luego el audio del `Cache` (mientras
SDL sigue vivo), luego `Audio`, `Screen` y por fin `SDL_Quit()`. Se hace
explícitamente (no vía destructores estáticos `atexit`) para evitar crashes en
`vkDestroyInstance` cuando el runtime descarga librerías antes de tiempo.
### 2.3. Gestión del tiempo y orden update/render
El "frame" vive en `Director::iterate()` (`director.cpp:509`). **No hay
timestep fijo**: se usa el delta real entre frames.
```cpp
const std::uint64_t NOW_MS = SDL_GetTicks();
int delta_ms = static_cast<int>(NOW_MS - last_ticks_ms_);
delta_ms = std::max(delta_ms, 0); // nunca negativo
last_ticks_ms_ = NOW_MS;
```
Orden exacto de un frame (`director.cpp:509`–`549`):
```mermaid
sequenceDiagram
participant SDL
participant Dir as Director::iterate
participant Scene
SDL->>Dir: SDL_AppIterate
Note over Dir: Mouse::handleEvent(ev) para todos los eventos
Dir->>Dir: processGamepadEvents() (hot-plug + botón SERVICE)
Dir->>Dir: processMenuInput() (ServiceMenu consume teclas)
Dir->>Dir: processEvents() (detecta SDL_EVENT_QUIT)
Dir->>Dir: delta_ms = now - last
Dir->>Dir: Audio::update()
Dir->>Dir: Mouse::updateCursorVisibility()
Dir->>Dir: ServiceMenu::update(delta_ms)
Dir->>Scene: update(delta_ms)
Dir->>Scene: render()
Dir->>Dir: switchSceneIfRequested()
Dir->>Dir: EventBuffer::events.clear()
```
El orden de procesamiento de eventos es deliberado y está comentado en
`director.cpp:514`: gamepad → menú → quit, para que un `SDL_EVENT_QUIT`
inyectado por el menú ("Salir del juego") se vea en el mismo frame antes de
limpiar el buffer.
El bucle termina (`SDL_APP_SUCCESS`) cuando la escena se reinicia a `nullptr`
(quit) o cuando `current_scene_->wantsQuit()` es `true`.
---
## 3. Gestión de escenas
### 3.1. La base `Scene`
`source/core/scene/scene.hpp` es minimalista:
```cpp
class Scene {
public:
virtual void update(int delta_ms) = 0;
virtual void render() = 0;
auto wantsQuit() const -> bool { return wants_quit_; }
auto takeExitRequest() -> std::any { return std::exchange(exit_request_, {}); }
protected:
bool wants_quit_{false};
std::any exit_request_;
};
```
El detalle clave: la petición de transición viaja dentro de un **`std::any`**.
Así, `core/scene/scene.hpp` **no necesita conocer los tipos del juego**. La
escena deposita un `ExitRequest` (tipo definido en `game/`) y el `Director`
hace el `std::any_cast` cuando le toca.
### 3.2. El flujo de escenas: descentralizado al pedir, centralizado al construir
`source/game/scenes/scene_flow.hpp` define el contrato:
- `enum class SceneId { LOGO, TITLE, INTRO, GAME }`.
- Structs de parámetros por escena (`LogoParams`, `TitleParams`,
`IntroParams`, `GameParams`) reunidos en un `std::variant SceneParams`.
- `struct ExitRequest { SceneId next; SceneParams params; }`.
Una escena **no construye la siguiente**: solo declara a dónde quiere ir. El
único punto que sabe cómo instanciar cada escena concreta es
`Director::buildScene()` (`director.cpp:484`), que tiene a mano todos los
recursos compartidos pre-cargados en el constructor.
```mermaid
graph LR
A[Escena activa] -->|deposita ExitRequest en std::any| B[Director::switchSceneIfRequested]
B -->|std::any_cast| C[Director::buildScene]
C -->|make_unique| D[Nueva escena]
```
Esto desacopla las escenas entre sí: ninguna `#include` a otra. Para **añadir
una escena nueva** (documentado en `scene_flow.hpp:14`): (1) una entrada en el
`enum`, (2) un struct `*Params` si necesita datos, (3) un `case` en
`buildScene`.
> Nota de migración: en `buildScene` se ve que algunas escenas todavía reciben
> recursos directamente del `Director` por el constructor en vez de pasar por
> el canal `ExitRequest`. Es un estado intermedio, comentado en
> `director.cpp:484`.
### 3.3. Flujo del juego y punto de arranque
El flujo "completo" que ve el jugador final es **LOGO → TITLE → INTRO → GAME**.
- **Release**: siempre arranca en `LOGO` (`director.cpp:314`).
- **Debug**: lee `debug.yaml` de la carpeta de sistema; si no existe, lo crea
con `start_scene: GAME` para no tragarse los placeholders en el día a día
(`director.cpp:301`). `debug.yaml` también puede fijar `starting_players` y
`starting_level` (`director.cpp:304`).
Las escenas concretas viven en `source/game/scenes/`:
`logo_scene`, `title_scene`, `intro_scene`, `game_scene`. La transición entre
salas dentro de GAME **no** es un cambio de escena: la `GameScene` persiste y
recarga la sala internamente (ver §7).
---
## 4. Renderizado: de la lógica al píxel
Esta es la sección más importante para entender el proyecto, porque el render
es poco convencional: **todo el dibujo lo hace la CPU sobre un buffer de
píxeles**, y la GPU solo interviene al final.
### 4.1. El canvas: un único framebuffer ARGB en RAM
- La resolución lógica del juego es **320×240** (`source/utils/defines.hpp:10`,
`GameCanvas::WIDTH/HEIGHT`).
- `Screen` mantiene `std::vector<std::uint32_t> canvas_` — un array plano de
píxeles ARGB8888 (`screen.hpp:143`).
- El acceso de los blitters no va por `Screen::get()` directamente, sino por
`Rendering::activeCanvas()` (`source/core/rendering/canvas.hpp:38`), para no
acoplarse al ciclo de vida de `Screen`. También existe un sistema de
**clipping** apilable (`pushClip`/`popClip`, `canvas.hpp:46`).
El bit alfa es siempre opaco: `Rendering::OPAQUE_ALPHA = 0xFF000000`
(`canvas.hpp:15`). El canal alfa no se usa para transparencia del frame final
— la transparencia de sprites se resuelve por *color index transparente*
durante el blit (ver §4.2).
### 4.2. `Surface`: sprites paletizados → píxeles ARGB
`source/core/rendering/surface.hpp` / `.cpp` es el blitter base.
- Una `Surface` guarda los píxeles como **índices de 8 bits** (`Uint8`) más una
**paleta propia** de 256 colores ARGB (`Palette = std::array<Uint32, 256>`).
- Se carga desde un GIF (`Surface(const std::string&)`, `surface.cpp:38`):
decodifica píxeles y paleta con `GIF::Gif` (`core/rendering/gif.*`).
- El índice 0 es transparente por convención de todos los GIF del proyecto
(`surface.hpp:78`); los tilesets que usen otro índice llaman a
`setTransparentColor()`.
El núcleo del dibujo es `renderWithColorReplace()` (`surface.cpp:90`). Para
cada píxel del rectángulo fuente:
1. Comprueba el *clip* y descarta fuera de él.
2. Lee el índice fuente; si es el transparente, **salta** (no escribe).
3. Aplica `flip` horizontal/vertical reindexando la coordenada fuente.
4. Opcionalmente reemplaza un índice por otro (`source_color → target_color`),
usado por `Text` para recolorear glifos (la "tinta" se guarda con un único
índice).
5. Escribe `palette_[remapped]` en el canvas.
`render()` (`surface.cpp:82`) es un caso particular de lo anterior (delega con
`source==target==transparent`, evitando duplicar el bucle).
> Hay además una `SubPalette` (`std::array<Uint8,256>`, identidad por defecto
> vía `std::iota`) que permite remapear índices antes de la paleta; se usa para
> efectos como el parpadeo de inmunidad del Player.
Sobre `Surface` se construyen abstracciones de mayor nivel en
`core/rendering/sprite/`:
- `Sprite` → `AnimatedSprite` → `MovingSprite` (herencia).
- `AnimatedSprite` reproduce tiras de fotogramas; su parser está en
`animated_sprite_parser.cpp`. **El `Player` hereda de `AnimatedSprite`**
(`player.hpp:18`).
### 4.3. El orden de pintado de un frame de juego
`GameScene::render()` (`source/game/scenes/game_scene.cpp:1682`) es la
referencia canónica de cómo se compone una imagen. Orden exacto:
1. **Fondo**: `Screen::fillRect` con el color de fondo de la paleta UI.
2. **Tilemap**: `tilemap_.render()` — copia de la textura prerenderizada
(ver §9.2). Las celdas de sarcófago quedan transparentes.
3. **(Editor Debug)**: si está activo, pinta entidades congeladas y vuelve.
4. **Capa de pintado** (`PaintLayer`): el "rastro" del jugador por el suelo.
5. **Sombras**: `p->renderShadow()` de cada jugador en el aire — por debajo de
todos los actores.
6. **Z-sort de actores**: entidades + jugadores se fusionan en un vector y se
ordenan con `std::ranges::stable_sort` por **`getFeetY()` ascendente**
(`game_scene.cpp:1755`). Quien tiene los pies más arriba se pinta antes (más
al fondo). Esto da la falsa perspectiva 2D.
7. **Pasada de iluminación**: `applyLightingPass()` (ver §4.4).
8. **Fade de sala** (`renderRoomFade`): negro entre actores y HUD.
9. **HUD**: `hud_->render()`.
10. **Texto de transición de sala** y **rótulo de game over**.
11. **Fade final de game over**.
12. `Notifier::render()` y `RenderInfo::render()` (overlays de debug).
13. **`Screen::get().present()`**.
El criterio recurrente está comentado: la UI (HUD, notificaciones, menú) se
pinta **después** de los fades para quedar siempre legible.
### 4.4. Iluminación por CPU
`source/core/rendering/lighting.hpp` / `.cpp`. Es una pasada que **modifica
cada píxel del canvas** después de pintar el mundo. Dos modos seleccionables
por sala (`lighting.mode` en el YAML):
- **SUBTRACTIVE** (histórico, por defecto): `out = src × max(room_light,
max_emitter_intensity)`. Los emisores "rompen" una capa oscura. El color del
emisor se ignora; solo cuenta la intensidad.
- **ADDITIVE**: `out = saturate(src × room_light + Σ emitter_color × intensity)`.
Los emisores suman halo cromático.
Cada emisor (jugadores, antorchas, momias con `light_radius > 0`) proyecta un
círculo radial de 3 anillos configurables en `data/config/lighting.yaml`. Las
antorchas pueden tener *flicker* (`light_flicker`), con parámetros resueltos
por entidad (`Entity::getFlickerParams`, `entity.hpp:306`).
### 4.5. `Screen::present()` y el backend GPU
`Screen::present()` (`source/core/rendering/screen.cpp:306`):
1. Llama al **overlay hook** (menú de servicio) para que pinte *sobre* el
canvas antes de subirlo (`screen.cpp:310`).
2. Actualiza el contador de FPS.
3. Si hay **backend GPU** (`shader_backend_`): `uploadPixels(canvas)` +
`render()`. Este es el camino por defecto.
4. Si no (fallback): `SDL_UpdateTexture` de una textura STREAMING +
`SDL_RenderTexture` + `SDL_RenderPresent`.
El backend GPU vive en `source/core/rendering/sdl3gpu/` (SDL3 GPU API +
shaders SPIR-V compilados). En `configure()` (`screen.cpp:108`) se intenta el
path GPU primero; si falla, hay **fallback automático a `SDL_Renderer`**. El
escalado a la ventana usa modos *letterbox / integer / stretch / overscan*
(`Defaults::ScalingMode`).
**Post-procesado**: shaders en `data/shaders/` (`postfx.frag`, `postfx.vert`,
`crtpi_frag.glsl`). El efecto CRT y similares se aplican aquí, en GPU, sobre la
textura del canvas. El juego no sabe nada de esto: solo escribe píxeles en el
canvas. Los presets se gestionan desde `Options` y se ciclan con hotkeys (§5).
```mermaid
graph TD
LOG["Lógica del juego (game/)"] -->|Surface::render| CV["Canvas ARGB 320×240 (CPU)"]
HUD[HUD / Notifier / RenderInfo] --> CV
LIGHT[Lighting pass CPU] --> CV
MENU[ServiceMenu overlay] -->|hook en present| CV
CV -->|uploadPixels| GPU["ShaderBackend (SDL3 GPU)"]
GPU -->|shader postfx/crtpi| WIN[Ventana]
CV -.fallback.-> SDLR[SDL_Renderer] --> WIN
```
> **Resumen honesto**: la GPU en este proyecto **no dibuja geometría del
> juego**. Solo recibe un mapa de bits ya compuesto y lo escala + filtra. Todo
> el "render" del juego es manipulación de un array de `uint32_t` en RAM.
---
## 5. Entrada
Hay **dos vías de entrada totalmente distintas**, y entender la diferencia
evita mucha confusión:
### 5.1. Vía por eventos (`EventBuffer`)
Los `SDL_Event` se acumulan en `EventBuffer::events`
(`source/core/system/event_buffer.hpp`) durante `SDL_AppEvent`. Los consumen,
en orden, dentro de `Director::iterate()`:
- **`Mouse`** (`core/input/mouse.*`): solo reacciona a `MOUSE_MOTION` para
reaparecer el cursor (auto-ocultación).
- **Gamepad hot-plug y botón SERVICE** (`processGamepadEvents`,
`director.cpp:417`): ADDED/REMOVED van a `GamepadManager`; el botón SERVICE
(por defecto BACK, remapeable) abre/cierra el menú.
- **Menú de servicio** (`processMenuInput`, `director.cpp:446`): con el menú
abierto consume todas las teclas salvo F1–F12 y ESC.
- **`GlobalInputs`** (`core/input/global_inputs.cpp:190`): hotkeys de sistema
F1–F12 + ESC. Itera `EventBuffer` buscando `KEY_DOWN`. Las acciones se leen
de `KeyConfig` (`data/config/input.yaml`):
| Acción | Efecto |
|---|---|
| `dec_zoom` / `inc_zoom` | cambia zoom de ventana |
| `fullscreen` | alterna pantalla completa |
| `toggle_vsync` | alterna VSync |
| `screenshot` | guarda captura |
| `toggle_shader` / `next_shader` / `next_preset` | post-FX |
| `next_scaling_mode` | cicla letterbox/integer/… |
| `toggle_render_info` | cicla overlay de debug OFF→TOP→BOTTOM |
| `exit` (ESC) | **salida en dos pasos** (ver abajo) |
La salida con ESC es en dos pulsaciones: la primera muestra una notificación
de confirmación y arma un temporizador; la segunda, dentro de la ventana,
activa `wantsQuit()` (`global_inputs.cpp:176`, `global_inputs.hpp:1`).
### 5.2. Vía por *polling* (la del gameplay)
El **`Player` lee su input directamente cada frame**, no por eventos:
- Teclado: `SDL_GetKeyboardState(nullptr)` (`player.cpp:629`, `764`, `784`).
Cada `Player` tiene su propio `Controls` (struct de `SDL_Scancode`,
`player.hpp:23`). Por defecto P1 usa flechas + M/N (`Defaults::KeysGame`,
`defaults.hpp:75`) y P2 usa WASD (`Defaults::KeysGameP2`).
- Mando: `GamepadManager::get().getForPlayer(id)` +
`GamepadBindings::isPressed(pad, btn)` (`player.cpp:769`).
Esto encaja con que el movimiento se calcula en `Player::tick(dt)` durante el
bloque de mundo de `GameScene::update` (§7), donde importa el estado *actual*
de las teclas, no la secuencia de eventos.
### 5.3. Detección puntual de pulsaciones en escena
Para "¿se ha pulsado START este frame?" hay un helper compartido,
`Input::Detect` (`source/game/scenes/start_input.hpp`):
- `pressedStart(player_id)` — combina eventos de teclado y botón de mando.
Lo usan `TitleScene` (empezar partida) y `GameScene` (*join in progress*).
- `pressedActionButton(player_id)` — salto o ataque, usado para saltar el
*countdown* de CONTINUE?.
### 5.4. Mandos
`core/input/`:
- `gamepad_manager.*` — descubre, abre/cierra mandos y asigna mando↔jugador.
- `gamepad_bindings.*` — vinculaciones por dispositivo `(name, path)`,
persistidas en `gamepad_configs.yaml` (fichero aparte de `config.yaml`
porque es por hardware, no por perfil; `director.cpp:93`).
- `define_buttons.*`, `key_config.*` — catálogo de botones y mapeo de scancodes
de hotkeys.
---
## 6. Comunicación entre módulos
El proyecto combina tres mecanismos, cada uno para su propósito:
### 6.1. `EventBus` — pub/sub tipado dentro de un nivel
`source/game/entities/event_bus.hpp`. Hay **un `EventBus` por mundo**, propiedad
del `EntityManager` (`entity_manager.hpp:199`). Las entidades reciben un
puntero vía `Entity::setEventBus`.
- El *payload* es un **`std::variant`** tipado (`event_bus.hpp:98`), no
`void*` ni `std::any`: `int`, `std::string`, `Entity*`, `TilePos`,
`PaintedTile`, `WorldPos`, `WhipSpawnPayload`, `PlayerStatsPayload`.
- API: `subscribe(name, handler) → SubId`, `unsubscribe(id)`,
`publish(Event)` (+ sobrecargas ergonómicas por tipo de payload).
Eventos reales que circulan (vistos en el código): `sarcophagus_opened`,
`level_complete`, `player_hit`, `player_killed`, `player_disabled`,
`player_jumped`, `player_landed`, `on_tile_entered`, `tile_painted`,
`whip_spawn`, `player_stats_changed`.
Ejemplo de flujo desacoplado: el `Player` no conoce ni `WhipAttack` ni
`EntityManager`; al atacar publica `whip_spawn` con un `WhipSpawnPayload`, y
`GameScene` lo escucha y crea la entidad (`event_bus.hpp:66`).
Otro: cuando el `Player` muta vidas/score/tesoros, publica
`player_stats_changed` con un *snapshot completo*; el HUD lo consume como
estado absoluto (no delta). Así cualquier vía que cambie stats se refleja sin
que el caller recuerde notificar al HUD (`event_bus.hpp:28`).
### 6.2. Llamadas directas e inyección de dependencias
Donde la relación es estructural y estable, se usa llamada directa o inyección
por *setter*: p.ej. `Entity::setEntityManager` (una momia consulta el mundo
para no caer en un `Hole`, `entity.hpp:180`), o `Map::setEntityCollider` (el
`EntityManager` registra un predicado de colisión para muros dinámicos,
`map.hpp:171`).
### 6.3. *Hooks* / callbacks para invertir dependencias
`core/` no conoce `game/`, así que cuando `core/` necesita algo del juego se
inyecta un `std::function`:
- `Screen::setOverlayRenderer(callback)` — el `Director` registra una lambda
que pinta el `ServiceMenu` sobre el canvas en `present()`. Así `Screen` puede
poner el menú por encima de cualquier escena sin incluir `game/ui`
(`screen.hpp:81`, `director.cpp:291`).
### 6.4. Singletons
Varios subsistemas son **Meyers singletons** (`get()`): `Screen`,
`Resource::Cache`, `GamepadManager`, `GamepadBindings`, `Locale`, `UiConfig`,
`Lighting`, `LevelManager`, `ServiceMenu`, `Notifier`, `RenderInfo`. Ver §13
para el matiz de su ciclo de vida.
---
## 7. Lógica del juego
El grueso vive en `source/game/scenes/game_scene.cpp` (la clase más grande del
proyecto, ~1800 líneas). `GameScene` coordina mapa, entidades, jugadores, HUD,
y **dos máquinas de estado**.
### 7.1. FSM de sala — `RoomFsm`
`source/game/scenes/room_fsm.hpp`. Lógica **pura** (no toca SDL/Audio; solo
`Locale` para formatear texto), extraída para ser testeable.
```
ROOM_INTRO → PLAYING → ROOM_OUTRO → (siguiente sala) ROOM_INTRO
```
Cada estado tiene sub-fases con timings cargados de
`data/scenes/game/game.yaml` (`room_fsm.hpp:33`). Por ejemplo, `ROOM_INTRO`
tiene: `fade_in → text_delay → text_enter → text_hold → text_exit →
music_tail`. La fase `music_tail` es una "cola silenciosa" calculada para que
el *timeline* visual termine exactamente cuando drena el `.ogg` de música —
**no hay callback de fin de música**; la transición la dispara
`GameScene::updateRoomTransition` cuando la FSM consume la última fase
(`room_fsm.hpp:27`).
La función central es `RoomFsm::advance(current, transition, cfg, delta_ms)`
→ devuelve el estado resultante; si cambia, el caller resetea la transición.
### 7.2. FSM de game over — `GameOverFsm`
`source/game/scenes/game_over_fsm.*`. Sub-FSM independiente
(`CONTINUE? → countdown → GAME OVER → FADE_OUT → DONE`) que se superpone a la
`RoomFsm` y puede dispararse durante `PLAYING` cuando llegan a 0 los jugadores
en partida. Al terminar pide la transición a `LOGO`
(`game_scene.hpp:280`–`333`). El *countdown* puede saltarse pulsando acción
(`handleCountdownSkip`).
### 7.3. FSM del `Player`
`source/game/entities/player.hpp:66` define `enum class State`:
`GROUNDED, AIRBORNE, LANDING_STUN, HURT, ATTACKING, EXITED, DISABLED, DYING`.
Cada estado define qué input acepta y cómo se mueve/colisiona. Detalles
relevantes:
- El **salto** usa `jump_z_` (altura): saltar por encima de la `height_` de
una entidad permite esquivar momias/rocas (`entity.hpp:219`).
- `DISABLED` = slot inactivo (P2 en single-player, o todos durante
transiciones); `EXITED` = ha salido por la puerta; `DYING` = animación de
muerte + delay antes de respawn.
- El array `players_` **siempre tiene 2 entradas** (P1, P2); el slot inactivo
queda `DISABLED` y puede entrar a media partida ("join in progress") pulsando
START (`game_scene.hpp:87`, `handleJoinInput`).
### 7.4. El bloque de mundo: orden de update
Dentro de `GameScene::update` (`game_scene.cpp:1519`), el mundo solo avanza si
**no** está abierto el menú ni el editor y `delta_ms > 0`. El pipeline de
movimiento es de **tres pasadas** para intercalar la resolución jugador↔jugador
(`game_scene.cpp:1629`):
1. `player->tick(DT)` — cada jugador calcula su velocidad intencionada (teclado
+ mando + *facing*).
2. `resolvePlayerVsPlayer(...)` — anula velocidades en choque frontal o las
escala a `push_factor_` (empuje).
3. `player->applyMovement(DT)` — en orden "empujado primero".
Después: `entity_manager_.update(DT)` (tras los jugadores, para que un
`on_tile_entered` emitido este frame se procese ya) y
`checkPlayerCollisions(...)`.
### 7.5. Reglas del juego
- **Sarcófagos** (`concrete/sarcophagus.hpp`): el jugador debe "pintar"
(recorrer) todo el perímetro de tiles del sarcófago para abrirlo. FSM:
`CLOSED → OPENING → OPEN`. Al abrirse publica `sarcophagus_opened` y reparte
recompensa (`points`/`treasures`) al jugador atribuido.
- **Puerta** (`concrete/door.hpp`): FSM `CLOSED → OPENING → OPEN`. El
`EntityManager` la abre automáticamente cuando el nº de sarcófagos abiertos
iguala el total (`entity_manager.hpp:21`). Al abrirse libera el pasadizo
(`Map::door_open = true`). Cruzarla dispara `Player::exit()` →
`player_exited` → `level_complete` → OUTRO.
- **Pintado del suelo** (`game/map/paint_layer.*`): escucha `on_tile_entered`,
filtra pintabilidad (`Map::isPaintable`) y re-publica `tile_painted` para los
sarcófagos (`game_scene.hpp:219`).
- **Hazards**: momias, rocas rodantes (`rolling_rock`), trampas de suelo/pared
(`floor_trap`, `wall_trap`), agujeros (`hole`). Los letales publican
`player_killed`.
---
## 8. Entidades
### 8.1. `Entity`: composición, no herencia de sprite
`source/game/entities/entity.hpp`. Decisión de diseño explícita
(`entity.hpp:34`): **`Entity` NO hereda de `AnimatedSprite`**. La animación es
un *componente opcional* (`std::optional<AnimationComponent> anim_`), porque
no todas las entidades la necesitan (trampas invisibles, triggers) y para no
imponer semántica de movimiento continuo a actores estáticos.
`Entity` reúne lo común: posición en tiles y píxeles, caja de colisión
opcional (`CollisionBox`), flags de comportamiento (`collides_`,
`blocks_movement_`, `triggers_on_overlap_`, `is_floor_decal_`, `dead_`),
parámetros de luz, y un conjunto de **hooks polimórficos**:
| Hook | Para qué |
|---|---|
| `update(dt)` / `render()` | tick lógico / dibujo |
| `onPlayerContact(Player*)` | el jugador toca la entidad (daño, salida…) |
| `onWhipHit(WhipAttack*)` | recibe un latigazo (override en `Mummy`) |
| `notifyTilePainted(col,row,id)` | un tile se pintó (lo usa `Sarcophagus`) |
| `onJustSpawned()` | recién nacida (dispara animación EMERGING) |
| `onRemoved()` | antes de borrarse (mantener invariantes) |
| `shouldReceiveContact()` | filtro de si recibe contacto este frame |
| `getFeetY()` | Y de los pies, para el z-sort (§4.3) |
El polimorfismo evita "sacos" separados por tipo: el `EntityManager` itera
todas y cada entidad decide qué hace (`entity_manager.hpp:78`).
### 8.2. `EntityManager`: contenedor y coordinador por nivel
`source/game/entities/entity_manager.hpp`. **No es singleton**: una instancia
por nivel, vive dentro de `GameScene` (`game_scene.hpp:80`).
Responsabilidades: poseer las entidades (`std::vector<unique_ptr<Entity>>`),
propagar `update`, contar sarcófagos abiertos y abrir la puerta, hacer *fan-out*
de `notifyTilePainted`, comprobar colisiones jugador↔entidad, y cargar el nivel
desde YAML (`loadFromLevel`). También gestiona una **cola de actores pendientes**
(`pending_actors_`): los hazards dinámicos declarados en el YAML no se instancian
de inmediato, sino que `spawnPendingActors()` los materializa en una sub-fase
concreta del ROOM_INTRO con su cinemática de aparición (`entity_manager.hpp:130`).
### 8.3. `EntityFactory`: registro abierto a extensión
`source/game/entities/entity_factory.hpp`. Factory por nombre con
**auto-registro**: cada `concrete/*.cpp` declara su creador con la macro
`REGISTER_ENTITY(name, Class)`, que genera un objeto estático cuyo constructor
se ejecuta antes de `main` y se registra en la factory (`entity_factory.hpp:56`).
> **Añadir un tipo de entidad = nueva clase en `concrete/` + una línea
> `REGISTER_ENTITY`.** No se toca la factory ni el manager.
Aviso importante recogido en el código: para que el registrador se ejecute, el
`.cpp` debe enlazarse directamente al ejecutable (no oculto en una static lib
sin `--whole-archive`); todos los `concrete/*.cpp` entran en `APP_SOURCES`
(`entity_factory.hpp:53`).
Tipos concretos existentes (`source/game/entities/concrete/`): `door`, `dust`,
`floor_trap`, `hole`, `mummy`, `rolling_rock`, `sarcophagus`, `solid_props`,
`torch`, `wall_trap`, `whip_attack`.
### 8.4. Jerarquía intermedia
Entre `Entity` y las concretas hay clases intermedias que aportan semántica:
`decoration.hpp`, `solid_decoration.hpp`, `interactable.hpp`, `hazard.hpp`
(con `DynamicHazard` para actores con fase de aparición). `Sarcophagus` hereda
de `Interactable`, por ejemplo.
---
## 9. Mapa y niveles
### 9.1. `Map`: geometría y colisión (un namespace, no una clase)
`source/game/map/map.hpp`. **`Map` es un `namespace`**, no una clase, porque no
hay estado de instancia: la geometría de la sala activa vive como variables
`inline` del namespace (`map.hpp:32`). Se carga con `Map::loadFrom(yaml)` desde
el bloque `geometry:` del YAML de la pirámide.
Contiene la geometría de la sala (graella de sarcófagos, *pitch*, anchos de
muro, playfield, posición de la puerta `door_col`) y las funciones de colisión:
- `collides(box)` — ¿la caja choca con sarcófago, pasadizo de puerta cerrada o
entidad sólida? (`map.hpp:164`).
- `insidePlayfield(box)`, `isPaintable(col,row)`, `pixelToTile(px,py)`.
Para no acoplar a todos al namespace global, existe `Map::Info` — un *snapshot*
POD copiable de la geometría + punteros a funciones de colisión, que se inyecta
a entidades (hoy el `Player`, decoupling COUP-02; `map.hpp:201`).
### 9.2. `Tilemap`: prerenderizado una vez
`source/game/map/tilemap.hpp`. La cuadrícula de tiles 16×16 se **prerrenderiza
una sola vez a un buffer ARGB propio** en el constructor; cada frame es una
copia clipada al canvas (`tilemap.hpp:8`). Así no se recorre la cuadrícula cada
frame. En Debug guarda además `data_` (la rejilla de índices) para el editor.
### 9.3. `LevelManager`: progresión
`source/game/levels/level_manager.hpp`. Meyers singleton, **fuente de verdad de
la progresión**. Carga `data/maps/levels.yaml` (lista de salas con `pyramid`,
`room`, `file`). API: `currentPath()`, `advance()`, `restart()`, `setIndex()`,
`setStartingPoint(pyramid, room)`. No hace *loop* por sí mismo: la política la
decide `GameScene` (`level_manager.hpp:49`).
Los mapas reales viven en `data/maps/p1/`…`p5/` (5 pirámides × ~6 salas), cada
una con sus `rN.yaml` y un `pyramid.yaml` con la geometría.
---
## 10. Recursos
### 10.1. `Resource::Cache`
`source/core/resources/resource_cache.hpp`. Meyers singleton que cachea por
nombre: `Surface`, animaciones, paletas, música (`Ja::Music`) y sonidos
(`Ja::Sound`). API `loadX(name, path)` + `getX(name)`. La búsqueda es lineal
sobre vectores (suficiente para el tamaño actual).
Tiene un `clearAudio()` que **el `Director` llama explícitamente** antes de
destruir `Audio` y `SDL_Quit`, porque al ser Meyers el Cache se destruiría
tarde, con los streams SDL ya invalidados (`resource_cache.hpp:43`).
### 10.2. Pack vs disco
`source/core/resources/resource_pack.*` + `resource_helper.*`. Toda lectura de
fichero pasa por `Resource::Helper::loadFile(path)`. En **Release** los datos
salen de `resources.pack` (empaquetado por `tools/pack_resources/` en cada
build). En **Debug** se activa *fallback* a disco apuntando al árbol fuente, de
modo que editar un YAML o un GIF en `data/` se ve sin re-empaquetar
(`director.cpp:52`).
### 10.3. Formatos de arte
- **GIF** es el formato de sprites, tilesets y fuentes (`data/**/*.gif`),
decodificado por `core/rendering/gif.*`. Cada GIF lleva su paleta.
- **Fuentes**: pares `.fnt` + `.gif` (`data/fonts/`), consumidos por `Text`
(`core/rendering/text.*`).
- **Música**: `.ogg` decodificado con `stb_vorbis` (`source/external/`).
- **Paletas sueltas**: `.pal` JASC o `.gif` (`palette_loader.*`,
`data/palettes/`).
---
## 11. Configuración y constantes
### 11.1. La cadena YAML → `Options` → subsistema
El patrón es uniforme: un YAML en `data/config/` (o en la carpeta de sistema
del usuario) se carga, y si falta una clave cae al valor compilado en
`source/utils/defaults.hpp`. `defaults.hpp` es el **catálogo central de
constantes** organizado en sub-namespaces (`Window`, `Video`, `Audio`,
`KeysGame`, `KeysGameP2`, `I18n`, `Paths`, `UI`, …).
`source/game/options.*` + `options_apply.*` traducen esas opciones a structs
tipados que los subsistemas consumen sin conocer YAML (p.ej.
`Options::buildScreenConfig()` → `Screen::Config`). Esto mantiene `Screen`,
`Audio`, etc. desacoplados de la capa de configuración.
Ficheros de configuración por categoría:
- `data/config/`: `ui.yaml`, `input.yaml`, `lighting.yaml`, `sounds.yaml`,
`editor.yaml`.
- En la carpeta de sistema del usuario: `config.yaml`, `gameplay.yaml`,
`gamepad_configs.yaml`, `debug.yaml` (Debug), y `shaders/postfx.yaml`,
`shaders/crtpi.yaml`.
### 11.2. Carpeta de sistema por plataforma
`Director::createSystemFolder()` (`director.cpp:353`):
- Windows: `%APPDATA%\jailgames\aee_arcade`
- macOS: `~/Library/Application Support/jailgames/aee_arcade`
- Linux: `$XDG_CONFIG_HOME/jailgames/aee_arcade` (por defecto
`~/.config/jailgames/aee_arcade`)
Las capturas de pantalla y los presets de shader cuelgan de ahí.
### 11.3. Localización
`source/core/locale/locale.*` + `data/locale/{ca,en}.yaml`. Todo texto visible
pasa por `Locale::get().text(clave)`; si falta el fichero o la clave, devuelve
la propia clave como *fallback* (`director.cpp:124`). Idioma por defecto: `ca`
(`defaults.hpp:41`).
---
## 12. Modo demo / IA
**No existe un modo demo, *attract mode*, autoplay ni IA que controle a los
jugadores.** Lo he buscado explícitamente y no lo hay.
Aclaración para evitar malentendidos: la única aparición de la palabra "ai" en
el código es un **bloque YAML `ai:`** en las plantillas de `mummy` y
`rolling_rock`, que solo fija la **dirección inicial** del hazard:
```yaml
ai:
initial_direction: east # mummy.cpp:96, rolling_rock.cpp:41
```
No es un sistema de IA: es un parámetro de spawn. El movimiento de las momias
es lógica fija en `mummy.cpp` (patrullaje/persecución simple con consulta al
mundo para no caer en agujeros), no un módulo de IA separado ni dirigido por el
modo de juego.
Si en el futuro se quisiera un *attract mode*, el punto natural de enganche
sería el flujo de escenas (`scene_flow.hpp`) y/o sustituir el *polling* de
input del `Player` por una fuente sintética, pero **hoy nada de eso está
implementado**.
---
## 13. Convenciones y patrones recurrentes
Patrones que verás una y otra vez (reconocerlos acelera mucho la lectura):
- **Idioma de los comentarios**: valenciano/catalán. Los identificadores son en
inglés; las constantes locales suelen ir en `MAYÚSCULAS`.
- **Estilo C++ moderno**: `[[nodiscard]]` en casi todo getter, trailing return
types (`auto f() -> T`), `enum class` con tipo subyacente explícito,
`std::optional`/`std::variant` en vez de punteros nulos o `void*`. Hay
`.clang-format` y `.clang-tidy` en la raíz.
- **Meyers singleton con ciclo de vida manual**: los singletons que dependen de
SDL (`Screen`, `Audio`, `Cache` para audio) exponen `get()` pero su
`configure()`/`shutdown()` los llama explícitamente el `Director`, **no** el
destructor estático. El motivo (crashes en `vkDestroyInstance` al descargar
Vulkan/Wayland antes de tiempo) está documentado en
`screen.hpp:28` y `director.cpp:336`.
- **Inyección de dependencias por `std::function` / setters** para invertir la
dirección `core/` ↛ `game/` (ver §6.3).
- **Data-driven con fallback compilado**: nunca asumas que un valor es una
constante de código sin más; suele venir de un YAML con default en
`defaults.hpp`.
- **`std::any` solo en la frontera `Scene`↔`Director`** para no acoplar la base
de escenas a los tipos del juego (§3.1).
- **Composición sobre herencia para entidades** (§8.1).
- **Separación render-de-colisión**: el `Tilemap` que se ve y `Map::collides`
que bloquea son cosas distintas (un sarcófago se dibuja como actor pero el
`Map` lo trata como obstáculo).
- **Bloques `#ifdef _DEBUG`**: el editor de tilemap (`game/editor/`), los saltos
de sala (`[`/`]`), forzar apertura de puerta, y el arranque configurable por
`debug.yaml` solo existen en builds Debug.
- **Tests**: en `tests/` hay smoke tests y tests de FSM/animación
(`test_scene_flow`, `test_game_fsm`, `test_whip_attack`, etc.) con *stubs*
para aislar dependencias pesadas (audio, player).
- **Política de commits y branches**: ver `docs/convencions.md` (mensajes
cortos en valenciano, trabajo en ramas de feature, merge `--no-ff`).
---
## 14. Guía de navegación: "si quieres tocar X, mira Y"
| Quiero… | Empieza por… |
|---|---|
| Entender el arranque y el orden de init | `source/core/system/director.cpp` (ctor + `iterate()`) |
| Cambiar el orden update/render del frame | `Director::iterate()` (`director.cpp:509`) |
| Añadir una escena | `source/game/scenes/scene_flow.hpp` + `Director::buildScene` |
| Cómo se dibuja un sprite | `source/core/rendering/surface.cpp` (`renderWithColorReplace`) |
| El orden de pintado del juego | `GameScene::render()` (`game_scene.cpp:1682`) |
| El z-order / falsa perspectiva | `getFeetY()` + `stable_sort` en `game_scene.cpp:1755` |
| Iluminación / antorchas | `core/rendering/lighting.*` + `data/config/lighting.yaml` |
| Shaders / efecto CRT | `core/rendering/sdl3gpu/` + `data/shaders/` + `Options` |
| Escalado de ventana / fullscreen | `core/rendering/screen.cpp` (`adjustWindowSize`, `setScalingMode`) |
| Controles del jugador | `Player::Controls` (`player.hpp:23`) + `Player::tick` (`player.cpp`) |
| Hotkeys de sistema (F1–F12, ESC) | `core/input/global_inputs.cpp` + `data/config/input.yaml` |
| Menú de servicio (F9/SERVICE) | `source/game/ui/service_menu.*` |
| Mandos / vinculaciones | `core/input/gamepad_manager.*`, `gamepad_bindings.*` |
| Añadir un tipo de entidad | nueva clase en `game/entities/concrete/` + `REGISTER_ENTITY` |
| Eventos entre entidades | `game/entities/event_bus.hpp` |
| Lógica de una sala (intro/outro) | `RoomFsm` (`game/scenes/room_fsm.*`) + `data/scenes/game/game.yaml` |
| Game over / continue | `GameOverFsm` (`game/scenes/game_over_fsm.*`) |
| Sarcófagos / puerta / pintado | `concrete/sarcophagus.*`, `concrete/door.*`, `game/map/paint_layer.*` |
| Geometría/colisión de la sala | `source/game/map/map.hpp` |
| El tileset que se ve | `source/game/map/tilemap.*` + `data/tilesets/` |
| Progresión de salas | `game/levels/level_manager.*` + `data/maps/levels.yaml` |
| Editar mapas (Debug) | `source/game/editor/` |
| Cargar un recurso | `core/resources/resource_cache.*` + `resource_helper.*` |
| Empaquetar datos | `tools/pack_resources/` |
| Valores por defecto / constantes | `source/utils/defaults.hpp` |
| Textos / traducciones | `core/locale/locale.*` + `data/locale/*.yaml` |
| HUD (vidas/score/tesoros) | `source/game/ui/hud.*` (escucha `player_stats_changed`) |
| Audio (música/sfx) | `core/audio/` (`audio.*`, `jail_audio.*`) |
---
*Documento generado a partir de la lectura directa del código en el commit
actual de la rama `main`. Si algo aquí no cuadra con el código, el código
manda: actualiza este documento.*
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 164 KiB

-14
View File
@@ -90,7 +90,6 @@ void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossf
} }
// Pausa la música (l'estat el transiciona Engine::pauseMusic) // Pausa la música (l'estat el transiciona Engine::pauseMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::pauseMusic() { void Audio::pauseMusic() {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) { if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
engine_->pauseMusic(); engine_->pauseMusic();
@@ -98,7 +97,6 @@ void Audio::pauseMusic() {
} }
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic) // Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::resumeMusic() { void Audio::resumeMusic() {
if (music_enabled_ && getMusicState() == MusicState::PAUSED) { if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
engine_->resumeMusic(); engine_->resumeMusic();
@@ -106,14 +104,12 @@ void Audio::resumeMusic() {
} }
// Atura la música (l'estat el transiciona Engine::stopMusic) // Atura la música (l'estat el transiciona Engine::stopMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::stopMusic() { void Audio::stopMusic() {
if (music_enabled_) { if (music_enabled_) {
engine_->stopMusic(); engine_->stopMusic();
} }
} }
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::setMusicSpeed(float ratio) { void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) { if (music_enabled_) {
engine_->setMusicSpeed(ratio); engine_->setMusicSpeed(ratio);
@@ -121,7 +117,6 @@ void Audio::setMusicSpeed(float 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
void Audio::playSound(const std::string& name, Group group) { void Audio::playSound(const std::string& name, Group group) {
if (sound_enabled_) { if (sound_enabled_) {
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group)); engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
@@ -129,7 +124,6 @@ void Audio::playSound(const std::string& name, Group group) {
} }
// Reprodueix un so per punter directe // Reprodueix un so per punter directe
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(Ja::Sound* sound, Group group) { void Audio::playSound(Ja::Sound* sound, Group group) {
if (sound_enabled_ && sound != nullptr) { if (sound_enabled_ && sound != nullptr) {
engine_->playSound(sound, 0, static_cast<int>(group)); engine_->playSound(sound, 0, static_cast<int>(group));
@@ -141,7 +135,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
// processa tot el sample amb el ratio des del primer pull del callback. // processa tot el sample amb el ratio des del primer pull del callback.
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem // Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// la crida al ratio — sense efectes col·laterals. // la crida al ratio — sense efectes col·laterals.
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::playSound(const std::string& name, Group group, float speed) { void Audio::playSound(const std::string& name, Group group, float speed) {
if (!sound_enabled_) { return; } if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
@@ -155,7 +148,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) {
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no // Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound // existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
// sec — l'usuari sent el so encara que la cua no s'apliqui. // sec — l'usuari sent el so encara que la cua no s'apliqui.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) { void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; } if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
@@ -175,7 +167,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix // Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
// fallback que playSoundWithEcho. // fallback que playSoundWithEcho.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) { void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; } if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
@@ -194,7 +185,6 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
} }
// Atura tots els sons // Atura tots els sons
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::stopAllSounds() { void Audio::stopAllSounds() {
if (sound_enabled_) { if (sound_enabled_) {
engine_->stopChannel(-1); engine_->stopChannel(-1);
@@ -202,7 +192,6 @@ void Audio::stopAllSounds() {
} }
// Fa una fosa de sortida de la música // Fa una fosa de sortida de la música
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::fadeOutMusic(int milliseconds) { void Audio::fadeOutMusic(int milliseconds) {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) { if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
engine_->fadeOutMusic(milliseconds); engine_->fadeOutMusic(milliseconds);
@@ -212,7 +201,6 @@ void Audio::fadeOutMusic(int milliseconds) {
// Registra un callback que el motor dispararà quan la pista actual acabi de // Registra un callback que el motor dispararà quan la pista actual acabi de
// drenar (times == 0 + stream buit). S'executa al mateix thread que // drenar (times == 0 + stream buit). S'executa al mateix thread que
// Audio::update (render loop); els consumidors no poden fer I/O blocant. // Audio::update (render loop); els consumidors no poden fer I/O blocant.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::setOnMusicEnded(std::function<void()> callback) { void Audio::setOnMusicEnded(std::function<void()> callback) {
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); } if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
} }
@@ -251,13 +239,11 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
} }
// Estableix el volum dels sons (float 0.0..1.0) // Estableix el volum dels sons (float 0.0..1.0)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::setSoundVolume(float sound_volume, Group group) { void Audio::setSoundVolume(float sound_volume, Group group) {
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group)); engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
} }
// Estableix el volum de la música (float 0.0..1.0) // Estableix el volum de la música (float 0.0..1.0)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::setMusicVolume(float music_volume) { void Audio::setMusicVolume(float music_volume) {
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_)); engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
} }
-1
View File
@@ -5,7 +5,6 @@
#include <functional> // Per a std::function #include <functional> // Per a std::function
#include <memory> // Per a std::unique_ptr #include <memory> // Per a std::unique_ptr
#include <string> // Per a string #include <string> // Per a string
#include <utility> // Per a move
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els // Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
// tres símbols (Music/Sound per al punter que exposa la API i Engine per al // tres símbols (Music/Sound per al punter que exposa la API i Engine per al
+1 -1
View File
@@ -15,7 +15,7 @@
// (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi). // (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi).
#define STB_VORBIS_HEADER_ONLY #define STB_VORBIS_HEADER_ONLY
// clang-format off // clang-format off
// NOLINTNEXTLINE(bugprone-suspicious-include) // NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis és single-file: la macro de dalt limita aquest TU a només-declaracions; la implementació viu a external/stb_vorbis_impl.cpp.
#include "external/stb_vorbis.c" #include "external/stb_vorbis.c"
// clang-format on // clang-format on
+16 -8
View File
@@ -198,42 +198,50 @@ void DefineButtons::render() const {
// Títol: nom del mando. // Títol: nom del mando.
const std::string TITLE = pad_name_; const std::string TITLE = pad_name_;
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X, CENTER_X,
cursor_y, cursor_y,
TITLE, TITLE,
TITLE_KERNING, TITLE_KERNING,
UiConfig::get().menuTitle()); UiConfig::get().menuTitle(),
1,
UiConfig::get().menuTitleOutline());
cursor_y += TITLE_H + LINE_GAP; cursor_y += TITLE_H + LINE_GAP;
if (message_shown_) { if (message_shown_) {
// Pantalla final: missatge de configuració completa. // Pantalla final: missatge de configuració completa.
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X, CENTER_X,
cursor_y, cursor_y,
Locale::get().text("menu.values.define_buttons_complete"), Locale::get().text("menu.values.define_buttons_complete"),
ITEM_KERNING, ITEM_KERNING,
UiConfig::get().menuValue()); UiConfig::get().menuValue(),
1,
UiConfig::get().menuValueOutline());
return; return;
} }
// Instrucció + acció pendent. Replica visualment el WindowMessage de // Instrucció + acció pendent. Replica visualment el WindowMessage de
// coffee_crisis_arcade_edition. // coffee_crisis_arcade_edition.
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X, CENTER_X,
cursor_y, cursor_y,
Locale::get().text("menu.values.press_button"), Locale::get().text("menu.values.press_button"),
ITEM_KERNING, ITEM_KERNING,
UiConfig::get().menuLabel()); UiConfig::get().menuLabel(),
1,
UiConfig::get().menuLabelOutline());
cursor_y += ITEM_H + LINE_GAP; cursor_y += ITEM_H + LINE_GAP;
if (index_ < sequence_.size()) { if (index_ < sequence_.size()) {
const auto& current = sequence_[index_]; const auto& current = sequence_[index_];
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X, CENTER_X,
cursor_y, cursor_y,
Locale::get().text(current.label_key), Locale::get().text(current.label_key),
ITEM_KERNING, ITEM_KERNING,
UiConfig::get().menuCursor()); UiConfig::get().menuCursor(),
1,
UiConfig::get().menuCursorOutline());
} }
} }
-1
View File
@@ -1,6 +1,5 @@
#include "core/input/gamepad_bindings.hpp" #include "core/input/gamepad_bindings.hpp"
#include <algorithm>
#include <array> #include <array>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
+2 -3
View File
@@ -14,9 +14,8 @@
// //
// El polling real de botons i eixos NO es fa a través d'aquesta classe: // El polling real de botons i eixos NO es fa a través d'aquesta classe:
// els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a // els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a
// multi-jugador futur) i el passen a `InputState::getGamepadButton/Axis`, // multi-jugador futur) i el passen directament a
// que ho filtra pel flag de bloqueig de la UI (mateixa estratègia que // `SDL_GetGamepadButton`/`SDL_GetGamepadAxis`.
// amb el teclat).
class GamepadManager { class GamepadManager {
public: public:
static auto get() -> GamepadManager&; static auto get() -> GamepadManager&;
-2
View File
@@ -9,8 +9,6 @@
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp" #include "core/rendering/notification_presets.hpp"
#include "core/rendering/notifier.hpp"
#include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/screenshot.hpp" #include "core/rendering/screenshot.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
-39
View File
@@ -1,39 +0,0 @@
#include "core/input/input_state.hpp"
#include <array>
namespace InputState {
namespace {
bool blocked = false;
// Buffer retornat quan el bloqueig és actiu. `SDL_GetKeyboardState`
// retorna un pointer d'una mida igual a `SDL_SCANCODE_COUNT`; el
// nostre buffer ha de ser com a mínim igual de gran perquè els
// consumidors puguin indexar-lo amb qualsevol scancode sense
// llegir fora de rang.
auto zeroKeyboard() -> const bool* {
static const std::array<bool, SDL_SCANCODE_COUNT> ZEROS{};
return ZEROS.data();
}
} // namespace
void setBlocked(bool value) { blocked = value; }
auto isBlocked() -> bool { return blocked; }
auto getKeyboardState() -> const bool* {
return blocked ? zeroKeyboard() : SDL_GetKeyboardState(nullptr);
}
auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool {
return (blocked || gp == nullptr) ? false : SDL_GetGamepadButton(gp, b);
}
auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16 {
return (blocked || gp == nullptr) ? static_cast<Sint16>(0) : SDL_GetGamepadAxis(gp, ax);
}
} // namespace InputState
-46
View File
@@ -1,46 +0,0 @@
#pragma once
// Wrapper de l'estat d'input (polling) amb gating de bloqueig.
//
// El service menu (i qualsevol UI modal futura) ha de poder "amagar" l'estat
// d'entrada als consumidors de polling — el jugador, entitats controlades
// per l'usuari, etc. — mentre la UI captura l'atenció, sense interrompre la
// resta del joc. Per això tots els consumidors de polling han de llegir a
// través d'aquest mòdul i no cridar `SDL_GetKeyboardState` directament. Si
// `setBlocked(true)` està actiu, el wrapper retorna un buffer tot-a-zero;
// altrament, la crida és transparent (mateix pointer que SDL).
//
// El menú NO passa per aquí: consumeix events (`SDL_EVENT_KEY_DOWN`) abans
// que l'escena els vegi, així que la seva navegació segueix funcionant per
// molt que el flag estigui posat.
//
// El mateix patró aplica al gamepad: `getGamepadButton`/`getGamepadAxis`
// retornen zero/false quan el bloqueig és actiu. Els mandos concrets els
// obtén el caller via `GamepadManager::get().getPrimary()` o similars.
#include <SDL3/SDL.h>
namespace InputState {
// Activa/desactiva el bloqueig global de polling. El ServiceMenu el
// crida a `toggle()`/`close()`. No es toca des de cap altre lloc.
void setBlocked(bool value);
[[nodiscard]] auto isBlocked() -> bool;
// Equivalent a `SDL_GetKeyboardState(nullptr)`: retorna un buffer de
// `SDL_SCANCODE_COUNT` bools. Si l'input està bloquejat, tots zero —
// el consumidor veu com si cap tecla estigués premuda, sense branques
// explícites. El buffer és estable mentre dura la sessió; no cal
// guardar-lo frame a frame.
[[nodiscard]] auto getKeyboardState() -> const bool*;
// Versió filtrada de `SDL_GetGamepadButton`. Retorna false si el bloqueig
// és actiu o si `gp` és nullptr (cas habitual: no hi ha cap mand).
[[nodiscard]] auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool;
// Versió filtrada de `SDL_GetGamepadAxis`. Retorna 0 si el bloqueig
// és actiu o si `gp` és nullptr.
[[nodiscard]] auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16;
} // namespace InputState
-1
View File
@@ -1,6 +1,5 @@
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include <fstream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
-1
View File
@@ -1,6 +1,5 @@
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include <fstream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <cstdint>
#include <random> #include <random>
class Surface; class Surface;
-1
View File
@@ -4,7 +4,6 @@
#include <algorithm> #include <algorithm>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
namespace Effects::Shadow { namespace Effects::Shadow {
+1
View File
@@ -4,6 +4,7 @@
#include <numeric> #include <numeric>
#include <utility> #include <utility>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/ui/ui_config.hpp" #include "core/ui/ui_config.hpp"
-2
View File
@@ -7,9 +7,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "utils/defines.hpp"
class Text; class Text;
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <array> #include <array>
#include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
-1
View File
@@ -7,7 +7,6 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <memory> #include <memory>
#include <ranges>
#include "core/rendering/canvas.hpp" #include "core/rendering/canvas.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp" #include "core/rendering/sdl3gpu/viewport.hpp"
+1 -1
View File
@@ -17,7 +17,7 @@ namespace Screenshot {
// Estat de mòdul: ruta base on s'escriu la subcarpeta // Estat de mòdul: ruta base on s'escriu la subcarpeta
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback // `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
// si el Director no ha pogut establir la ruta per plataforma). // si el Director no ha pogut establir la ruta per plataforma).
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) -- estat de mòdul (singleton-like): s'estableix una vegada al startup via setBaseDir() i es llegeix N vegades; encapsulat dins del namespace anònim, no és accessible des de fora.
// Construeix la ruta del directori on van les captures: // Construeix la ruta del directori on van les captures:
// `<base>/screenshots/`. Si la base és buida, retorna // `<base>/screenshots/`. Si la base és buida, retorna
@@ -22,7 +22,7 @@ struct PostVOut {
struct PostFXUniforms { struct PostFXUniforms {
float vignette_strength; float vignette_strength;
float chroma_strength; float chroma_min;
float scanline_strength; float scanline_strength;
float screen_height; float screen_height;
float mask_strength; float mask_strength;
@@ -32,7 +32,7 @@ struct PostFXUniforms {
float pixel_scale; float pixel_scale;
float time; float time;
float flicker; float flicker;
float pad2; float chroma_max;
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML) // vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio; float scan_dark_ratio;
float scan_dark_floor; float scan_dark_floor;
@@ -40,6 +40,18 @@ struct PostFXUniforms {
float pad3; float pad3;
}; };
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel.
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
float2 tex_size = float2(scene.get_width(), scene.get_height());
float px = uv_target.x * tex_size.x - 0.5f;
float p_floor = floor(px);
float f = px - p_floor;
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
static float3 rgb_to_ycc(float3 rgb) { static float3 rgb_to_ycc(float3 rgb) {
return float3( return float3(
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b, 0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
@@ -94,9 +106,14 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
colour = base; colour = base;
} }
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f)); // Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r; // queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b; // NEAREST sampler amb offsets subpíxel.
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
}
if (u.gamma_strength > 0.0f) { if (u.gamma_strength > 0.0f) {
float3 lin = pow(colour, float3(2.4f)); float3 lin = pow(colour, float3(2.4f));
@@ -234,7 +234,8 @@ namespace Rendering {
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) { void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
uniforms_.vignette_strength = params.vignette; uniforms_.vignette_strength = params.vignette;
uniforms_.scanline_strength = params.scanlines; uniforms_.scanline_strength = params.scanlines;
uniforms_.chroma_strength = params.chroma; uniforms_.chroma_min = params.chroma_min;
uniforms_.chroma_max = params.chroma_max;
uniforms_.mask_strength = params.mask; uniforms_.mask_strength = params.mask;
uniforms_.gamma_strength = params.gamma; uniforms_.gamma_strength = params.gamma;
uniforms_.curvature = params.curvature; uniforms_.curvature = params.curvature;
@@ -15,7 +15,7 @@
struct PostFXUniforms { struct PostFXUniforms {
// vec4 #0 // vec4 #0
float vignette_strength; float vignette_strength;
float chroma_strength; float chroma_min;
float scanline_strength; float scanline_strength;
float screen_height; float screen_height;
// vec4 #1 // vec4 #1
@@ -27,7 +27,7 @@ struct PostFXUniforms {
float pixel_scale; float pixel_scale;
float time; float time;
float flicker; float flicker;
float pad2; float chroma_max;
// vec4 #3 — paràmetres de scanlines exposats al preset YAML // vec4 #3 — paràmetres de scanlines exposats al preset YAML
float scan_dark_ratio; float scan_dark_ratio;
float scan_dark_floor; float scan_dark_floor;
@@ -92,7 +92,7 @@ namespace Rendering {
PostFXUniforms uniforms_{ PostFXUniforms uniforms_{
.vignette_strength = 0.0F, .vignette_strength = 0.0F,
.chroma_strength = 0.0F, .chroma_min = 0.0F,
.scanline_strength = 0.0F, .scanline_strength = 0.0F,
.screen_height = 0.0F, .screen_height = 0.0F,
.mask_strength = 0.0F, .mask_strength = 0.0F,
@@ -102,7 +102,7 @@ namespace Rendering {
.pixel_scale = 1.0F, .pixel_scale = 1.0F,
.time = 0.0F, .time = 0.0F,
.flicker = 0.0F, .flicker = 0.0F,
.pad2 = 0.0F, .chroma_max = 0.0F,
// Defaults canònics: 2/3 brillant + 1/3 fosca, brillantor 42%, suavitzat complet (1 px físic). // Defaults canònics: 2/3 brillant + 1/3 fosca, brillantor 42%, suavitzat complet (1 px físic).
.scan_dark_ratio = 0.333F, .scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F, .scan_dark_floor = 0.42F,
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -19,7 +19,10 @@ namespace Rendering {
struct PostFXParams { struct PostFXParams {
float vignette{0.0F}; float vignette{0.0F};
float scanlines{0.0F}; float scanlines{0.0F};
float chroma{0.0F}; // Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
// queda estàtica. Permet un "floor" perquè la imatge mai sigui lliure de chroma.
float chroma_min{0.0F};
float chroma_max{0.0F};
float mask{0.0F}; float mask{0.0F};
float gamma{0.0F}; float gamma{0.0F};
float curvature{0.0F}; float curvature{0.0F};
+1 -1
View File
@@ -62,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 ]"); printStatus("Surface", getFileName(file_path), StatusKind::SUCCESS);
} }
void Surface::clear(Uint8 color) { void Surface::clear(Uint8 color) {
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <array> #include <array>
#include <cstdint>
#include <memory> #include <memory>
#include <numeric> #include <numeric>
#include <string> #include <string>
+1 -1
View File
@@ -101,7 +101,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
} }
} }
printWithDots("Text File", getFileName(file_path), "[ LOADED ]"); printStatus("Text File", getFileName(file_path), StatusKind::SUCCESS);
return tf; return tf;
} }
+4 -4
View File
@@ -37,13 +37,13 @@ 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 ]"); printStatus("Animation", getFileName(file_path), StatusKind::SUCCESS);
} }
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 ]"); printStatus("Palette", getFileName(file_path), StatusKind::SUCCESS);
} }
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> { auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
@@ -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 ]"); printStatus("Music", getFileName(file_path), StatusKind::SUCCESS);
} }
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,7 +96,7 @@ 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 ]"); printStatus("Sound", getFileName(file_path), StatusKind::SUCCESS);
} }
auto Cache::getMusic(const std::string& name) -> Ja::Music* { auto Cache::getMusic(const std::string& name) -> Ja::Music* {
+3 -7
View File
@@ -4,6 +4,7 @@
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <numeric>
#include "utils/log.hpp" #include "utils/log.hpp"
@@ -19,12 +20,7 @@ namespace Resource {
auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t { auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t {
// djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai // djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai
// reutilitzem eines de diagnòstic entre projectes. // reutilitzem eines de diagnòstic entre projectes.
std::uint32_t checksum = 0x12345678; return std::accumulate(data.begin(), data.end(), std::uint32_t{0x12345678}, [](std::uint32_t acc, unsigned char b) { return ((acc << 5) + acc) + b; });
for (unsigned char b : data) {
// cppcheck-suppress useStlAlgorithm
checksum = ((checksum << 5) + checksum) + b;
}
return checksum;
} }
void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) { void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) {
@@ -151,7 +147,7 @@ namespace Resource {
return false; return false;
} }
// cppcheck-suppress useStlAlgorithm // cppcheck-suppress useStlAlgorithm -- bucle amb early-return en error (addFile fallit) + filtre amb continue: no es pot expressar net amb std::ranges::all_of perquè la lògica combina filtratge i acumulació amb sortida prematura.
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
if (!entry.is_regular_file()) { continue; } if (!entry.is_regular_file()) { continue; }
+9 -20
View File
@@ -9,7 +9,6 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
#include <variant>
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/audio/sound_effects_config.hpp" #include "core/audio/sound_effects_config.hpp"
@@ -24,7 +23,6 @@
#include "core/rendering/render_info.hpp" #include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/screenshot.hpp" #include "core/rendering/screenshot.hpp"
#include "core/rendering/surface.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
@@ -35,8 +33,8 @@
#include "game/gameplay_config.hpp" #include "game/gameplay_config.hpp"
#ifdef _DEBUG #ifdef _DEBUG
#include "game/editor/editor_config.hpp" #include "game/editor/editor_config.hpp"
#endif
#include "game/levels/level_manager.hpp" #include "game/levels/level_manager.hpp"
#endif
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/game_scene.hpp" #include "game/scenes/game_scene.hpp"
#include "game/scenes/intro_scene.hpp" #include "game/scenes/intro_scene.hpp"
@@ -446,9 +444,12 @@ void Director::processGamepadEvents() {
} }
void Director::processMenuInput() { void Director::processMenuInput() {
// Intercepta input abans que l'escena vegi els events. Si el menú està // Intercepta input abans que l'escena vegi els events. Amb el menú
// obert, consumeix TOTS els KEY_DOWN perquè ni GlobalInputs (F1–F8, // obert, `handleEvent` consumeix tot KEY_DOWN excepte les F-keys
// F11, F12) ni el jugador els veuen. F9 obre el menú quan està tancat. // (F1–F12) i ESC, que són hotkeys d'UI omnipresents i passen avall
// a GlobalInputs (ESC manté la doble pulsació per a confirmar l'exit
// independentment del context). Quan el menú està tancat, la branca
// de baix detecta `menu_toggle` (F12 per defecte) i obre el menú.
// Erase invalida iteradors, per això actualitzem `it` a cada branca. // Erase invalida iteradors, per això actualitzem `it` a cada branca.
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) { for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
if (ServiceMenu::get().isOpen() && ServiceMenu::get().handleEvent(*it)) { if (ServiceMenu::get().isOpen() && ServiceMenu::get().handleEvent(*it)) {
@@ -465,16 +466,6 @@ void Director::processMenuInput() {
void Director::switchSceneIfRequested() { void Director::switchSceneIfRequested() {
if (!current_scene_) { return; } if (!current_scene_) { return; }
// Prioritat al menú: si un item del ServiceMenu ha encuat una
// transició (típicament "Reiniciar" → LOGO amb reset), s'aplica
// primer. Substitueix l'`ExitRequest` que pugui haver publicat
// l'escena en aquest mateix frame — l'usuari ha cancel·lat l'estat
// de joc explícitament i no ens interessa mantenir-ne la sortida.
if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) {
Log::info("Director", "scene transition (menu) → {}", toString(menu_req->next));
current_scene_ = buildScene(menu_req->next, menu_req->params);
return;
}
// L'escena ha publicat un `ExitRequest` dins de `exit_request_`. // L'escena ha publicat un `ExitRequest` dins de `exit_request_`.
// Director el desempaqueta i construeix la nova escena via // Director el desempaqueta i construeix la nova escena via
// `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el // `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el
@@ -497,10 +488,8 @@ auto Director::buildScene(SceneId id, const SceneParams& params) -> std::unique_
// ctors antics no l'utilitzen directament. Quan totes estiguen // ctors antics no l'utilitzen directament. Quan totes estiguen
// migrades a `ExitRequest`, aquestes lambdes desapareixeran. // migrades a `ExitRequest`, aquestes lambdes desapareixeran.
switch (id) { switch (id) {
case SceneId::LOGO: { case SceneId::LOGO:
const auto& p = std::get<LogoParams>(params); return std::make_unique<LogoScene>(logo_full_surface_);
return std::make_unique<LogoScene>(logo_full_surface_, p.reset);
}
case SceneId::TITLE: case SceneId::TITLE:
return std::make_unique<TitleScene>(placeholder_text_, copyright_text_, title_bg_surface_, game_logo_surface_, mini_logo_surface_); return std::make_unique<TitleScene>(placeholder_text_, copyright_text_, title_bg_surface_, game_logo_surface_, mini_logo_surface_);
case SceneId::INTRO: { case SceneId::INTRO: {
+51
View File
@@ -0,0 +1,51 @@
// relaunch.cpp - Implementació del reinici en calent
#include "core/system/relaunch.hpp"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <iostream>
#ifdef _WIN32
#include <process.h> // _execv
#else
#include <unistd.h> // execv
#endif
namespace {
// Estat global (process-scope). Aquesta TU és l'única que gestiona el
// reinici, així que els statics interns no s'escapen.
char** g_argv = nullptr;
bool g_requested = false;
} // namespace
namespace System::Relaunch {
void setArgv(int /*argc*/, char** argv) {
g_argv = argv;
}
void request() {
g_requested = true;
}
void execIfRequested() {
if (!g_requested || g_argv == nullptr || g_argv[0] == nullptr) {
return;
}
std::cout << "[Relaunch] Reiniciant " << g_argv[0] << "...\n";
#ifdef _WIN32
_execv(g_argv[0], g_argv);
#else
execv(g_argv[0], g_argv);
#endif
// Si arribem aquí, execv ha fallat. Tots els subsistemes ja estan
// destruïts; sortim amb error i el shell rebrà el codi.
std::cerr << "[Relaunch] Ha fallat: " << std::strerror(errno) << '\n';
std::exit(EXIT_FAILURE);
}
} // namespace System::Relaunch
+28
View File
@@ -0,0 +1,28 @@
// relaunch.hpp - Reinici en calent del procés (execv)
//
// Helper desacoblat perquè el menú de servei demani un reinici sense conèixer
// ni Director ni main.cpp. Patró d'ús:
//
// main() → Relaunch::setArgv(argc, argv) (a l'arrencada)
// ServiceMenu → Relaunch::request() (en activar "Reiniciar")
// main() → Relaunch::execIfRequested() (al final de SDL_AppQuit)
//
// L'execv() reemplaça el procés actual: si torna, ha fallat. Es crida després
// de destruir el Director per garantir que GPU/àudio/SDL ja són morts.
#pragma once
namespace System::Relaunch {
// Emmagatzema l'argv original. Cal cridar-ho una vegada des de main.
void setArgv(int argc, char** argv);
// Demana un reinici (no actua immediatament; només marca el flag).
void request();
// Si hi ha reinici demanat i tenim argv vàlid, fa execv. En cas d'èxit
// no torna. Si execv falla, registra l'error per stderr i surt amb
// EXIT_FAILURE (no podem reprendre — els subsistemes ja estan destruïts).
void execIfRequested();
} // namespace System::Relaunch
+30 -7
View File
@@ -81,6 +81,10 @@ UiConfig::UiConfig()
menu_value_(Defaults::UI::MENU_VALUE), menu_value_(Defaults::UI::MENU_VALUE),
menu_cursor_(Defaults::UI::MENU_CURSOR), menu_cursor_(Defaults::UI::MENU_CURSOR),
menu_disabled_(Defaults::UI::MENU_DISABLED), menu_disabled_(Defaults::UI::MENU_DISABLED),
menu_title_outline_(Defaults::UI::MENU_TITLE_OUTLINE),
menu_label_outline_(Defaults::UI::MENU_LABEL_OUTLINE),
menu_value_outline_(Defaults::UI::MENU_VALUE_OUTLINE),
menu_cursor_outline_(Defaults::UI::MENU_CURSOR_OUTLINE),
hud_bg_(Defaults::UI::HUD_BG), hud_bg_(Defaults::UI::HUD_BG),
hud_p1_label_(Defaults::UI::HUD_P1_LABEL), hud_p1_label_(Defaults::UI::HUD_P1_LABEL),
hud_p1_value_(Defaults::UI::HUD_P1_VALUE), hud_p1_value_(Defaults::UI::HUD_P1_VALUE),
@@ -215,19 +219,38 @@ void UiConfig::load(const std::string& file_path) {
readColorIndex(c, "notification_bg", notification_bg_); readColorIndex(c, "notification_bg", notification_bg_);
readColorIndex(c, "exit_text", exit_text_); readColorIndex(c, "exit_text", exit_text_);
readColorIndex(c, "exit_bg", exit_bg_); readColorIndex(c, "exit_bg", exit_bg_);
readColorIndex(c, "menu_bg", menu_bg_);
readColorIndex(c, "menu_border", menu_border_);
readColorIndex(c, "menu_title", menu_title_);
readColorIndex(c, "menu_label", menu_label_);
readColorIndex(c, "menu_value", menu_value_);
readColorIndex(c, "menu_cursor", menu_cursor_);
readColorIndex(c, "menu_disabled", menu_disabled_);
readColorIndex(c, "hud_bg", hud_bg_); readColorIndex(c, "hud_bg", hud_bg_);
readColorIndex(c, "hud_flash", hud_flash_); readColorIndex(c, "hud_flash", hud_flash_);
readColorIndex(c, "room_text", room_text_); readColorIndex(c, "room_text", room_text_);
readColorIndex(c, "room_text_outline", room_text_outline_); readColorIndex(c, "room_text_outline", room_text_outline_);
} }
// Tema actiu del menú: `menu_theme` és el nom del bloc dins de
// `menu_themes` que aporta els 7 slots de color. Si qualsevol
// de les dues claus manca o el nom no existeix, els slots
// queden amb el default compilat (= tema "classic" hardcoded
// a Defaults::UI). Per-key dins del tema també és opcional.
if (yaml.contains("menu_theme") && yaml.contains("menu_themes")) {
const auto THEME_NAME = yaml["menu_theme"].get_value<std::string>();
const auto& themes = yaml["menu_themes"];
if (themes.contains(THEME_NAME)) {
const auto& t = themes[THEME_NAME];
readColorIndex(t, "bg", menu_bg_);
readColorIndex(t, "border", menu_border_);
readColorIndex(t, "title", menu_title_);
readColorIndex(t, "label", menu_label_);
readColorIndex(t, "value", menu_value_);
readColorIndex(t, "cursor", menu_cursor_);
readColorIndex(t, "disabled", menu_disabled_);
readColorIndex(t, "title_outline", menu_title_outline_);
readColorIndex(t, "label_outline", menu_label_outline_);
readColorIndex(t, "value_outline", menu_value_outline_);
readColorIndex(t, "cursor_outline", menu_cursor_outline_);
} else {
Log::warn("UiConfig", "menu_theme '{}' no existeix a menu_themes — s'usen defaults", THEME_NAME);
}
}
// Famílies de color del HUD: blocs `hud_p1`, `hud_center`, // Famílies de color del HUD: blocs `hud_p1`, `hud_center`,
// `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i // `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i
// cada clau són opcionals — el que manqui cau al default // cada clau són opcionals — el que manqui cau al default
+8
View File
@@ -65,6 +65,10 @@ class UiConfig {
[[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; } [[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; }
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; } [[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; } [[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
[[nodiscard]] auto menuTitleOutline() const -> Uint8 { return menu_title_outline_; }
[[nodiscard]] auto menuLabelOutline() const -> Uint8 { return menu_label_outline_; }
[[nodiscard]] auto menuValueOutline() const -> Uint8 { return menu_value_outline_; }
[[nodiscard]] auto menuCursorOutline() const -> Uint8 { return menu_cursor_outline_; }
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; } [[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
// Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada // Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada
// família té label (etiqueta apagada), value (valor destacat) i // família té label (etiqueta apagada), value (valor destacat) i
@@ -214,6 +218,10 @@ class UiConfig {
Uint8 menu_value_; Uint8 menu_value_;
Uint8 menu_cursor_; Uint8 menu_cursor_;
Uint8 menu_disabled_; Uint8 menu_disabled_;
Uint8 menu_title_outline_;
Uint8 menu_label_outline_;
Uint8 menu_value_outline_;
Uint8 menu_cursor_outline_;
Uint8 hud_bg_; Uint8 hud_bg_;
Uint8 hud_p1_label_; Uint8 hud_p1_label_;
Uint8 hud_p1_value_; Uint8 hud_p1_value_;
+1 -3
View File
@@ -345,9 +345,7 @@ namespace {
} // namespace } // namespace
// cppcheck-suppress constParameterReference // cppcheck-suppress constParameterReference -- Tilemap& mutable per simetria amb `toggle()` i per futures invalidacions de caches d'edició; cppcheck no pot anticipar que les futures mutacions estan previstes en el contracte.
// Tilemap& mutable per simetria amb `toggle()` i per futures
// invalidacions de caches d'edició, encara que `save()` sigui const.
void Editor::exit(Tilemap& tilemap, const EntityManager& entities) { void Editor::exit(Tilemap& tilemap, const EntityManager& entities) {
if (!active_) { return; } if (!active_) { return; }
// Constraint del nivell: cal exactament una porta i com a mínim un // Constraint del nivell: cal exactament una porta i com a mínim un
+5 -10
View File
@@ -1,6 +1,8 @@
#include "game/entities/animation_component.hpp" #include "game/entities/animation_component.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <iterator>
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>
@@ -33,8 +35,7 @@ namespace {
if (speeds.empty()) { if (speeds.empty()) {
return std::vector<float>(frame_count, K_DEFAULT_FRAME_SECONDS); return std::vector<float>(frame_count, K_DEFAULT_FRAME_SECONDS);
} }
// cppcheck-suppress useStlAlgorithm std::ranges::transform(speeds, speeds.begin(), sanitizeSpeed);
for (auto& s : speeds) { s = sanitizeSpeed(s); }
if (speeds.size() == frame_count) { return speeds; } if (speeds.size() == frame_count) { return speeds; }
if (speeds.size() > frame_count) { if (speeds.size() > frame_count) {
speeds.resize(frame_count); speeds.resize(frame_count);
@@ -50,10 +51,7 @@ namespace {
// component vàlid. // component vàlid.
auto parseFrames(const fkyaml::node& anim) -> std::vector<int> { auto parseFrames(const fkyaml::node& anim) -> std::vector<int> {
std::vector<int> frames; std::vector<int> frames;
for (const auto& f : anim["frames"]) { std::ranges::transform(anim["frames"], std::back_inserter(frames), [](const fkyaml::node& f) { return f.template get_value<int>(); });
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); } if (frames.empty()) { frames.push_back(0); }
return frames; return frames;
} }
@@ -66,10 +64,7 @@ namespace {
std::vector<float> speeds_secs; std::vector<float> speeds_secs;
const auto& fm = anim["frame_ms"]; const auto& fm = anim["frame_ms"];
if (fm.is_sequence()) { if (fm.is_sequence()) {
for (const auto& v : fm) { std::ranges::transform(fm, std::back_inserter(speeds_secs), [](const fkyaml::node& v) { return static_cast<float>(v.template get_value<int>()) / 1000.0F; });
// cppcheck-suppress useStlAlgorithm
speeds_secs.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F);
}
} else { } else {
const float SECS = static_cast<float>(fm.get_value<int>()) / 1000.0F; const float SECS = static_cast<float>(fm.get_value<int>()) / 1000.0F;
speeds_secs.assign(frame_count, SECS); speeds_secs.assign(frame_count, SECS);
@@ -2,10 +2,10 @@
#include <algorithm> #include <algorithm>
#include <filesystem> #include <filesystem>
#include <iterator>
#include <string> #include <string>
#include <vector> #include <vector>
#include "game/entities/concrete/mummy.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/player.hpp" #include "game/entities/player.hpp"
@@ -41,10 +41,7 @@ namespace {
if (!yaml.contains("animations") || !yaml["animations"].contains("swing")) { return; } if (!yaml.contains("animations") || !yaml["animations"].contains("swing")) { return; }
const auto& sw = yaml["animations"]["swing"]; const auto& sw = yaml["animations"]["swing"];
if (!sw.contains("active_frames")) { return; } if (!sw.contains("active_frames")) { return; }
for (const auto& f : sw["active_frames"]) { std::ranges::transform(sw["active_frames"], std::back_inserter(active_out), [](const fkyaml::node& f) { return f.template get_value<int>(); });
// cppcheck-suppress useStlAlgorithm
active_out.push_back(f.get_value<int>());
}
} }
} // namespace } // namespace
+13 -12
View File
@@ -53,18 +53,19 @@ class EntityFactory {
// IMPORTANT: perquè el registrador es cridi, el `.cpp` que el conté ha // IMPORTANT: perquè el registrador es cridi, el `.cpp` que el conté ha
// d'estar enllaçat directament a l'executable (no dins una STATIC lib // d'estar enllaçat directament a l'executable (no dins una STATIC lib
// sense `--whole-archive`). Tots els `concrete/*.cpp` entren a APP_SOURCES. // sense `--whole-archive`). Tots els `concrete/*.cpp` entren a APP_SOURCES.
#define REGISTER_ENTITY_AS(NAME, CLASS, TAG) \ #define REGISTER_ENTITY_AS(NAME, CLASS, TAG) \
namespace { \ namespace { \
struct TAG##_Registrar { \ struct TAG##_Registrar { \
TAG##_Registrar() { \ TAG##_Registrar() { \
EntityFactory::instance().registerCreator( \ EntityFactory::instance().registerCreator( \
NAME, \ NAME, \
[](const fkyaml::node& yaml) -> std::unique_ptr<Entity> { \ [](const fkyaml::node& yaml) -> std::unique_ptr<Entity> { \
return std::make_unique<CLASS>(yaml); \ return std::make_unique<CLASS>(yaml); \
}); \ }); \
} \ } \
}; \ }; \
const TAG##_Registrar g_##TAG##_registrar; /* NOLINT(cert-err58-cpp) */ \ /* NOLINTNEXTLINE(cert-err58-cpp) -- inicialització estàtica intencionada: el constructor registra el creator a l'EntityFactory abans de main(); el patró és l'únic mecanisme C++ que permet auto-registre per tipus sense modificar la factory. */ \
const TAG##_Registrar g_##TAG##_registrar; \
} }
#define REGISTER_ENTITY(NAME, CLASS) REGISTER_ENTITY_AS(NAME, CLASS, CLASS) #define REGISTER_ENTITY(NAME, CLASS) REGISTER_ENTITY_AS(NAME, CLASS, CLASS)
+5 -9
View File
@@ -1,11 +1,14 @@
#include "game/entities/entity_loader.hpp" #include "game/entities/entity_loader.hpp"
#include <algorithm>
#include <iterator>
#include <stdexcept> #include <stdexcept>
#include <unordered_map> #include <unordered_map>
#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 "game/map/room_yaml.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
namespace EntityLoader { namespace EntityLoader {
@@ -44,20 +47,13 @@ namespace EntityLoader {
} }
auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> { auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> {
auto bytes = Resource::Helper::loadFile(level_yaml_path); fkyaml::node root = RoomYaml::loadMerged(level_yaml_path);
if (bytes.empty()) {
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + level_yaml_path);
}
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
fkyaml::node root = fkyaml::node::deserialize(begin, end);
std::vector<fkyaml::node> spawns; std::vector<fkyaml::node> spawns;
if (!root.contains("entities")) { return spawns; } if (!root.contains("entities")) { return spawns; }
const auto& list = root["entities"]; const auto& list = root["entities"];
if (!list.is_sequence()) { return spawns; } if (!list.is_sequence()) { return spawns; }
spawns.reserve(list.size()); spawns.reserve(list.size());
// cppcheck-suppress useStlAlgorithm std::ranges::copy(list, std::back_inserter(spawns));
for (const auto& item : list) { spawns.push_back(item); }
return spawns; return spawns;
} }
+2 -2
View File
@@ -87,7 +87,7 @@ auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
entity->setEntityManager(this); entity->setEntityManager(this);
Entity* raw = entity.get(); Entity* raw = entity.get();
entities_.push_back(std::move(entity)); entities_.push_back(std::move(entity));
// cppcheck-suppress returnDanglingLifetime // cppcheck-suppress returnDanglingLifetime -- fals positiu: std::move transfereix ownership del unique_ptr al vector, però l'objecte apuntat per `raw` no es mou ni es destrueix. El punter continua vàlid mentre el vector el posseesca.
return raw; return raw;
} }
@@ -124,7 +124,7 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
Sarcophagus* raw = sarc.get(); Sarcophagus* raw = sarc.get();
entities_.push_back(std::move(sarc)); entities_.push_back(std::move(sarc));
++total_sarcs_; ++total_sarcs_;
// cppcheck-suppress returnDanglingLifetime // cppcheck-suppress returnDanglingLifetime -- fals positiu: std::move transfereix ownership al vector però l'objecte no es mou. `raw` continua vàlid (vegeu `add` més amunt).
return raw; return raw;
} }
@@ -11,7 +11,6 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/entities/concrete/door.hpp" #include "game/entities/concrete/door.hpp"
-1
View File
@@ -7,7 +7,6 @@
#include <functional> #include <functional>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <utility>
#include <variant> #include <variant>
#include <vector> #include <vector>
File diff suppressed because it is too large. Load diff
+190 -212
View File
@@ -10,7 +10,6 @@
#include "core/rendering/lighting.hpp" #include "core/rendering/lighting.hpp"
#include "core/rendering/sprite/animated_sprite.hpp" #include "core/rendering/sprite/animated_sprite.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "external/fkyaml_node.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
class EventBus; class EventBus;
@@ -55,6 +54,15 @@ class Player : public AnimatedSprite {
HOLE, // caiguda per agujero — animació `death_hole` HOLE, // caiguda per agujero — animació `death_hole`
}; };
// Direcció cardinal completa. Pública perquè el `WhipAttack` la passa a
// `enterHurt` quan colpeja un altre jugador (friendly fire). Declarada
// aquí (a sobre de `Config` perquè `TurnConfig` l'utilitza).
enum class Cardinal : std::uint8_t { NONE,
NORTH,
SOUTH,
EAST,
WEST };
enum class State : std::uint8_t { enum class State : std::uint8_t {
GROUNDED, GROUNDED,
AIRBORNE, AIRBORNE,
@@ -92,80 +100,172 @@ class Player : public AnimatedSprite {
DYING, DYING,
}; };
// Geometria carregada des del YAML del player (p.ex. `data/entities/player/sam.yaml`). // Configuració per-instància del Player. Conté tot el que abans vivia
// `sprite_*` són // com a `inline static` o `static *_config`: així cada jugador (P1=sam,
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els // P2=morcus) pot tenir paràmetres diferents llegits del seu YAML.
// 12 píxels superiors són el cap i els 12 inferiors la base). // GameScene la carrega amb `Config::loadFromYaml` abans d'instanciar
// La caixa de col·lisió és un quadrat `collision_size` concèntric amb el // cada Player i la passa al ctor (mou-la). Per refrescar-la entre
// quadrat inferior del sprite (`base_size`). Els offsets els calculem // sales s'utilitza `Player::setConfig`.
// al load a partir d'aquests tres valors; el mapa de col·lisions i el //
// de dibuix són independents (el sprite pot sobresortir sense afectar). // Els sub-structs (`InertiaConfig`, `TurnConfig`, ...) viuen dins de
inline static int sprite_w = 0; // `Config` per agrupar-los visualment amb la resta del config — no hi
inline static int sprite_h = 0; // ha cap raó tècnica per haver-los a fora.
inline static int base_size = 0; struct Config {
inline static int collision_size = 0; // ---- Geometria del sprite i col·lisió --------------------------
inline static int collision_padding = 0; // `sprite_*` són la mida del frame (sprite 12×24 amb perspectiva
inline static int collision_x_offset = 0; // falsa). La caixa de col·lisió és un quadrat `collision_size`
inline static int collision_y_offset = 0; // concèntric amb el quadrat inferior del sprite (`base_size`).
// Els offsets es calculen al load.
int sprite_w{0};
int sprite_h{0};
int base_size{0};
int collision_size{0};
int collision_padding{0};
int collision_x_offset{0};
int collision_y_offset{0};
// Caixa de "contacte" més petita que la caixa de col·lisió: s'usa per // Caixa de contacte actor-actor (més petita que la col·lisió).
// a les interaccions actor-actor (player↔player, player↔momia/trampa), int hit_size{0};
// on volem que el contacte sigui precís i no dispari a "distància" int hit_x_offset{0};
// de la vora del sprite. La caixa de col·lisió gran continua usant-se int hit_y_offset{0};
// per xocar amb murs i sarcòfags (per mantenir el carril del
// passadís). Sense `hit_size` al YAML, s'iguala a `collision_size` i
// el comportament és idèntic a l'anterior.
inline static int hit_size = 0;
inline static int hit_x_offset = 0;
inline static int hit_y_offset = 0;
// Alçada del cos del jugador en píxels (camp `collision.height` del // Alçada del cos en píxels (filtre de col·lisió per altura).
// YAML, mateixa convenció que entitats com mummy o rolling_rock). int body_height{0};
// L'usen els filtres de col·lisió per altura: si un altre actor ens
// creua mentre `jump_z_` és superior a aquest valor, no col·lisionem.
inline static int body_height = 0;
// Radi de la llum emesa pel jugador (en píxels del canvas), llegit // ---- Emissió de llum -----------------------------------------
// del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }` // Llegit del bloc opcional `light:` del YAML.
// del YAML. 0 = no emet. Es declara estàtic com la resta de configs // `radius` → 0 = no emet.
// del jugador (immunity, jump, ...): tots els jugadors del mateix YAML // `flicker` → activa modulació tipus foc.
// comparteixen el valor. `light_flicker` activa la modulació "tipus // `color` → tint en mode additive (ignorat en subtractive).
// foc" del radi; `light_color_*` són el tint quan la sala usa mode // `offset` → desplaçament del centre respecte el sprite (Y avall).
// additive (en subtractive s'ignora); `light_offset_*` desplaça el int light_radius{0};
// centre del cercle respecte el centre del sprite (Y creix avall: bool light_flicker{false};
// `y: -3` puja la llum 3 px). std::uint8_t light_color_r{255};
inline static int light_radius = 0; std::uint8_t light_color_g{255};
inline static bool light_flicker = false; std::uint8_t light_color_b{255};
inline static std::uint8_t light_color_r = 255; int light_offset_x{0};
inline static std::uint8_t light_color_g = 255; int light_offset_y{0};
inline static std::uint8_t light_color_b = 255; // Paràmetres del flicker resolts (global + override del YAML +
inline static int light_offset_x = 0; // override de sala). Només té efecte si `light_flicker` és true.
inline static int light_offset_y = 0; Lighting::FlickerConfig flicker_params{};
// Paràmetres del flicker resolts per al jugador (combinació del
// global + override del YAML del personatge + override de sala).
// Només té efecte si `light_flicker` és true.
inline static Lighting::FlickerConfig flicker_params{};
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. // ---- Sub-structs de gameplay ---------------------------------
static void loadFrom(const std::string& yaml_path);
// Vides amb què arrenca un jugador (camp `lives:` del YAML del // Inèrcia: amb `brake_time_ms == 0` el comportament és instantani.
// jugador). El consumeix el HUD per inicialitzar el comptador del struct InertiaConfig {
// marcador i `Player::resetForJoin` per a re-entrades mid-partida. int brake_time_ms{0};
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; } int threshold_ms{0};
};
// Gir de 180°: amb `duration_ms == 0` els canvis de direcció són
// instantanis. `Cardinal` ha d'estar visible — declarada a sobre.
struct TurnConfig {
int duration_ms{0};
Cardinal w_to_e{Cardinal::SOUTH};
Cardinal e_to_w{Cardinal::NORTH};
Cardinal n_to_s{Cardinal::EAST};
Cardinal s_to_n{Cardinal::WEST};
};
// Salt: amb `impulse_px_per_s == 0` el botó no té efecte.
struct JumpConfig {
float impulse_px_per_s{0.0F};
float gravity_px_per_s2{0.0F};
int landing_stun_ms{0};
std::string sound_key;
std::string land_sound_key;
};
// HURT (estat post-impacte). Amb `initial_speed_px_per_s == 0`
// entrada/sortida instantànies (sense moviment).
struct HurtConfig {
float initial_speed_px_per_s{0.0F};
int brake_time_ms{0};
std::string sound_key;
};
// Látigo: hitbox `range_tiles`, swing `swing_ms`, cooldown
// `cooldown_ms`. Per ara `damage` només es llegeix.
struct WhipConfig {
float range_tiles{0.0F};
int swing_ms{0};
int cooldown_ms{0};
int damage{0};
std::string sound_key;
};
// Mort/respawn: `respawn_delay_ms` entre l'animació i la
// reaparició; `initial_lives` és el comptador d'intents.
struct DeathConfig {
int respawn_delay_ms{0};
int initial_lives{3};
};
// Immunitat post-spawn. `duration_ms == 0` desactiva la mecànica.
// `palette_key` (basename del .pal) és la paleta plana del blink.
struct ImmunityConfig {
float duration_ms{0.0F};
int blink_period_ms{0};
std::string palette_key;
};
InertiaConfig inertia{};
TurnConfig turn{};
JumpConfig jump{};
HurtConfig hurt{};
WhipConfig whip{};
DeathConfig death{};
ImmunityConfig immunity{};
// Llig tot el YAML del personatge i retorna un `Config` poblat.
// Substitueix l'antic `Player::loadFrom` que mutava estàtics.
// Llança si manca el bloc `collision:` (obligatori).
[[nodiscard]] static auto loadFromYaml(const std::string& yaml_path) -> Config;
// Aplica l'override de sala (bloc `lighting.emitters.player` del
// YAML del mapa) sobre els camps de llum d'aquesta Config. Es
// crida després de `loadFromYaml` i abans de passar la Config al
// ctor de Player (o a `setConfig` en transicions de sala).
void applyRoomLightOverride();
};
Player(Config cfg, const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
// Reemplaça el `Config` actual del Player. Cridat per GameScene al
// canvi de sala per re-aplicar valors base + override de la sala nova.
// No toca cap estat de joc (lives, posició, FSM): només la config.
void setConfig(Config cfg);
// Accés a la config viva (lectura). El callsites que abans accedien
// a estàtics fan servir `getters d'instància` (sota); aquest és per
// codi que vol l'objecte sencer.
[[nodiscard]] auto getConfig() const -> const Config& { return cfg_; }
// Vides amb què arrenca un jugador (camp `lives:` del YAML). El
// consumeix el HUD per inicialitzar el marcador i `resetForJoin`
// per a re-entrades mid-partida.
[[nodiscard]] auto getInitialLives() const -> int { return cfg_.death.initial_lives; }
// Geometria del cos — instance getters dels camps de `cfg_` que
// alguns callsites externs (game_scene) consulten directament.
[[nodiscard]] auto getBodyHeight() const -> int { return cfg_.body_height; }
[[nodiscard]] auto getSpriteW() const -> int { return cfg_.sprite_w; }
[[nodiscard]] auto getSpriteH() const -> int { return cfg_.sprite_h; }
[[nodiscard]] auto getCollisionSize() const -> int { return cfg_.collision_size; }
[[nodiscard]] auto getCollisionYOffset() const -> int { return cfg_.collision_y_offset; }
// Emissió de llum (mateix paper que `Entity::getLightRadius` per a // Emissió de llum (mateix paper que `Entity::getLightRadius` per a
// les entitats). El centre puja amb el salt: durant `jump_z_>0` el // les entitats). El centre puja amb el salt: durant `jump_z_>0` el
// sprite es dibuixa amb `pos_.y - jump_z_`, i la llum ha de seguir-lo // sprite es dibuixa amb `pos_.y - jump_z_`, i la llum ha de seguir-lo
// per a que el cercle "salti" amb el jugador, no es quedi al sòl. // per a que el cercle "salti" amb el jugador, no es quedi al sòl.
[[nodiscard]] static auto getLightRadius() -> int { return light_radius; } [[nodiscard]] auto getLightRadius() const -> int { return cfg_.light_radius; }
[[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; } [[nodiscard]] auto getLightFlicker() const -> bool { return cfg_.light_flicker; }
[[nodiscard]] static auto getLightColorR() -> std::uint8_t { return light_color_r; } [[nodiscard]] auto getLightColorR() const -> std::uint8_t { return cfg_.light_color_r; }
[[nodiscard]] static auto getLightColorG() -> std::uint8_t { return light_color_g; } [[nodiscard]] auto getLightColorG() const -> std::uint8_t { return cfg_.light_color_g; }
[[nodiscard]] static auto getLightColorB() -> std::uint8_t { return light_color_b; } [[nodiscard]] auto getLightColorB() const -> std::uint8_t { return cfg_.light_color_b; }
[[nodiscard]] static auto getLightOffsetX() -> int { return light_offset_x; } [[nodiscard]] auto getLightOffsetX() const -> int { return cfg_.light_offset_x; }
[[nodiscard]] static auto getLightOffsetY() -> int { return light_offset_y; } [[nodiscard]] auto getLightOffsetY() const -> int { return cfg_.light_offset_y; }
[[nodiscard]] static auto getFlickerParams() -> const Lighting::FlickerConfig& { return flicker_params; } [[nodiscard]] auto getFlickerParams() const -> const Lighting::FlickerConfig& { return cfg_.flicker_params; }
// Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador // Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador
// local) amb un offset gran per evitar col·lisions amb el contador // local) amb un offset gran per evitar col·lisions amb el contador
// d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases // d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases
@@ -173,8 +273,6 @@ class Player : public AnimatedSprite {
[[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; } [[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; }
[[nodiscard]] auto getLightCenter() const -> SDL_FPoint; [[nodiscard]] auto getLightCenter() const -> SDL_FPoint;
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
// escenaris amb un sol jugador. Internament és equivalent a cridar // escenaris amb un sol jugador. Internament és equivalent a cridar
// `tick(dt)` seguit d'`applyMovement(dt)`. // `tick(dt)` seguit d'`applyMovement(dt)`.
@@ -275,7 +373,7 @@ class Player : public AnimatedSprite {
// que coincideix amb aquest mètode i s'usa específicament per // que coincideix amb aquest mètode i s'usa específicament per
// marcar la posició de la sombra a nivell de sòl. // marcar la posició de la sombra a nivell de sòl.
[[nodiscard]] auto getFeetY() const -> float { [[nodiscard]] auto getFeetY() const -> float {
return getPosY() + static_cast<float>(collision_y_offset + collision_size); return getPosY() + static_cast<float>(cfg_.collision_y_offset + cfg_.collision_size);
} }
// Alies semàntic: els peus del jugador al sòl. Coincideix amb // Alies semàntic: els peus del jugador al sòl. Coincideix amb
@@ -484,7 +582,7 @@ class Player : public AnimatedSprite {
// arbitrària (p. ex. la posició proposada per `tryMove` abans // arbitrària (p. ex. la posició proposada per `tryMove` abans
// d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora // d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora
// de Player. // de Player.
[[nodiscard]] static auto hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect; [[nodiscard]] auto hitBoxFromBase(const SDL_FRect& base) const -> SDL_FRect;
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta // Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
// caixa al callback de col·lisió. La base del Player és més petita // caixa al callback de col·lisió. La base del Player és més petita
@@ -505,6 +603,15 @@ class Player : public AnimatedSprite {
void renderShadow() const; void renderShadow() const;
private: private:
// Sub-handlers de tick(), un per estat amb sortida prematura. Mantenir-los
// a part redueix la complexitat cognitiva del dispatcher principal.
void tickDying();
void tickLandingStun(float delta_time);
void tickAirborne();
void tickAttacking(float delta_time);
void tickHurt(float delta_time);
void tickGroundedInput(float delta_time);
// Alias privat històric. Mantingut perquè viu dins de l'implementació // Alias privat històric. Mantingut perquè viu dins de l'implementació
// (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`. // (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`.
[[nodiscard]] auto baseBox() const -> SDL_FRect { return getBaseBox(); } [[nodiscard]] auto baseBox() const -> SDL_FRect { return getBaseBox(); }
@@ -568,6 +675,11 @@ class Player : public AnimatedSprite {
// efecte immediat sense haver de tornar a entrar a la GameScene. // efecte immediat sense haver de tornar a entrar a la GameScene.
void refreshControlsFromOptions(); void refreshControlsFromOptions();
// Tot el config del personatge (geometria, llum, jump, hurt, whip,
// death, immunity, turn, inertia). Llegit del YAML per instància
// (P1 i P2 poden tenir valors diferents) i mogut al ctor.
Config cfg_;
int id_{-1}; int id_{-1};
Controls controls_; Controls controls_;
@@ -639,26 +751,6 @@ class Player : public AnimatedSprite {
static auto facingSuffix(Facing facing) -> const char*; static auto facingSuffix(Facing facing) -> const char*;
static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*; static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*;
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
// un altre jugador (friendly fire).
public:
enum class Cardinal : std::uint8_t { NONE,
NORTH,
SOUTH,
EAST,
WEST };
private:
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
// el comportament és instantani: soltar input para el jugador en sec.
struct InertiaConfig {
int brake_time_ms{0}; // Temps en ms perquè la velocitat caiga de ±SPEED a 0.
int threshold_ms{0}; // Temps mínim en moviment continu per activar la inèrcia.
};
static InertiaConfig inertia_config;
// Integra un eix. Si hi ha input en aquest eix, la velocitat física // Integra un eix. Si hi ha input en aquest eix, la velocitat física
// salta a aquest valor (acceleració instantània). Si no, i hi ha // salta a aquest valor (acceleració instantània). Si no, i hi ha
// input en l'altre eix (`any_input == true`), la velocitat d'aquest // input en l'altre eix (`any_input == true`), la velocitat d'aquest
@@ -666,127 +758,7 @@ class Player : public AnimatedSprite {
// soltant l'input — no volem que arrossegue en diagonal. Només quan // soltant l'input — no volem que arrossegue en diagonal. Només quan
// l'usuari deixa anar tot l'input (`any_input == false`) s'activa la // l'usuari deixa anar tot l'input (`any_input == false`) s'activa la
// inèrcia de frenada (si `motion_time_ms` ha passat el llindar). // inèrcia de frenada (si `motion_time_ms` ha passat el llindar).
static void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input); void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) const;
// Configuració del gir de 180°, llegida del bloc `turn:` del YAML del
// player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica
// queda desactivada i els canvis de direcció són instantanis.
struct TurnConfig {
int duration_ms{0};
Cardinal w_to_e{Cardinal::SOUTH};
Cardinal e_to_w{Cardinal::NORTH};
Cardinal n_to_s{Cardinal::EAST};
Cardinal s_to_n{Cardinal::WEST};
};
// Definida out-of-class al .cpp: dins de la classe, els initializers
// per defecte de TurnConfig encara no són visibles per al compilador.
static TurnConfig turn_config;
// Configuració del salt llegida del bloc `jump:` del YAML del player.
// Si `impulse_px_per_s == 0` (o el bloc no existeix) la mecànica del
// salt queda desactivada — el botó no té efecte.
struct JumpConfig {
float impulse_px_per_s{0.0F}; // Velocitat vertical inicial.
float gravity_px_per_s2{0.0F}; // Acceleració descendent constant.
int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta).
std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci).
std::string land_sound_key; // Clau del so d'aterratge (buit = silenci).
};
static JumpConfig jump_config;
// Llig el bloc `collision:` del YAML i n'omple les caixes (size/base/
// hit) + `body_height`. Llança si el bloc no existeix. Membre estàtic
// per accedir als estàtics privats sense exposar-los.
static void readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path);
// Llig el bloc `jump:` del YAML i n'omple `jump_config`. Si el bloc
// manca, deixa la mecànica del salt desactivada.
static void readJumpBlock(const fkyaml::node& yaml);
// Configuració de l'estat HURT (estat post-impacte). Si
// `initial_speed_px_per_s == 0` el jugador entra i surt
// instantàniament (cap moviment).
struct HurtConfig {
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
// 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
// de clau al Resource::Cache i de path. Buit o absent = entrada
// silenciosa.
std::string sound_key;
};
static HurtConfig hurt_config;
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
static void readHurtBlock(const fkyaml::node& yaml);
// Configuració del látigo del jugador llegida del bloc `whip:` del
// YAML. `range_tiles` defineix la longitud de la hitbox abans de
// retallar; `swing_ms` és la durada de l'animació d'atac (des
// d'`enterAttacking` fins a desbloquejar el cooldown); `cooldown_ms`
// afegeix una fase posterior durant la qual el jugador segueix
// bloquejat però la hitbox ja no fa dany. `damage` queda llegit per
// a futur; per ara els objectius reben sempre `enterHurt`.
struct WhipConfig {
float range_tiles{0.0F};
int swing_ms{0};
int cooldown_ms{0};
int damage{0};
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
// `data/audio/sounds/`). Convenció: el basename val tant de clau al
// Resource::Cache com de path. Buit o absent = swing silenciós.
std::string sound_key;
};
static WhipConfig whip_config;
// Llig el bloc `whip:` del YAML i n'omple `whip_config`. Si manca el
// bloc, els camps queden a 0 i el jugador no pot atacar (el flanc
// d'input es perd silenciosament).
static void readWhipBlock(const fkyaml::node& yaml);
// Configuració de mort/respawn llegida del bloc `death:` del YAML.
// `respawn_delay_ms` és la pausa entre la fi de `death_spinning` i la
// reaparició al spawn; `initial_lives` és el comptador d'intents amb
// què naix cada Player. Si el bloc manca, defaults segurs (0/3).
struct DeathConfig {
int respawn_delay_ms{0};
int initial_lives{3};
};
static DeathConfig death_config;
// Llig el bloc `death:` del YAML i n'omple `death_config`. Si manca,
// `respawn_delay_ms` queda a 0 (el respawn és immediat al final de
// l'animació) i `initial_lives` es manté al default (3).
static void readDeathBlock(const fkyaml::node& yaml);
// Configuració de la immunitat post-spawn llegida del bloc `immunity:`
// del YAML. `duration_ms` és el temps per defecte que dura cada
// immunitat (l'invoca `respawn`). `blink_period_ms` és el mig-cicle del
// parpadeig visual: cada `blink_period_ms` ms, el render alterna
// entre la paleta normal i la paleta `palette_key`. Si `duration_ms`
// és 0 (o el bloc manca) la immunitat queda desactivada i el
// comportament és l'antic.
struct ImmunityConfig {
float duration_ms{0.0F};
int blink_period_ms{0};
// Clau al Resource::Cache de la paleta plana usada al blink (basename
// 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
// pot ser confusa, però no és incorrecte).
std::string palette_key;
};
static ImmunityConfig immunity_config;
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
// hi ha `palette`, registra la paleta corresponent al Resource::Cache
// (path: `data/entities/player/<palette_key>.pal`). Mateix patró que els altres
// readers — `loadFrom` el crida al final.
static void readImmunityBlock(const fkyaml::node& yaml);
// Llig el bloc opcional `light: { radius: N, flicker: bool }` del
// YAML i pobla `light_radius` / `light_flicker`. Si manca, els
// valors queden a 0 / false (jugador sense llum).
static void readLightBlock(const fkyaml::node& yaml);
// Activa l'estat ATTACKING en la direcció cardinal indicada (la // Activa l'estat ATTACKING en la direcció cardinal indicada (la
// direcció cap on mira el jugador) i publica `whip_spawn` al bus // direcció cap on mira el jugador) i publica `whip_spawn` al bus
@@ -992,10 +964,16 @@ class Player : public AnimatedSprite {
// Extret de `update` per acotar la complexitat cognitiva (clang-tidy). // Extret de `update` per acotar la complexitat cognitiva (clang-tidy).
void updateFacingAndTurn(float delta_time, float vx, float vy); void updateFacingAndTurn(float delta_time, float vx, float vy);
static auto cardinalFromInput(float vx, float vy) -> Cardinal; static auto cardinalFromInput(float vx, float vy) -> Cardinal;
static auto cardinalFromString(const std::string& s) -> Cardinal;
static auto opposite(Cardinal c) -> Cardinal; static auto opposite(Cardinal c) -> Cardinal;
static auto intermediateFor(Cardinal target) -> Cardinal; auto intermediateFor(Cardinal target) const -> Cardinal;
public:
// Helper pur: tradueix un string YAML ("north", "south", ...) al
// Cardinal corresponent. Públic perquè `Config::loadFromYaml` el crida
// des d'un helper de la TU anònima a player.cpp.
static auto cardinalFromString(const std::string& s) -> Cardinal;
private:
// Bus del nivell (opcional). Propietat externa (EntityManager); el // Bus del nivell (opcional). Propietat externa (EntityManager); el
// Player només en guarda un punter per publicar events. // Player només en guarda un punter per publicar events.
EventBus* bus_{nullptr}; EventBus* bus_{nullptr};
+2 -8
View File
@@ -5,8 +5,8 @@
#include <stdexcept> #include <stdexcept>
#include <unordered_map> #include <unordered_map>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/map/room_yaml.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/log.hpp" #include "utils/log.hpp"
@@ -157,13 +157,7 @@ namespace Map {
} }
void loadFrom(const std::string& yaml_path) { void loadFrom(const std::string& yaml_path) {
auto bytes = Resource::Helper::loadFile(yaml_path); auto yaml = RoomYaml::loadMerged(yaml_path);
if (bytes.empty()) {
throw std::runtime_error("Map: no s'ha pogut obrir " + yaml_path);
}
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (!yaml.contains("geometry")) { if (!yaml.contains("geometry")) {
throw std::runtime_error("Map: manca bloc `geometry:` a " + yaml_path); throw std::runtime_error("Map: manca bloc `geometry:` a " + yaml_path);
} }
+66
View File
@@ -0,0 +1,66 @@
#include "game/map/room_yaml.hpp"
#include <stdexcept>
#include "core/resources/resource_helper.hpp"
namespace RoomYaml {
namespace {
// Fusiona `overlay` damunt `base` in-place. Recursió només per a
// mapes; per a la resta (escalars, sequences) la `overlay` substitueix
// l'entrada de `base`. Manté l'ordre de claus de `base` per a les
// que ja hi eren; les noves s'afegeixen al final.
void deepMerge(fkyaml::node& base, const fkyaml::node& overlay) {
if (!overlay.is_mapping()) { return; }
for (const auto& kv : overlay.as_map()) {
const auto KEY = kv.first.get_value<std::string>();
const auto& val = kv.second;
if (base.contains(KEY) && base[KEY].is_mapping() && val.is_mapping()) {
deepMerge(base[KEY], val);
} else {
base[KEY] = val;
}
}
}
auto deserializeFile(const std::string& path) -> fkyaml::node {
auto bytes = Resource::Helper::loadFile(path);
if (bytes.empty()) {
throw std::runtime_error("RoomYaml: no s'ha pogut obrir " + path);
}
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
return fkyaml::node::deserialize(begin, end);
}
// Retorna el directori del path (sense barra final). Si no hi ha
// separador, retorna cadena buida.
auto dirOf(const std::string& path) -> std::string {
const auto POS = path.find_last_of('/');
if (POS == std::string::npos) { return ""; }
return path.substr(0, POS);
}
} // namespace
auto loadMerged(const std::string& room_yaml_path) -> fkyaml::node {
const auto DIR = dirOf(room_yaml_path);
const auto PYRAMID_PATH = DIR.empty() ? std::string{"pyramid.yaml"} : (DIR + "/pyramid.yaml");
// El pyramid.yaml és opcional: si Resource::Helper::loadFile el
// troba (disc o pack), el fem servir com a base; si no, només
// retornem el YAML de sala tal qual.
auto pyramid_bytes = Resource::Helper::loadFile(PYRAMID_PATH);
auto room = deserializeFile(room_yaml_path);
if (pyramid_bytes.empty()) { return room; }
const auto* begin = reinterpret_cast<const char*>(pyramid_bytes.data());
const auto* end = begin + pyramid_bytes.size();
auto merged = fkyaml::node::deserialize(begin, end);
deepMerge(merged, room);
return merged;
}
} // namespace RoomYaml
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <string>
#include "external/fkyaml_node.hpp"
// Carregador de YAMLs de sala amb fusió per piràmide. Cada piràmide pot
// tenir un `pyramid.yaml` germà al directori de sala que aporta camps
// compartits (tileset, lighting, push_factor, paint_tiles...); el YAML
// de sala només declara el que canvia (data, entities, geometry, room).
//
// Regles de fusió:
// - mapes: fusió recursiva, claus de la sala guanyen
// - sequences i escalars: la sala substitueix sencer
//
// Si no hi ha `pyramid.yaml` al directori, retorna només el YAML de sala
// sense modificacions — així rutes externes (tests, mapes solts) segueixen
// funcionant igual.
namespace RoomYaml {
auto loadMerged(const std::string& room_yaml_path) -> fkyaml::node;
} // namespace RoomYaml
+2 -8
View File
@@ -9,8 +9,8 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/map/room_yaml.hpp"
#include "utils/log.hpp" #include "utils/log.hpp"
#ifdef _DEBUG #ifdef _DEBUG
@@ -41,13 +41,7 @@ namespace {
} // namespace } // namespace
Tilemap::Tilemap(const std::string& yaml_path) { Tilemap::Tilemap(const std::string& yaml_path) {
auto bytes = Resource::Helper::loadFile(yaml_path); auto yaml = RoomYaml::loadMerged(yaml_path);
if (bytes.empty()) {
throw std::runtime_error("Tilemap: no s'ha pogut obrir " + yaml_path);
}
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
tileset_path_ = yaml["tileset"].get_value<std::string>(); tileset_path_ = yaml["tileset"].get_value<std::string>();
tile_size_ = yaml["tile_size"].get_value<int>(); tile_size_ = yaml["tile_size"].get_value<int>();
+20 -9
View File
@@ -248,12 +248,15 @@ namespace {
} }
// Presets per defecte de PostFX, portats directament de projecte_2026. // Presets per defecte de PostFX, portats directament de projecte_2026.
// chroma_min == chroma_max → chroma estàtic (per defecte). Ajusta'ls al gust
// (per exemple, augmentar el rang en CRT Live perquè "pulsi" visiblement).
auto defaultPostFXPresets() -> std::vector<Options::PostFXPreset> { auto defaultPostFXPresets() -> std::vector<Options::PostFXPreset> {
return { return {
{.name = "CRT", {.name = "CRT",
.vignette = 0.6F, .vignette = 0.6F,
.scanlines = 0.7F, .scanlines = 0.7F,
.chroma = 0.15F, .chroma_min = 0.15F,
.chroma_max = 0.15F,
.mask = 0.6F, .mask = 0.6F,
.gamma = 0.8F, .gamma = 0.8F,
.curvature = 0.0F, .curvature = 0.0F,
@@ -262,7 +265,8 @@ namespace {
{.name = "NTSC", {.name = "NTSC",
.vignette = 0.4F, .vignette = 0.4F,
.scanlines = 0.5F, .scanlines = 0.5F,
.chroma = 0.2F, .chroma_min = 0.2F,
.chroma_max = 0.2F,
.mask = 0.4F, .mask = 0.4F,
.gamma = 0.5F, .gamma = 0.5F,
.curvature = 0.0F, .curvature = 0.0F,
@@ -271,7 +275,8 @@ namespace {
{.name = "Curved", {.name = "Curved",
.vignette = 0.5F, .vignette = 0.5F,
.scanlines = 0.6F, .scanlines = 0.6F,
.chroma = 0.1F, .chroma_min = 0.1F,
.chroma_max = 0.1F,
.mask = 0.5F, .mask = 0.5F,
.gamma = 0.7F, .gamma = 0.7F,
.curvature = 0.8F, .curvature = 0.8F,
@@ -280,7 +285,8 @@ namespace {
{.name = "Scanlines", {.name = "Scanlines",
.vignette = 0.0F, .vignette = 0.0F,
.scanlines = 0.8F, .scanlines = 0.8F,
.chroma = 0.0F, .chroma_min = 0.0F,
.chroma_max = 0.0F,
.mask = 0.0F, .mask = 0.0F,
.gamma = 0.0F, .gamma = 0.0F,
.curvature = 0.0F, .curvature = 0.0F,
@@ -289,7 +295,8 @@ namespace {
{.name = "Subtle", {.name = "Subtle",
.vignette = 0.3F, .vignette = 0.3F,
.scanlines = 0.4F, .scanlines = 0.4F,
.chroma = 0.05F, .chroma_min = 0.05F,
.chroma_max = 0.05F,
.mask = 0.0F, .mask = 0.0F,
.gamma = 0.3F, .gamma = 0.3F,
.curvature = 0.0F, .curvature = 0.0F,
@@ -298,7 +305,8 @@ namespace {
{.name = "CRT Live", {.name = "CRT Live",
.vignette = 0.5F, .vignette = 0.5F,
.scanlines = 0.6F, .scanlines = 0.6F,
.chroma = 0.3F, .chroma_min = 0.3F,
.chroma_max = 0.3F,
.mask = 0.3F, .mask = 0.3F,
.gamma = 0.4F, .gamma = 0.4F,
.curvature = 0.3F, .curvature = 0.3F,
@@ -311,7 +319,8 @@ namespace {
readField(node, "name", preset.name); readField(node, "name", preset.name);
readField(node, "vignette", preset.vignette); readField(node, "vignette", preset.vignette);
readField(node, "scanlines", preset.scanlines); readField(node, "scanlines", preset.scanlines);
readField(node, "chroma", preset.chroma); readField(node, "chroma_min", preset.chroma_min);
readField(node, "chroma_max", preset.chroma_max);
readField(node, "mask", preset.mask); readField(node, "mask", preset.mask);
readField(node, "gamma", preset.gamma); readField(node, "gamma", preset.gamma);
readField(node, "curvature", preset.curvature); readField(node, "curvature", preset.curvature);
@@ -326,7 +335,8 @@ namespace {
out << " - name: \"" << preset.name << "\"\n"; out << " - name: \"" << preset.name << "\"\n";
out << " vignette: " << preset.vignette << "\n"; out << " vignette: " << preset.vignette << "\n";
out << " scanlines: " << preset.scanlines << "\n"; out << " scanlines: " << preset.scanlines << "\n";
out << " chroma: " << preset.chroma << "\n"; out << " chroma_min: " << preset.chroma_min << "\n";
out << " chroma_max: " << preset.chroma_max << "\n";
out << " mask: " << preset.mask << "\n"; out << " mask: " << preset.mask << "\n";
out << " gamma: " << preset.gamma << "\n"; out << " gamma: " << preset.gamma << "\n";
out << " curvature: " << preset.curvature << "\n"; out << " curvature: " << preset.curvature << "\n";
@@ -876,7 +886,8 @@ namespace Options {
return { return {
.vignette = IT->vignette, .vignette = IT->vignette,
.scanlines = IT->scanlines, .scanlines = IT->scanlines,
.chroma = IT->chroma, .chroma_min = IT->chroma_min,
.chroma_max = IT->chroma_max,
.mask = IT->mask, .mask = IT->mask,
.gamma = IT->gamma, .gamma = IT->gamma,
.curvature = IT->curvature, .curvature = IT->curvature,
+3 -1
View File
@@ -59,7 +59,9 @@ namespace Options {
std::string name; std::string name;
float vignette{0.0F}; float vignette{0.0F};
float scanlines{0.0F}; float scanlines{0.0F};
float chroma{0.0F}; // Aberració cromàtica entre min i max (si coincideixen, estàtica).
float chroma_min{0.0F};
float chroma_max{0.0F};
float mask{0.0F}; float mask{0.0F};
float gamma{0.0F}; float gamma{0.0F};
float curvature{0.0F}; float curvature{0.0F};
+64 -91
View File
@@ -14,7 +14,6 @@
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/input/global_inputs.hpp" #include "core/input/global_inputs.hpp"
#include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/lighting.hpp" #include "core/rendering/lighting.hpp"
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
@@ -22,8 +21,6 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp" #include "core/ui/ui_config.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/entities/concrete/dust.hpp" #include "game/entities/concrete/dust.hpp"
@@ -33,11 +30,16 @@
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_loader.hpp" #include "game/entities/entity_loader.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
#include "game/entities/hazard.hpp"
#include "game/entities/player.hpp" #include "game/entities/player.hpp"
#ifdef _DEBUG
#include "core/input/key_config.hpp"
#include "core/system/event_buffer.hpp"
#endif
#include "game/gameplay_config.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/map/room_yaml.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/scene_flow.hpp" #include "game/scenes/scene_flow.hpp"
#include "game/scenes/start_input.hpp" #include "game/scenes/start_input.hpp"
@@ -65,21 +67,13 @@ namespace {
// pinta amb l'índex declarat al mapa, i un mapa sense aquests camps // pinta amb l'índex declarat al mapa, i un mapa sense aquests camps
// simplement no deixa rastre visual. // simplement no deixa rastre visual.
auto readOptionalInt(const std::string& yaml_path, const std::string& key, int fallback = -1) -> int { auto readOptionalInt(const std::string& yaml_path, const std::string& key, int fallback = -1) -> int {
auto bytes = Resource::Helper::loadFile(yaml_path); auto yaml = RoomYaml::loadMerged(yaml_path);
if (bytes.empty()) { return fallback; }
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(key)) { return fallback; } if (!yaml.contains(key)) { return fallback; }
return yaml[key].get_value<int>(); return yaml[key].get_value<int>();
} }
auto readOptionalFloat(const std::string& yaml_path, const std::string& key, float fallback) -> float { auto readOptionalFloat(const std::string& yaml_path, const std::string& key, float fallback) -> float {
auto bytes = Resource::Helper::loadFile(yaml_path); auto yaml = RoomYaml::loadMerged(yaml_path);
if (bytes.empty()) { return fallback; }
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(key)) { return fallback; } if (!yaml.contains(key)) { return fallback; }
return yaml[key].get_value<float>(); return yaml[key].get_value<float>();
} }
@@ -171,9 +165,10 @@ namespace {
// velocitats horitzontals i els deixem rebotar fins a aterrar // velocitats horitzontals i els deixem rebotar fins a aterrar
// — els dos cauen en stun, com a càstig per abusar del salt. // — els dos cauen en stun, com a càstig per abusar del salt.
if (A_AIR) { if (A_AIR) {
// cppcheck-suppress knownConditionTrueFalse // Usem el body_height d'A (els dos haurien de ser idèntics en
const auto BODY_H = static_cast<float>(Player::body_height); // multijugador local actual; si en algun moment difereixen entre
// cppcheck-suppress knownConditionTrueFalse // P1 i P2, prenem el del primer participant per consistència).
const auto BODY_H = static_cast<float>(a->getBodyHeight());
if (BODY_H <= 0.0F) { return; } if (BODY_H <= 0.0F) { return; }
if (std::fabs(a->getJumpZ() - b->getJumpZ()) >= BODY_H) { return; } if (std::fabs(a->getJumpZ() - b->getJumpZ()) >= BODY_H) { return; }
// Saltadors estàtics horitzontalment (cas degenerat: tots dos // Saltadors estàtics horitzontalment (cas degenerat: tots dos
@@ -232,37 +227,14 @@ namespace {
return out; return out;
} }
// Override de sala per a la llum del jugador. Si el YAML de la sala // Carrega la config d'un jugador des del seu YAML i aplica l'override
// declara `lighting.emitters.players`, substitueix els valors carregats // de sala (si el bloc `lighting.emitters.player` el reescriu). Helper
// per `Player::loadFrom`. Extret aquí per no inflar la complexitat // compartit entre ctor i `loadRoom` per fer la mateixa cosa als dos
// cognitiva del ctor de GameScene (clang-tidy: cognitive-complexity). // moments: arrencada de partida i transició de sala.
// No s'aplica una "tercera capa" per-instància al jugador perquè no auto loadPlayerConfigWithRoomOverride(const std::string& yaml_path) -> Player::Config {
// neix com a spawn del nivell; el dia d'un ítem "linterna" aquell auto cfg = Player::Config::loadFromYaml(yaml_path);
// sistema mutarà els mateixos camps en runtime — ortogonal a aquest cfg.applyRoomLightOverride();
// override. return cfg;
void applyRoomLightOverrideToPlayer() {
const auto* room = Map::getRoomEmitterOverride("player");
if (room == nullptr) { return; }
if (!room->enabled) {
Player::light_radius = 0;
Player::light_flicker = false;
return;
}
if (room->radius.has_value()) { Player::light_radius = *room->radius; }
if (room->flicker.has_value()) { Player::light_flicker = *room->flicker; }
if (room->color.has_value()) {
Player::light_color_r = (*room->color)[0];
Player::light_color_g = (*room->color)[1];
Player::light_color_b = (*room->color)[2];
}
if (room->offset.has_value()) {
Player::light_offset_x = (*room->offset)[0];
Player::light_offset_y = (*room->offset)[1];
}
if (room->flicker_wave_amplitude_ratio.has_value()) { Player::flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; }
if (room->flicker_wave_frequency_hz.has_value()) { Player::flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; }
if (room->flicker_noise_amplitude_ratio.has_value()) { Player::flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; }
if (room->flicker_noise_frequency_hz.has_value()) { Player::flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; }
} }
} // namespace } // namespace
@@ -315,14 +287,14 @@ GameScene::GameScene(Config config)
const std::string ANIM_P1 = load_player_assets(config_.player_sprite, config_.player_yaml); const std::string ANIM_P1 = load_player_assets(config_.player_sprite, config_.player_yaml);
const std::string ANIM_P2 = load_player_assets(config_.player2_sprite, config_.player2_yaml); const std::string ANIM_P2 = load_player_assets(config_.player2_sprite, config_.player2_yaml);
// Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada // Geometria de col·lisió i tota la resta de config del jugador
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar // (caixa 16×16, base 12, jump, hurt, llum, etc.) carregada per
// Player perquè el spawn consulta `collision_*`. Els membres de Player // instància des del YAML del personatge corresponent. Cada Player
// són `static inline`, compartits per tots els jugadors; carregar des // rep el seu `Config` al ctor (moure'l avall). L'override de sala
// del YAML de P1 és suficient (els dos personatges comparteixen // s'aplica aquí abans de moure'l perquè el jugador ja arrenqui
// geometria — morcus és un repaletat de sam). // amb la llum correcta per a la sala actual.
Player::loadFrom(config_.player_yaml); Player::Config cfg_p1 = loadPlayerConfigWithRoomOverride(config_.player_yaml);
applyRoomLightOverrideToPlayer(); Player::Config cfg_p2 = loadPlayerConfigWithRoomOverride(config_.player2_yaml);
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia, // Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa // etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
@@ -418,8 +390,9 @@ GameScene::GameScene(Config config)
// Spawn lligat a la porta: tots els jugadors apareixen davant del // Spawn lligat a la porta: tots els jugadors apareixen davant del
// passadís de sortida. `computeSpawn` deriva les coordenades de // passadís de sortida. `computeSpawn` deriva les coordenades de
// `Map::door_col` perquè canviar la columna de la porta al YAML // `Map::door_col` perquè canviar la columna de la porta al YAML
// moga automàticament l'spawn — no hi ha cap valor hardcoded. // moga automàticament l'spawn — no hi ha cap valor hardcoded. P1
const auto SPAWN = computeSpawn(); // dicta el centrament (consistent amb el comportament pre-refactor).
const auto SPAWN = computeSpawn(cfg_p1);
const bool BOTH_ACTIVE = (USE_P1 && USE_P2); const bool BOTH_ACTIVE = (USE_P1 && USE_P2);
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa // Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
@@ -445,8 +418,8 @@ GameScene::GameScene(Config config)
if (USE_P1) { p1_x = SPAWN.p1_x + TILE_F; } if (USE_P1) { p1_x = SPAWN.p1_x + TILE_F; }
if (USE_P2) { p2_x = SPAWN.p2_x - TILE_F; } if (USE_P2) { p2_x = SPAWN.p2_x - TILE_F; }
} }
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT)); players_.emplace_back(std::make_unique<Player>(std::move(cfg_p1), ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT)); players_.emplace_back(std::make_unique<Player>(std::move(cfg_p2), ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
// Tots els slots arrenquen DISABLED. Els actius els reactivarà el // Tots els slots arrenquen DISABLED. Els actius els reactivarà el
// callback de fi de música d'INTRO via `completeIntro()` (spawn aeri // callback de fi de música d'INTRO via `completeIntro()` (spawn aeri
@@ -499,10 +472,12 @@ 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);
// Vides inicials del HUD = `initial_lives` del YAML del jugador // Vides inicials del HUD = `initial_lives` del Config del P1.
// (`Player::loadFrom` ja ha omplert `death_config` abans d'aquí). // (Cada Player té el seu propi Config post-refactor a per-instance;
// Sense això, el marcador es queda fixe a 3 i el YAML perd efecte. // el HUD agafa el de P1 com a referència — historicament era el
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, Player::getInitialLives()); // valor compartit per tots els jugadors). Si en algun futur P1 i P2
// arrenquen amb vides diferents, caldria un HUD per slot.
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, players_[0]->getInitialLives());
// Activem el bloc HUD per als slots que arranquen en partida. Els // Activem el bloc HUD per als slots que arranquen en partida. Els
// inactius queden mostrant el prompt "POLSA START / PER JUGAR" via // inactius queden mostrant el prompt "POLSA START / PER JUGAR" via
// `refreshJoinPrompt` cada frame. Tots dos s'haurien de poder // `refreshJoinPrompt` cada frame. Tots dos s'haurien de poder
@@ -564,19 +539,19 @@ GameScene::~GameScene() {
} }
} }
auto GameScene::computeSpawn() -> PlayerSpawn { auto GameScene::computeSpawn(const Player::Config& cfg) -> 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
// col·lisió del jugador (12 px) cap dins del passadís (16 px) amb 2 // col·lisió del jugador (12 px) cap dins del passadís (16 px) amb 2
// px de marge a banda i banda; alinear el sprite al centre de la // px de marge a banda i banda; alinear el sprite al centre de la
// columna garanteix que la base del jugador i la del passadís // columna garanteix que la base del jugador i la del passadís
// coincideixin verticalment. // coincideixin verticalment.
const float DOOR_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 DOOR_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_TL_X = DOOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F); const float CENTER_TL_X = DOOR_CENTER_X - (static_cast<float>(cfg.sprite_w) / 2.0F);
// Y: centre del corredor exterior superior (mateix valor que abans; // Y: centre del corredor exterior superior (mateix valor que abans;
// el corredor és simètric respecte a `playfield_y`). // el corredor és simètric respecte a `playfield_y`).
const float CORRIDOR_CENTER_Y = static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F); const float CORRIDOR_CENTER_Y = static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F); const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(cfg.collision_y_offset) - (static_cast<float>(cfg.collision_size) / 2.0F);
// Mode BOTH: P1 a -tile_size, P2 a +tile_size respecte al centre. // Mode BOTH: P1 a -tile_size, P2 a +tile_size respecte al centre.
// Els callsites individuals decideixen si usar aquests valors o si // Els callsites individuals decideixen si usar aquests valors o si
@@ -594,14 +569,18 @@ void GameScene::loadRoom(const std::string& yaml_path) {
// CLOSED amb visited buit, momies i altres actors a la posició inicial. // CLOSED amb visited buit, momies i altres actors a la posició inicial.
config_.map_yaml = yaml_path; config_.map_yaml = yaml_path;
Map::loadFrom(yaml_path); Map::loadFrom(yaml_path);
// Refresquem els statics del Player a partir del YAML i tornem a // Refresquem la Config de cada jugador llegint el seu YAML i aplicant
// aplicar l'override d'aquesta sala. Sense això, una sala amb // l'override d'aquesta sala. Sense això, una sala amb `players: false`
// `players: false` deixa `light_radius=0` i, en transicionar a una // deixaria `light_radius=0` al jugador i, en transicionar a una altra
// altra sala que no toqui el camp (o digui `players: true`), els // sala que no toqui el camp (o digui `players: true`), els jugadors
// jugadors no tornen a emetre — l'override actual és "additiu" // no tornarien a emetre — l'override és "additiu" sobre el Config
// sobre els statics existents, no un reset. // actual, no un reset, així que cal partir sempre del YAML.
Player::loadFrom(config_.player_yaml); if (!players_.empty() && players_[0] != nullptr) {
applyRoomLightOverrideToPlayer(); players_[0]->setConfig(loadPlayerConfigWithRoomOverride(config_.player_yaml));
}
if (players_.size() >= 2 && players_[1] != nullptr) {
players_[1]->setConfig(loadPlayerConfigWithRoomOverride(config_.player2_yaml));
}
// Tilemap pre-blita els tiles a un buffer ARGB intern al ctor — sense // Tilemap pre-blita els tiles a un buffer ARGB intern al ctor — sense
// reconstruir-lo, la imatge de la sala anterior es queda fixada per a // reconstruir-lo, la imatge de la sala anterior es queda fixada per a
// sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor // sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor
@@ -705,7 +684,7 @@ void GameScene::respawnPlayersInstant() {
// música de sala, sense caiguda aèria). Reusem `computeSpawn` i el // música de sala, sense caiguda aèria). Reusem `computeSpawn` i el
// 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(players_[0]->getConfig());
const bool BOTH_ACTIVE = (players_active_[0] && players_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) {
@@ -736,7 +715,7 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
// 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(players_[0]->getConfig());
const bool BOTH_ACTIVE = (players_active_[0] && players_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);
@@ -841,7 +820,7 @@ void GameScene::joinPlayer(std::size_t slot_index) {
// porta de sortida. Mai el centre (en single-player la cinemàtica // 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 // inicial sí que centra el slot actiu, però en un join mid-partida
// volem el costat per coherència visual amb la regla 2). // volem el costat per coherència visual amb la regla 2).
const auto SPAWN = computeSpawn(); const auto SPAWN = computeSpawn(players_[0]->getConfig());
const float X = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x; const float X = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x;
players_[slot_index]->setSpawnPoint(X, SPAWN.y); players_[slot_index]->setSpawnPoint(X, SPAWN.y);
players_[slot_index]->resetForJoin(); players_[slot_index]->resetForJoin();
@@ -1055,22 +1034,16 @@ void GameScene::applyLightingPass() {
// serà tocar `Map::light_level` per sales concretes. // serà tocar `Map::light_level` per sales concretes.
std::vector<Lighting::Emitter> emitters; std::vector<Lighting::Emitter> emitters;
emitters.reserve(players_.size() + entity_manager_.entityCount()); emitters.reserve(players_.size() + entity_manager_.entityCount());
const int PLAYER_R = Player::getLightRadius(); // Cada jugador té el seu propi config (post-refactor a per-instance):
// Quan el YAML del jugador no declara `light:`, `PLAYER_R` és 0 i // radi, color, flicker_params, etc. poden diferir entre P1 i P2.
// `Lighting::apply` salta el cercle al filtre intern. No fem un // Quan el YAML d'un jugador no declara `light:`, el radi és 0 i
// gating extern aquí (els jugadors són només 1-2 actors i el cost // `Lighting::apply` salta el cercle al filtre intern.
// d'afegir-los al vector és menyspreable).
const bool PLAYER_FLICKER = Player::getLightFlicker();
const Lighting::Color PLAYER_COLOR{
.r = Player::getLightColorR(),
.g = Player::getLightColorG(),
.b = Player::getLightColorB(),
};
const Lighting::FlickerConfig PLAYER_FP = Player::getFlickerParams();
for (const auto& p : players_) { for (const auto& p : players_) {
if (p->isOutOfPlay()) { continue; } if (p->isOutOfPlay()) { continue; }
const int R = p->getLightRadius();
const auto C = p->getLightCenter(); const auto C = p->getLightCenter();
emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER, .color = PLAYER_COLOR, .flicker_params = PLAYER_FP}); const Lighting::Color COL{.r = p->getLightColorR(), .g = p->getLightColorG(), .b = p->getLightColorB()};
emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = p->getLightSeed(), .flicker = p->getLightFlicker(), .color = COL, .flicker_params = p->getFlickerParams()});
} }
for (const auto* e : entity_manager_.getEntities()) { for (const auto* e : entity_manager_.getEntities()) {
const int R = e->getLightRadius(); const int R = e->getLightRadius();
+6 -3
View File
@@ -8,9 +8,9 @@
#include "core/rendering/transitions/fade_to_black.hpp" #include "core/rendering/transitions/fade_to_black.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
#include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp" #include "game/debug_config.hpp"
#include "game/entities/entity_manager.hpp" #include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp"
#include "game/map/paint_layer.hpp" #include "game/map/paint_layer.hpp"
#include "game/map/tilemap.hpp" #include "game/map/tilemap.hpp"
#include "game/scenes/game_over_fsm.hpp" #include "game/scenes/game_over_fsm.hpp"
@@ -21,7 +21,6 @@
#endif #endif
class Hud; class Hud;
class Player;
class Text; class Text;
class GameScene : public Scene { class GameScene : public Scene {
@@ -382,7 +381,11 @@ class GameScene : public Scene {
float p2_x{0.0F}; float p2_x{0.0F};
float y{0.0F}; float y{0.0F};
}; };
[[nodiscard]] static auto computeSpawn() -> PlayerSpawn; // Calcula la posició de spawn dels jugadors usant la geometria del
// sprite/col·lisió del `Config` passat (típicament la del P1, que
// dicta el centrament — si en algun futur P2 té geometria diferent,
// el seu propi spawn pot recalcular-se amb el seu `cfg`).
[[nodiscard]] static auto computeSpawn(const Player::Config& cfg) -> PlayerSpawn;
// Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria, // Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria,
// porta tancada), entitats (sarcs/porta/momies des de zero), neteja // porta tancada), entitats (sarcs/porta/momies des de zero), neteja
-1
View File
@@ -17,7 +17,6 @@
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/placeholder_input.hpp" #include "game/scenes/placeholder_input.hpp"
#include "game/scenes/scene_flow.hpp" #include "game/scenes/scene_flow.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/log.hpp" #include "utils/log.hpp"
+2 -8
View File
@@ -26,15 +26,9 @@ namespace {
} // namespace } // namespace
LogoScene::LogoScene(std::shared_ptr<Surface> logo, bool reset) LogoScene::LogoScene(std::shared_ptr<Surface> logo)
: logo_(std::move(logo)) { : logo_(std::move(logo)) {
Log::info("scene", "{} start{}", SCENE_NAME, reset ? " (reset)" : ""); Log::info("scene", "{} start", SCENE_NAME);
if (reset) {
if (auto* audio = Audio::get(); audio != nullptr) {
audio->stopMusic();
audio->stopAllSounds();
}
}
// Arrenquem en BLACK_INTRO: ni so ni efecte. El glitch sound i el // Arrenquem en BLACK_INTRO: ni so ni efecte. El glitch sound i el
// begin() de scan_ es disparen quan entrem a SCAN. // begin() de scan_ es disparen quan entrem a SCAN.
} }
+1 -5
View File
@@ -29,13 +29,9 @@ class Surface;
// Skip: qualsevol tecla no reservada salta directament a la transició // Skip: qualsevol tecla no reservada salta directament a la transició
// (sense fade-out) — el TitleScene té el seu propi `intro.fade_in_ms`, // (sense fade-out) — el TitleScene té el seu propi `intro.fade_in_ms`,
// així que la transició queda visualment continua igualment. // així que la transició queda visualment continua igualment.
//
// Si arriba amb `reset=true` (acció "Reiniciar" del menú de servei),
// atura música i tots els sons al constructor — el següent LOGO arrenca
// silenciós encara que veníssem del joc en marxa.
class LogoScene : public Scene { class LogoScene : public Scene {
public: public:
LogoScene(std::shared_ptr<Surface> logo, bool reset); explicit LogoScene(std::shared_ptr<Surface> logo);
~LogoScene() override; ~LogoScene() override;
void update(int delta_ms) override; void update(int delta_ms) override;
+1 -9
View File
@@ -30,15 +30,7 @@ enum class SceneId : std::uint8_t {
// rep cap dada de l'anterior; en cas contrari, només els camps que // rep cap dada de l'anterior; en cas contrari, només els camps que
// fluixen pel canal de transició (els recursos, defaults i config // fluixen pel canal de transició (els recursos, defaults i config
// interna els resol Director quan construeix). // interna els resol Director quan construeix).
struct LogoParams { struct LogoParams {};
// Quan val `true`, indica que aquesta entrada al LOGO és una reinicialització
// completa (acció "Reiniciar" del menú de servei): l'escena haurà d'aturar
// música i sons abans de mostrar-se. En el flux normal LOGO→TITLE→… es
// queda a `false` perquè qualsevol música prèvia (p. ex. la pista de
// TITLE durant l'attract-out) continui sonant fins que la nova escena la
// substituïsca.
bool reset{false};
};
struct TitleParams {}; struct TitleParams {};
struct IntroParams { struct IntroParams {
DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH}; DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH};
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <cstdint>
#include <string> #include <string>
#include <utility> #include <utility>
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff. Show more