Compare commits

...
29 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
111 changed files with 10489 additions and 8473 deletions

No files matched your search

+5 -2
View File
@@ -65,6 +65,7 @@ set(ENTITY_SOURCES
set(APP_SOURCES
source/main.cpp
source/core/system/director.cpp
source/core/system/relaunch.cpp
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
source/core/audio/audio_effects.cpp
@@ -74,7 +75,6 @@ set(APP_SOURCES
source/core/ui/ui_config.cpp
source/core/input/key_config.cpp
source/core/input/global_inputs.cpp
source/core/input/input_state.cpp
source/core/input/gamepad_manager.cpp
source/core/input/gamepad_bindings.cpp
source/core/input/define_buttons.cpp
@@ -131,6 +131,7 @@ set(APP_SOURCES
# al TilemapWriter. Només Debug.
source/game/map/map_writer.cpp
source/game/map/paint_layer.cpp
source/game/map/room_yaml.cpp
source/game/levels/level_manager.cpp
# 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
@@ -204,7 +205,7 @@ add_executable(${PROJECT_NAME} ${APP_SOURCES})
set_source_files_properties(
source/external/stb_image_write_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)
@@ -374,6 +375,7 @@ if(BUILD_TESTS)
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/map/map.cpp
source/game/map/room_yaml.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE}
)
@@ -405,6 +407,7 @@ if(BUILD_TESTS)
source/core/resources/resource_pack.cpp
source/game/gameplay_config.cpp
source/game/map/map.cpp
source/game/map/room_yaml.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp
+15 -8
View File
@@ -27,7 +27,14 @@ ifeq ($(OS),Windows_NT)
else
NULDEV := /dev/null
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
# VERSION (única font de veritat: source/utils/defines.hpp).
@@ -84,7 +91,7 @@ else
MKDIR := mkdir -p
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Ó
@@ -115,19 +122,19 @@ run-debug: debug
# resultat a $(DIST_DIR)/.
release:
ifeq ($(OS),Windows_NT)
@"$(MAKE)" _windows_release
@"$(MAKE)" _windows-release
else
ifeq ($(UNAME_S),Darwin)
@$(MAKE) _macos_release
@$(MAKE) _macos-release
else
@$(MAKE) _linux_release
@$(MAKE) _linux-release
endif
endif
# ==============================================================================
# RELEASE — Linux (.tar.gz)
# ==============================================================================
_linux_release:
_linux-release:
@echo "Creando release para Linux - Version: $(VERSION)"
# Compila Release (genera shaders, resources.pack i binari)
@@ -158,7 +165,7 @@ _linux_release:
# ==============================================================================
# RELEASE — Windows (.zip)
# ==============================================================================
_windows_release:
_windows-release:
@echo off
@echo Creando release para Windows - Version: $(VERSION)
@@ -191,7 +198,7 @@ _windows_release:
# ==============================================================================
# RELEASE — macOS (.dmg per Intel + Apple Silicon)
# ==============================================================================
_macos_release:
_macos-release:
@echo "Creando release para macOS - Version: $(VERSION)"
# Verificar/instal·lar create-dmg si cal
+43 -7
View File
@@ -9,13 +9,6 @@ colors:
notification_bg: 64
exit_text: 71
exit_bg: 2
menu_bg: 2
menu_border: 232
menu_title: 237
menu_label: 58
menu_value: 232
menu_cursor: 71
menu_disabled: 16
hud_bg: 9
hud_flash: 63
# 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_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:
notification_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
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).
# 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
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# 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
@@ -37,24 +22,21 @@ origin: [-8, 0]
# (`light_level:` plà a l'arrel) també segueix sent vàlida per
# retrocompatibilitat.
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
emitters:
players: true
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
torches:
radius: 40
color: [255, 170, 60]
flicker_params:
wave_amplitude_ratio: 0.05
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 7.0
# 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
-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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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
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).
# 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 {
float vignette_strength;
float chroma_strength;
float chroma_min; // intensitat mínima de l'aberració cromàtica
float scanline_strength;
float screen_height;
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 time; // seconds since SDL init
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)
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
@@ -35,6 +35,19 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
float pad3; // padding per tancar a 64 bytes (4 × vec4)
} 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
vec3 rgb_to_ycc(vec3 rgb) {
return vec3(
@@ -92,10 +105,14 @@ void main() {
colour = base;
}
// Aberración cromática (drift animado con time para efecto NTSC real)
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3));
colour.r = texture(scene, uv + vec2(ca, 0.0)).r;
colour.b = texture(scene, uv - vec2(ca, 0.0)).b;
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
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)
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)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::pauseMusic() {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
engine_->pauseMusic();
@@ -98,7 +97,6 @@ void Audio::pauseMusic() {
}
// 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() {
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
engine_->resumeMusic();
@@ -106,14 +104,12 @@ void Audio::resumeMusic() {
}
// 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() {
if (music_enabled_) {
engine_->stopMusic();
}
}
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) {
engine_->setMusicSpeed(ratio);
@@ -121,7 +117,6 @@ void Audio::setMusicSpeed(float ratio) {
}
// Reprodueix un so per nom
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(const std::string& name, Group group) {
if (sound_enabled_) {
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
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(Ja::Sound* sound, Group group) {
if (sound_enabled_ && sound != nullptr) {
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.
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// 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) {
if (!sound_enabled_) { return; }
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
// 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.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; }
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
// 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) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
@@ -194,7 +185,6 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
}
// Atura tots els sons
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::stopAllSounds() {
if (sound_enabled_) {
engine_->stopChannel(-1);
@@ -202,7 +192,6 @@ void Audio::stopAllSounds() {
}
// 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) {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
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
// drenar (times == 0 + stream buit). S'executa al mateix thread que
// 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) {
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)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::setSoundVolume(float sound_volume, Group group) {
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
}
// 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) {
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
}
-1
View File
@@ -5,7 +5,6 @@
#include <functional> // Per a std::function
#include <memory> // Per a std::unique_ptr
#include <string> // Per a string
#include <utility> // Per a move
// 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
+1 -1
View File
@@ -15,7 +15,7 @@
// (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi).
#define STB_VORBIS_HEADER_ONLY
// 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"
// clang-format on
+16 -8
View File
@@ -198,42 +198,50 @@ void DefineButtons::render() const {
// Títol: nom del mando.
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,
cursor_y,
TITLE,
TITLE_KERNING,
UiConfig::get().menuTitle());
UiConfig::get().menuTitle(),
1,
UiConfig::get().menuTitleOutline());
cursor_y += TITLE_H + LINE_GAP;
if (message_shown_) {
// 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,
cursor_y,
Locale::get().text("menu.values.define_buttons_complete"),
ITEM_KERNING,
UiConfig::get().menuValue());
UiConfig::get().menuValue(),
1,
UiConfig::get().menuValueOutline());
return;
}
// Instrucció + acció pendent. Replica visualment el WindowMessage de
// 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,
cursor_y,
Locale::get().text("menu.values.press_button"),
ITEM_KERNING,
UiConfig::get().menuLabel());
UiConfig::get().menuLabel(),
1,
UiConfig::get().menuLabelOutline());
cursor_y += ITEM_H + LINE_GAP;
if (index_ < sequence_.size()) {
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,
cursor_y,
Locale::get().text(current.label_key),
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 <algorithm>
#include <array>
#include <fstream>
#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:
// els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a
// multi-jugador futur) i el passen a `InputState::getGamepadButton/Axis`,
// que ho filtra pel flag de bloqueig de la UI (mateixa estratègia que
// amb el teclat).
// multi-jugador futur) i el passen directament a
// `SDL_GetGamepadButton`/`SDL_GetGamepadAxis`.
class GamepadManager {
public:
static auto get() -> GamepadManager&;
-2
View File
@@ -9,8 +9,6 @@
#include "core/input/key_config.hpp"
#include "core/locale/locale.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/screenshot.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 <fstream>
#include <string>
#include "core/resources/resource_helper.hpp"
-1
View File
@@ -1,6 +1,5 @@
#include "core/locale/locale.hpp"
#include <fstream>
#include <string>
#include "core/resources/resource_helper.hpp"
@@ -1,6 +1,5 @@
#pragma once
#include <cstdint>
#include <random>
class Surface;
-1
View File
@@ -4,7 +4,6 @@
#include <algorithm>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
namespace Effects::Shadow {
+1
View File
@@ -4,6 +4,7 @@
#include <numeric>
#include <utility>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp"
#include "core/ui/ui_config.hpp"
-2
View File
@@ -7,9 +7,7 @@
#include <string>
#include <vector>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/surface.hpp"
#include "utils/defines.hpp"
class Text;
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h>
#include <array>
#include <cstdint>
#include <memory>
#include <string>
-1
View File
@@ -7,7 +7,6 @@
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <ranges>
#include "core/rendering/canvas.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
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
// 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:
// `<base>/screenshots/`. Si la base és buida, retorna
@@ -22,7 +22,7 @@ struct PostVOut {
struct PostFXUniforms {
float vignette_strength;
float chroma_strength;
float chroma_min;
float scanline_strength;
float screen_height;
float mask_strength;
@@ -32,7 +32,7 @@ struct PostFXUniforms {
float pixel_scale;
float time;
float flicker;
float pad2;
float chroma_max;
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio;
float scan_dark_floor;
@@ -40,6 +40,18 @@ struct PostFXUniforms {
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) {
return float3(
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;
}
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f));
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r;
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b;
// Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
// queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
// 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) {
float3 lin = pow(colour, float3(2.4f));
@@ -234,7 +234,8 @@ namespace Rendering {
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
uniforms_.vignette_strength = params.vignette;
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_.gamma_strength = params.gamma;
uniforms_.curvature = params.curvature;
@@ -15,7 +15,7 @@
struct PostFXUniforms {
// vec4 #0
float vignette_strength;
float chroma_strength;
float chroma_min;
float scanline_strength;
float screen_height;
// vec4 #1
@@ -27,7 +27,7 @@ struct PostFXUniforms {
float pixel_scale;
float time;
float flicker;
float pad2;
float chroma_max;
// vec4 #3 — paràmetres de scanlines exposats al preset YAML
float scan_dark_ratio;
float scan_dark_floor;
@@ -92,7 +92,7 @@ namespace Rendering {
PostFXUniforms uniforms_{
.vignette_strength = 0.0F,
.chroma_strength = 0.0F,
.chroma_min = 0.0F,
.scanline_strength = 0.0F,
.screen_height = 0.0F,
.mask_strength = 0.0F,
@@ -102,7 +102,7 @@ namespace Rendering {
.pixel_scale = 1.0F,
.time = 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).
.scan_dark_ratio = 0.333F,
.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 {
float vignette{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 gamma{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];
}
printWithDots("Surface", getFileName(file_path), "[ LOADED ]");
printStatus("Surface", getFileName(file_path), StatusKind::SUCCESS);
}
void Surface::clear(Uint8 color) {
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h>
#include <array>
#include <cstdint>
#include <memory>
#include <numeric>
#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;
}
+4 -4
View File
@@ -37,13 +37,13 @@ namespace Resource {
throw std::runtime_error("Cache: cannot load animation " + file_path);
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
printWithDots("Animation", getFileName(file_path), "[ LOADED ]");
printStatus("Animation", getFileName(file_path), StatusKind::SUCCESS);
}
void Cache::loadPalette(const std::string& name, const std::string& file_path) {
auto palette = PaletteLoader::loadFromFile(file_path);
palettes_.emplace_back(PaletteResource{.name = name, .palette = palette});
printWithDots("Palette", getFileName(file_path), "[ LOADED ]");
printStatus("Palette", getFileName(file_path), StatusKind::SUCCESS);
}
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);
}
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) {
@@ -96,7 +96,7 @@ namespace Resource {
throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path);
}
sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)});
printWithDots("Sound", getFileName(file_path), "[ LOADED ]");
printStatus("Sound", getFileName(file_path), StatusKind::SUCCESS);
}
auto Cache::getMusic(const std::string& name) -> Ja::Music* {
+3 -7
View File
@@ -4,6 +4,7 @@
#include <array>
#include <filesystem>
#include <fstream>
#include <numeric>
#include "utils/log.hpp"
@@ -19,12 +20,7 @@ namespace Resource {
auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t {
// djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai
// reutilitzem eines de diagnòstic entre projectes.
std::uint32_t checksum = 0x12345678;
for (unsigned char b : data) {
// cppcheck-suppress useStlAlgorithm
checksum = ((checksum << 5) + checksum) + b;
}
return checksum;
return std::accumulate(data.begin(), data.end(), std::uint32_t{0x12345678}, [](std::uint32_t acc, unsigned char b) { return ((acc << 5) + acc) + b; });
}
void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) {
@@ -151,7 +147,7 @@ namespace Resource {
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)) {
if (!entry.is_regular_file()) { continue; }
+9 -20
View File
@@ -9,7 +9,6 @@
#include <memory>
#include <string>
#include <utility>
#include <variant>
#include "core/audio/audio.hpp"
#include "core/audio/sound_effects_config.hpp"
@@ -24,7 +23,6 @@
#include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/screenshot.hpp"
#include "core/rendering/surface.hpp"
#include "core/rendering/text.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
@@ -35,8 +33,8 @@
#include "game/gameplay_config.hpp"
#ifdef _DEBUG
#include "game/editor/editor_config.hpp"
#endif
#include "game/levels/level_manager.hpp"
#endif
#include "game/options.hpp"
#include "game/scenes/game_scene.hpp"
#include "game/scenes/intro_scene.hpp"
@@ -446,9 +444,12 @@ void Director::processGamepadEvents() {
}
void Director::processMenuInput() {
// Intercepta input abans que l'escena vegi els events. Si el menú està
// obert, consumeix TOTS els KEY_DOWN perquè ni GlobalInputs (F1–F8,
// F11, F12) ni el jugador els veuen. F9 obre el menú quan està tancat.
// Intercepta input abans que l'escena vegi els events. Amb el menú
// obert, `handleEvent` consumeix tot KEY_DOWN excepte les F-keys
// (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.
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
if (ServiceMenu::get().isOpen() && ServiceMenu::get().handleEvent(*it)) {
@@ -465,16 +466,6 @@ void Director::processMenuInput() {
void Director::switchSceneIfRequested() {
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_`.
// Director el desempaqueta i construeix la nova escena via
// `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
// migrades a `ExitRequest`, aquestes lambdes desapareixeran.
switch (id) {
case SceneId::LOGO: {
const auto& p = std::get<LogoParams>(params);
return std::make_unique<LogoScene>(logo_full_surface_, p.reset);
}
case SceneId::LOGO:
return std::make_unique<LogoScene>(logo_full_surface_);
case SceneId::TITLE:
return std::make_unique<TitleScene>(placeholder_text_, copyright_text_, title_bg_surface_, game_logo_surface_, mini_logo_surface_);
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_cursor_(Defaults::UI::MENU_CURSOR),
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_p1_label_(Defaults::UI::HUD_P1_LABEL),
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, "exit_text", exit_text_);
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_flash", hud_flash_);
readColorIndex(c, "room_text", room_text_);
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`,
// `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i
// 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 menuCursor() const -> Uint8 { return menu_cursor_; }
[[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_; }
// Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada
// família té label (etiqueta apagada), value (valor destacat) i
@@ -214,6 +218,10 @@ class UiConfig {
Uint8 menu_value_;
Uint8 menu_cursor_;
Uint8 menu_disabled_;
Uint8 menu_title_outline_;
Uint8 menu_label_outline_;
Uint8 menu_value_outline_;
Uint8 menu_cursor_outline_;
Uint8 hud_bg_;
Uint8 hud_p1_label_;
Uint8 hud_p1_value_;
+1 -3
View File
@@ -345,9 +345,7 @@ namespace {
} // namespace
// cppcheck-suppress constParameterReference
// Tilemap& mutable per simetria amb `toggle()` i per futures
// invalidacions de caches d'edició, encara que `save()` sigui const.
// 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.
void Editor::exit(Tilemap& tilemap, const EntityManager& entities) {
if (!active_) { return; }
// 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 <algorithm>
#include <cmath>
#include <iterator>
#include <stdexcept>
#include <utility>
@@ -33,8 +35,7 @@ namespace {
if (speeds.empty()) {
return std::vector<float>(frame_count, K_DEFAULT_FRAME_SECONDS);
}
// cppcheck-suppress useStlAlgorithm
for (auto& s : speeds) { s = sanitizeSpeed(s); }
std::ranges::transform(speeds, speeds.begin(), sanitizeSpeed);
if (speeds.size() == frame_count) { return speeds; }
if (speeds.size() > frame_count) {
speeds.resize(frame_count);
@@ -50,10 +51,7 @@ namespace {
// component vàlid.
auto parseFrames(const fkyaml::node& anim) -> std::vector<int> {
std::vector<int> frames;
for (const auto& f : anim["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
std::ranges::transform(anim["frames"], std::back_inserter(frames), [](const fkyaml::node& f) { return f.template get_value<int>(); });
if (frames.empty()) { frames.push_back(0); }
return frames;
}
@@ -66,10 +64,7 @@ namespace {
std::vector<float> speeds_secs;
const auto& fm = anim["frame_ms"];
if (fm.is_sequence()) {
for (const auto& v : fm) {
// cppcheck-suppress useStlAlgorithm
speeds_secs.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F);
}
std::ranges::transform(fm, std::back_inserter(speeds_secs), [](const fkyaml::node& v) { return static_cast<float>(v.template get_value<int>()) / 1000.0F; });
} else {
const float SECS = static_cast<float>(fm.get_value<int>()) / 1000.0F;
speeds_secs.assign(frame_count, SECS);
@@ -2,10 +2,10 @@
#include <algorithm>
#include <filesystem>
#include <iterator>
#include <string>
#include <vector>
#include "game/entities/concrete/mummy.hpp"
#include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp"
@@ -41,10 +41,7 @@ namespace {
if (!yaml.contains("animations") || !yaml["animations"].contains("swing")) { return; }
const auto& sw = yaml["animations"]["swing"];
if (!sw.contains("active_frames")) { return; }
for (const auto& f : sw["active_frames"]) {
// cppcheck-suppress useStlAlgorithm
active_out.push_back(f.get_value<int>());
}
std::ranges::transform(sw["active_frames"], std::back_inserter(active_out), [](const fkyaml::node& f) { return f.template get_value<int>(); });
}
} // namespace
+13 -12
View File
@@ -53,18 +53,19 @@ class EntityFactory {
// 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
// sense `--whole-archive`). Tots els `concrete/*.cpp` entren a APP_SOURCES.
#define REGISTER_ENTITY_AS(NAME, CLASS, TAG) \
namespace { \
struct TAG##_Registrar { \
TAG##_Registrar() { \
EntityFactory::instance().registerCreator( \
NAME, \
[](const fkyaml::node& yaml) -> std::unique_ptr<Entity> { \
return std::make_unique<CLASS>(yaml); \
}); \
} \
}; \
const TAG##_Registrar g_##TAG##_registrar; /* NOLINT(cert-err58-cpp) */ \
#define REGISTER_ENTITY_AS(NAME, CLASS, TAG) \
namespace { \
struct TAG##_Registrar { \
TAG##_Registrar() { \
EntityFactory::instance().registerCreator( \
NAME, \
[](const fkyaml::node& yaml) -> std::unique_ptr<Entity> { \
return std::make_unique<CLASS>(yaml); \
}); \
} \
}; \
/* 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)
+5 -9
View File
@@ -1,11 +1,14 @@
#include "game/entities/entity_loader.hpp"
#include <algorithm>
#include <iterator>
#include <stdexcept>
#include <unordered_map>
#include "core/resources/resource_helper.hpp"
#include "game/entities/entity.hpp"
#include "game/entities/entity_factory.hpp"
#include "game/map/room_yaml.hpp"
#include "utils/defaults.hpp"
namespace EntityLoader {
@@ -44,20 +47,13 @@ namespace EntityLoader {
}
auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> {
auto bytes = Resource::Helper::loadFile(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);
fkyaml::node root = RoomYaml::loadMerged(level_yaml_path);
std::vector<fkyaml::node> spawns;
if (!root.contains("entities")) { return spawns; }
const auto& list = root["entities"];
if (!list.is_sequence()) { return spawns; }
spawns.reserve(list.size());
// cppcheck-suppress useStlAlgorithm
for (const auto& item : list) { spawns.push_back(item); }
std::ranges::copy(list, std::back_inserter(spawns));
return spawns;
}
+2 -2
View File
@@ -87,7 +87,7 @@ auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
entity->setEntityManager(this);
Entity* raw = entity.get();
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;
}
@@ -124,7 +124,7 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
Sarcophagus* raw = sarc.get();
entities_.push_back(std::move(sarc));
++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;
}
@@ -11,7 +11,6 @@
#include <string>
#include <utility>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
#include "external/fkyaml_node.hpp"
#include "game/entities/concrete/door.hpp"
-1
View File
@@ -7,7 +7,6 @@
#include <functional>
#include <string>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
+162 -152
View File
@@ -11,12 +11,9 @@
#include "core/audio/audio.hpp"
#include "core/input/gamepad_bindings.hpp"
#include "core/input/gamepad_manager.hpp"
#include "core/input/input_state.hpp"
#include "core/rendering/canvas.hpp"
#include "core/rendering/effects/shadow.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp"
#include "core/resources/palette_loader.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
@@ -46,12 +43,12 @@ namespace {
void accumulateGamepadMovement(int player_id, float& vx, float& vy) {
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(player_id);
if (pad == nullptr) { return; }
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT)) { vx -= SPEED_PX_PER_S; }
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) { vx += SPEED_PX_PER_S; }
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP)) { vy -= SPEED_PX_PER_S; }
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN)) { vy += SPEED_PX_PER_S; }
const Sint16 LX = InputState::getGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 LY = InputState::getGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
if (SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT)) { vx -= SPEED_PX_PER_S; }
if (SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) { vx += SPEED_PX_PER_S; }
if (SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP)) { vy -= SPEED_PX_PER_S; }
if (SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN)) { vy += SPEED_PX_PER_S; }
const Sint16 LX = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 LY = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
if (LX < -STICK_DEADZONE) { vx -= SPEED_PX_PER_S; }
if (LX > STICK_DEADZONE) { vx += SPEED_PX_PER_S; }
if (LY < -STICK_DEADZONE) { vy -= SPEED_PX_PER_S; }
@@ -514,154 +511,122 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot
}
}
// Pipeline d'estats i input separats; extracció a helpers empitjoraria la
// llegibilitat. La complexitat puja 1 per la guarda EXITED.
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void Player::tick(float delta_time) {
// EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap
// física, cap bus event. Continua sent al vector de jugadors per
// simplicitat (no s'esborra) però queda completament inert fins
// que GameScene el reactiva (respawn / spawnFromAir).
if (isOutOfPlay()) { return; }
// DYING: animació `death_spinning` + pausa de respawn. Ignorem tot
// l'input (ni moviment, ni salt, ni atac). El moviment queda
// congelat al lloc de la mort; `applyMovement` avança l'animació i
// detecta la fi per disparar el respawn. Mantenim els flancs de
// botó actualitzats perquè una tecla sostinguda no es comporte com
// a flanc nou en sortir del cicle.
void Player::tickDying() {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
input_vx_ = 0.0F;
input_vy_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
setVelX(0.0F);
setVelY(0.0F);
}
// Decrement del timer d'immunitat. Independent de l'estat: si el
// jugador mor durant la immunitat (només via forat, que no es gateja),
// el `respawn` rearma una nova finestra. Si la finestra acaba mentre
// és AIRBORNE/HURT, no passa res — el gate dels hazards passa a
// quedar obert al primer frame post-zero.
if (immunity_remaining_ms_ > 0.0F) {
immunity_remaining_ms_ -= delta_time * 1000.0F;
immunity_remaining_ms_ = std::max(immunity_remaining_ms_, 0.0F);
// En LANDING_STUN, descomptem el timer i ignorem qualsevol input
// (moviment i salt). Quan s'esgota, tornem a GROUNDED al pròxim
// frame. La velocitat queda a zero — després del stun hi tornarà
// a haver intenció si el jugador prem alguna tecla.
void Player::tickLandingStun(float delta_time) {
landing_stun_remaining_ms_ -= delta_time * 1000.0F;
if (landing_stun_remaining_ms_ <= 0.0F) {
landing_stun_remaining_ms_ = 0.0F;
state_ = State::GROUNDED;
}
input_vx_ = 0.0F;
input_vy_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
setVelX(0.0F);
setVelY(0.0F);
// Mantenim els flancs del botó de salt actualitzats fins i tot
// durant el stun: així si l'usuari l'ha estat prement abans, no
// se'l consumeix com a salt nou en sortir del stun.
(void)consumeJumpEdge();
(void)consumeAttackEdge();
}
// DYING: animació `death_spinning` + pausa de respawn. Ignorem tot
// l'input (ni moviment, ni salt, ni atac). El moviment queda
// congelat al lloc de la mort; `applyMovement` avança l'animació i
// detecta la fi per disparar el respawn. Mantenim els flancs de
// botó actualitzats perquè una tecla sostinguda no es comporte com
// a flanc nou en sortir del cicle.
if (state_ == State::DYING) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
input_vx_ = 0.0F;
input_vy_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
setVelX(0.0F);
setVelY(0.0F);
return;
// En AIRBORNE el jugador no llig input direccional: la velocitat
// horitzontal queda fixada al moment del despegue. Tampoc canvia
// facing. Sí actualitzem els flancs del botó perquè la propera
// pulsació quan torne a terra es detecte com a flanc nou.
void Player::tickAirborne() {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
setVelX(intended_vx_);
setVelY(intended_vy_);
}
// En ATTACKING el jugador queda bloquejat tot el `swing_ms +
// cooldown_ms`: ni input direccional, ni salt, ni atac, ni canvi
// de facing. El timer descompta amb `delta_time * 1000` per
// mantenir la convenció de delta_ms de la resta d'estats temporals.
// Mantenim els flancs dels botons actualitzats perquè una
// tecla mantinguda no es comporte com a flanc nou en sortir.
void Player::tickAttacking(float delta_time) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
attacking_remaining_ms_ -= delta_time * 1000.0F;
if (attacking_remaining_ms_ <= 0.0F) {
attacking_remaining_ms_ = 0.0F;
state_ = State::GROUNDED;
}
input_vx_ = 0.0F;
input_vy_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
setVelX(0.0F);
setVelY(0.0F);
}
// Reset per frame: el resolver del GameScene el tornarà a posar a
// true si aquest frame hi ha push actiu. Així quan el push acaba
// (un deixa d'empentar, l'altre comença a caminar), l'animació torna
// a governar-se per la velocitat real sense estat romanent.
being_pushed_ = false;
// En LANDING_STUN, descomptem el timer i ignorem qualsevol input
// (moviment i salt). Quan s'esgota, tornem a GROUNDED al pròxim
// frame. La velocitat queda a zero — després del stun hi tornarà
// a haver intenció si el jugador prem alguna tecla.
if (state_ == State::LANDING_STUN) {
landing_stun_remaining_ms_ -= delta_time * 1000.0F;
if (landing_stun_remaining_ms_ <= 0.0F) {
landing_stun_remaining_ms_ = 0.0F;
state_ = State::GROUNDED;
// En HURT el jugador està projectat sense control: descomptem
// velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a
// zero, tornem a GROUNDED i el frame següent ja torna a llegir input.
// El moviment real l'aplica `applyMovement` (igual que els altres
// estats), que també gestiona el xoc contra murs.
void Player::tickHurt(float delta_time) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
const float DECEL = (cfg_.hurt.brake_time_ms > 0)
? cfg_.hurt.initial_speed_px_per_s / (static_cast<float>(cfg_.hurt.brake_time_ms) / 1000.0F)
: 0.0F;
const float STEP = DECEL * delta_time;
auto decay = [STEP](float& v) {
if (std::fabs(v) <= STEP) {
v = 0.0F;
} else {
v -= std::copysign(STEP, v);
}
input_vx_ = 0.0F;
input_vy_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
setVelX(0.0F);
setVelY(0.0F);
// Mantenim els flancs del botó de salt actualitzats fins i tot
// durant el stun: així si l'usuari l'ha estat prement abans, no
// se'l consumeix com a salt nou en sortir del stun.
(void)consumeJumpEdge();
(void)consumeAttackEdge();
return;
}
// En AIRBORNE el jugador no llig input direccional: la velocitat
// horitzontal queda fixada al moment del despegue. Tampoc canvia
// facing. Sí actualitzem els flancs del botó perquè la propera
// pulsació quan torne a terra es detecte com a flanc nou.
if (state_ == State::AIRBORNE) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
setVelX(intended_vx_);
setVelY(intended_vy_);
return;
}
// En ATTACKING el jugador queda bloquejat tot el `swing_ms +
// cooldown_ms`: ni input direccional, ni salt, ni atac, ni canvi
// de facing. El timer descompta amb `delta_time * 1000` per
// mantenir la convenció de delta_ms de la resta d'estats temporals.
// Mantenim els flancs dels botons actualitzats perquè una
// tecla mantinguda no es comporte com a flanc nou en sortir.
if (state_ == State::ATTACKING) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
attacking_remaining_ms_ -= delta_time * 1000.0F;
if (attacking_remaining_ms_ <= 0.0F) {
attacking_remaining_ms_ = 0.0F;
state_ = State::GROUNDED;
}
input_vx_ = 0.0F;
input_vy_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
setVelX(0.0F);
setVelY(0.0F);
return;
}
// En HURT el jugador està projectat sense control: descomptem
// velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a
// zero, tornem a GROUNDED i el frame següent ja torna a llegir input.
// El moviment real l'aplica `applyMovement` (igual que els altres
// estats), que també gestiona el xoc contra murs.
if (state_ == State::HURT) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
const float DECEL = (cfg_.hurt.brake_time_ms > 0)
? cfg_.hurt.initial_speed_px_per_s / (static_cast<float>(cfg_.hurt.brake_time_ms) / 1000.0F)
: 0.0F;
const float STEP = DECEL * delta_time;
auto decay = [STEP](float& v) {
if (std::fabs(v) <= STEP) {
v = 0.0F;
} else {
v -= std::copysign(STEP, v);
}
};
decay(intended_vx_);
decay(intended_vy_);
if (intended_vx_ == 0.0F && intended_vy_ == 0.0F) {
state_ = State::GROUNDED;
}
input_vx_ = 0.0F;
input_vy_ = 0.0F;
setVelX(intended_vx_);
setVelY(intended_vy_);
return;
};
decay(intended_vx_);
decay(intended_vy_);
if (intended_vx_ == 0.0F && intended_vy_ == 0.0F) {
state_ = State::GROUNDED;
}
input_vx_ = 0.0F;
input_vy_ = 0.0F;
setVelX(intended_vx_);
setVelY(intended_vy_);
}
// Estat normal de joc (GROUNDED): llegeix input, integra física, actualitza
// facing i processa els flancs d'atac i salt.
void Player::tickGroundedInput(float delta_time) {
// Refresca les vinculacions des d'Options en calent: així un remap fet
// al menú de servei té efecte immediatament, sense esperar al pròxim
// GameScene::onEnter. Replica el comportament d'AEE
// (`KeyRemap::update` que llig `Options::keys_game` cada frame).
refreshControlsFromOptions();
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
// un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari
// navega el menú. Polling-blocked; veure `core/input/input_state.hpp`.
const bool* keys = InputState::getKeyboardState();
const bool* keys = SDL_GetKeyboardState(nullptr);
float vx = 0.0F;
float vy = 0.0F;
if (keys[controls_.left]) { vx -= SPEED_PX_PER_S; }
@@ -669,9 +634,7 @@ void Player::tick(float delta_time) {
if (keys[controls_.up]) { vy -= SPEED_PX_PER_S; }
if (keys[controls_.down]) { vy += SPEED_PX_PER_S; }
// Cruceta i stick esquerre del mando assignat (si n'hi ha). També
// respecten el bloqueig d'`InputState`, així que amb el menú obert
// els botons tornen false/0 i no contribueixen a la velocitat.
// Cruceta i stick esquerre del mando assignat (si n'hi ha).
accumulateGamepadMovement(id_, vx, vy);
input_vx_ = vx;
@@ -732,6 +695,55 @@ void Player::tick(float delta_time) {
}
}
// Dispatcher per estat. La complexitat real viu als handlers individuals.
void Player::tick(float delta_time) {
// EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap
// física, cap bus event. Continua sent al vector de jugadors per
// simplicitat (no s'esborra) però queda completament inert fins
// que GameScene el reactiva (respawn / spawnFromAir).
if (isOutOfPlay()) { return; }
// Decrement del timer d'immunitat. Independent de l'estat: si el
// jugador mor durant la immunitat (només via forat, que no es gateja),
// el `respawn` rearma una nova finestra. Si la finestra acaba mentre
// és AIRBORNE/HURT, no passa res — el gate dels hazards passa a
// quedar obert al primer frame post-zero.
if (immunity_remaining_ms_ > 0.0F) {
immunity_remaining_ms_ -= delta_time * 1000.0F;
immunity_remaining_ms_ = std::max(immunity_remaining_ms_, 0.0F);
}
if (state_ == State::DYING) {
tickDying();
return;
}
// Reset per frame: el resolver del GameScene el tornarà a posar a
// true si aquest frame hi ha push actiu. Així quan el push acaba
// (un deixa d'empentar, l'altre comença a caminar), l'animació torna
// a governar-se per la velocitat real sense estat romanent.
being_pushed_ = false;
if (state_ == State::LANDING_STUN) {
tickLandingStun(delta_time);
return;
}
if (state_ == State::AIRBORNE) {
tickAirborne();
return;
}
if (state_ == State::ATTACKING) {
tickAttacking(delta_time);
return;
}
if (state_ == State::HURT) {
tickHurt(delta_time);
return;
}
tickGroundedInput(delta_time);
}
void Player::refreshControlsFromOptions() {
if (id_ != 0 && id_ != 1) { return; }
const auto& kb = (id_ == 0) ? Options::controls().keyboard_p1
@@ -749,7 +761,7 @@ auto Player::consumeJumpEdge() -> bool {
// dos passa de no-premut a premut aquest frame, comptem com a salt.
// Sense detecció de flanc, mantenir la tecla provocaria saltar
// immediatament en aterrar.
const bool* keys = InputState::getKeyboardState();
const bool* keys = SDL_GetKeyboardState(nullptr);
const bool KEY_NOW = keys[controls_.jump];
bool edge = (KEY_NOW && !jump_held_keyboard_);
jump_held_keyboard_ = KEY_NOW;
@@ -758,8 +770,7 @@ auto Player::consumeJumpEdge() -> bool {
const auto& slot = (id_ == 0) ? Options::controls().gamepad_p1
: Options::controls().gamepad_p2;
const int JUMP_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::JUMP);
const bool PAD_NOW = (pad != nullptr) && !InputState::isBlocked() &&
GamepadBindings::isPressed(pad, JUMP_BTN);
const bool PAD_NOW = (pad != nullptr) && GamepadBindings::isPressed(pad, JUMP_BTN);
if (PAD_NOW && !jump_held_gamepad_) { edge = true; }
jump_held_gamepad_ = PAD_NOW;
@@ -770,7 +781,7 @@ auto Player::consumeAttackEdge() -> bool {
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. El botó
// físic ve d'`GamepadBindings`, persistit per dispositiu — l'usuari
// l'ha pogut remapejar des del menú de servei.
const bool* keys = InputState::getKeyboardState();
const bool* keys = SDL_GetKeyboardState(nullptr);
const bool KEY_NOW = keys[controls_.attack];
bool edge = (KEY_NOW && !attack_held_keyboard_);
attack_held_keyboard_ = KEY_NOW;
@@ -779,8 +790,7 @@ auto Player::consumeAttackEdge() -> bool {
const auto& slot = (id_ == 0) ? Options::controls().gamepad_p1
: Options::controls().gamepad_p2;
const int ATK_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::ATTACK);
const bool PAD_NOW = (pad != nullptr) && !InputState::isBlocked() &&
GamepadBindings::isPressed(pad, ATK_BTN);
const bool PAD_NOW = (pad != nullptr) && GamepadBindings::isPressed(pad, ATK_BTN);
if (PAD_NOW && !attack_held_gamepad_) { edge = true; }
attack_held_gamepad_ = PAD_NOW;
+9 -1
View File
@@ -10,7 +10,6 @@
#include "core/rendering/lighting.hpp"
#include "core/rendering/sprite/animated_sprite.hpp"
#include "core/rendering/surface.hpp"
#include "external/fkyaml_node.hpp"
#include "game/map/map.hpp"
class EventBus;
@@ -604,6 +603,15 @@ class Player : public AnimatedSprite {
void renderShadow() const;
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ó
// (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`.
[[nodiscard]] auto baseBox() const -> SDL_FRect { return getBaseBox(); }
+2 -8
View File
@@ -5,8 +5,8 @@
#include <stdexcept>
#include <unordered_map>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "game/map/room_yaml.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
@@ -157,13 +157,7 @@ namespace Map {
}
void loadFrom(const std::string& yaml_path) {
auto bytes = Resource::Helper::loadFile(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);
auto yaml = RoomYaml::loadMerged(yaml_path);
if (!yaml.contains("geometry")) {
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/surface.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "game/map/room_yaml.hpp"
#include "utils/log.hpp"
#ifdef _DEBUG
@@ -41,13 +41,7 @@ namespace {
} // namespace
Tilemap::Tilemap(const std::string& yaml_path) {
auto bytes = Resource::Helper::loadFile(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);
auto yaml = RoomYaml::loadMerged(yaml_path);
tileset_path_ = yaml["tileset"].get_value<std::string>();
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.
// 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> {
return {
{.name = "CRT",
.vignette = 0.6F,
.scanlines = 0.7F,
.chroma = 0.15F,
.chroma_min = 0.15F,
.chroma_max = 0.15F,
.mask = 0.6F,
.gamma = 0.8F,
.curvature = 0.0F,
@@ -262,7 +265,8 @@ namespace {
{.name = "NTSC",
.vignette = 0.4F,
.scanlines = 0.5F,
.chroma = 0.2F,
.chroma_min = 0.2F,
.chroma_max = 0.2F,
.mask = 0.4F,
.gamma = 0.5F,
.curvature = 0.0F,
@@ -271,7 +275,8 @@ namespace {
{.name = "Curved",
.vignette = 0.5F,
.scanlines = 0.6F,
.chroma = 0.1F,
.chroma_min = 0.1F,
.chroma_max = 0.1F,
.mask = 0.5F,
.gamma = 0.7F,
.curvature = 0.8F,
@@ -280,7 +285,8 @@ namespace {
{.name = "Scanlines",
.vignette = 0.0F,
.scanlines = 0.8F,
.chroma = 0.0F,
.chroma_min = 0.0F,
.chroma_max = 0.0F,
.mask = 0.0F,
.gamma = 0.0F,
.curvature = 0.0F,
@@ -289,7 +295,8 @@ namespace {
{.name = "Subtle",
.vignette = 0.3F,
.scanlines = 0.4F,
.chroma = 0.05F,
.chroma_min = 0.05F,
.chroma_max = 0.05F,
.mask = 0.0F,
.gamma = 0.3F,
.curvature = 0.0F,
@@ -298,7 +305,8 @@ namespace {
{.name = "CRT Live",
.vignette = 0.5F,
.scanlines = 0.6F,
.chroma = 0.3F,
.chroma_min = 0.3F,
.chroma_max = 0.3F,
.mask = 0.3F,
.gamma = 0.4F,
.curvature = 0.3F,
@@ -311,7 +319,8 @@ namespace {
readField(node, "name", preset.name);
readField(node, "vignette", preset.vignette);
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, "gamma", preset.gamma);
readField(node, "curvature", preset.curvature);
@@ -326,7 +335,8 @@ namespace {
out << " - name: \"" << preset.name << "\"\n";
out << " vignette: " << preset.vignette << "\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 << " gamma: " << preset.gamma << "\n";
out << " curvature: " << preset.curvature << "\n";
@@ -876,7 +886,8 @@ namespace Options {
return {
.vignette = IT->vignette,
.scanlines = IT->scanlines,
.chroma = IT->chroma,
.chroma_min = IT->chroma_min,
.chroma_max = IT->chroma_max,
.mask = IT->mask,
.gamma = IT->gamma,
.curvature = IT->curvature,
+3 -1
View File
@@ -59,7 +59,9 @@ namespace Options {
std::string name;
float vignette{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 gamma{0.0F};
float curvature{0.0F};
+8 -14
View File
@@ -14,7 +14,6 @@
#include "core/audio/audio.hpp"
#include "core/input/global_inputs.hpp"
#include "core/input/key_config.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/lighting.hpp"
#include "core/rendering/notifier.hpp"
@@ -22,8 +21,6 @@
#include "core/rendering/screen.hpp"
#include "core/rendering/text.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 "external/fkyaml_node.hpp"
#include "game/entities/concrete/dust.hpp"
@@ -33,11 +30,16 @@
#include "game/entities/entity_factory.hpp"
#include "game/entities/entity_loader.hpp"
#include "game/entities/event_bus.hpp"
#include "game/entities/hazard.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/levels/level_manager.hpp"
#include "game/map/map.hpp"
#include "game/map/room_yaml.hpp"
#include "game/options.hpp"
#include "game/scenes/scene_flow.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
// simplement no deixa rastre visual.
auto readOptionalInt(const std::string& yaml_path, const std::string& key, int fallback = -1) -> int {
auto bytes = Resource::Helper::loadFile(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);
auto yaml = RoomYaml::loadMerged(yaml_path);
if (!yaml.contains(key)) { return fallback; }
return yaml[key].get_value<int>();
}
auto readOptionalFloat(const std::string& yaml_path, const std::string& key, float fallback) -> float {
auto bytes = Resource::Helper::loadFile(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);
auto yaml = RoomYaml::loadMerged(yaml_path);
if (!yaml.contains(key)) { return fallback; }
return yaml[key].get_value<float>();
}
-1
View File
@@ -8,7 +8,6 @@
#include "core/rendering/transitions/fade_to_black.hpp"
#include "core/scene/scene.hpp"
#include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp"
-1
View File
@@ -17,7 +17,6 @@
#include "game/options.hpp"
#include "game/scenes/placeholder_input.hpp"
#include "game/scenes/scene_flow.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp"
#include "utils/log.hpp"
+2 -8
View File
@@ -26,15 +26,9 @@ namespace {
} // namespace
LogoScene::LogoScene(std::shared_ptr<Surface> logo, bool reset)
LogoScene::LogoScene(std::shared_ptr<Surface> logo)
: logo_(std::move(logo)) {
Log::info("scene", "{} start{}", SCENE_NAME, reset ? " (reset)" : "");
if (reset) {
if (auto* audio = Audio::get(); audio != nullptr) {
audio->stopMusic();
audio->stopAllSounds();
}
}
Log::info("scene", "{} start", SCENE_NAME);
// Arrenquem en BLACK_INTRO: ni so ni efecte. El glitch sound i el
// 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ó
// (sense fade-out) — el TitleScene té el seu propi `intro.fade_in_ms`,
// 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 {
public:
LogoScene(std::shared_ptr<Surface> logo, bool reset);
explicit LogoScene(std::shared_ptr<Surface> logo);
~LogoScene() 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
// fluixen pel canal de transició (els recursos, defaults i config
// interna els resol Director quan construeix).
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 LogoParams {};
struct TitleParams {};
struct IntroParams {
DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH};
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h>
#include <algorithm>
#include <cstdint>
#include <string>
#include <utility>
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more