Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd513f3bdb | ||
|
|
ebb15d32be | ||
|
|
53174f11f9 | ||
|
|
da923ae421 | ||
|
|
ef496bd971 | ||
|
|
3db0ed4457 | ||
|
|
0c6bebe8dc | ||
|
|
a8ccb3fae1 | ||
|
|
774874deb0 | ||
|
|
24afdef7cd | ||
|
|
f29cc7318a | ||
|
|
7a859c1e44 | ||
|
|
822bed0748 | ||
|
|
d7db02bafd | ||
|
|
b55473ae0b | ||
|
|
35d3b359eb | ||
|
|
b81a453b32 | ||
|
|
7a1261202c | ||
|
|
f0c118e6fa | ||
|
|
bc5a6abf53 | ||
|
|
4daeb0e15c | ||
|
|
b6403e8de5 | ||
|
|
0e8a195ddd | ||
|
|
80140434e7 | ||
|
|
fd3da8cb99 | ||
|
|
885af944c5 | ||
|
|
2e41815d3c | ||
|
|
125cdd0c74 | ||
|
|
5f206e03c8 | ||
|
|
fa77f4cee3 | ||
|
|
a02706ac8b | ||
|
|
2aef925286 |
No files matched your search
+5
-2
@@ -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
|
||||
|
||||
@@ -27,7 +27,14 @@ ifeq ($(OS),Windows_NT)
|
||||
else
|
||||
NULDEV := /dev/null
|
||||
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
|
||||
# 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 |
@@ -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_));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
@@ -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&;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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,6 +1,5 @@
|
||||
#include "core/input/key_config.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
|
||||
namespace Effects::Shadow {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
|
||||
#include "core/rendering/canvas.hpp"
|
||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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* {
|
||||
|
||||
@@ -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,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: {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,7 +64,8 @@ class EntityFactory {
|
||||
}); \
|
||||
} \
|
||||
}; \
|
||||
const TAG##_Registrar g_##TAG##_registrar; /* NOLINT(cert-err58-cpp) */ \
|
||||
/* NOLINTNEXTLINE(cert-err58-cpp) -- inicialització estàtica intencionada: el constructor registra el creator a l'EntityFactory abans de main(); el patró és l'únic mecanisme C++ que permet auto-registre per tipus sense modificar la factory. */ \
|
||||
const TAG##_Registrar g_##TAG##_registrar; \
|
||||
}
|
||||
|
||||
#define REGISTER_ENTITY(NAME, CLASS) REGISTER_ENTITY_AS(NAME, CLASS, CLASS)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
|
||||
+370
-388
File diff suppressed because it is too large.
Load diff
+190
-212
@@ -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;
|
||||
@@ -55,6 +54,15 @@ class Player : public AnimatedSprite {
|
||||
HOLE, // caiguda per agujero — animació `death_hole`
|
||||
};
|
||||
|
||||
// Direcció cardinal completa. Pública perquè el `WhipAttack` la passa a
|
||||
// `enterHurt` quan colpeja un altre jugador (friendly fire). Declarada
|
||||
// aquí (a sobre de `Config` perquè `TurnConfig` l'utilitza).
|
||||
enum class Cardinal : std::uint8_t { NONE,
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
enum class State : std::uint8_t {
|
||||
GROUNDED,
|
||||
AIRBORNE,
|
||||
@@ -92,80 +100,172 @@ class Player : public AnimatedSprite {
|
||||
DYING,
|
||||
};
|
||||
|
||||
// Geometria carregada des del YAML del player (p.ex. `data/entities/player/sam.yaml`).
|
||||
// `sprite_*` són
|
||||
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
|
||||
// 12 píxels superiors són el cap i els 12 inferiors la base).
|
||||
// La caixa de col·lisió és un quadrat `collision_size` concèntric amb el
|
||||
// quadrat inferior del sprite (`base_size`). Els offsets els calculem
|
||||
// al load a partir d'aquests tres valors; el mapa de col·lisions i el
|
||||
// de dibuix són independents (el sprite pot sobresortir sense afectar).
|
||||
inline static int sprite_w = 0;
|
||||
inline static int sprite_h = 0;
|
||||
inline static int base_size = 0;
|
||||
inline static int collision_size = 0;
|
||||
inline static int collision_padding = 0;
|
||||
inline static int collision_x_offset = 0;
|
||||
inline static int collision_y_offset = 0;
|
||||
// Configuració per-instància del Player. Conté tot el que abans vivia
|
||||
// com a `inline static` o `static *_config`: així cada jugador (P1=sam,
|
||||
// P2=morcus) pot tenir paràmetres diferents llegits del seu YAML.
|
||||
// GameScene la carrega amb `Config::loadFromYaml` abans d'instanciar
|
||||
// cada Player i la passa al ctor (mou-la). Per refrescar-la entre
|
||||
// sales s'utilitza `Player::setConfig`.
|
||||
//
|
||||
// Els sub-structs (`InertiaConfig`, `TurnConfig`, ...) viuen dins de
|
||||
// `Config` per agrupar-los visualment amb la resta del config — no hi
|
||||
// ha cap raó tècnica per haver-los a fora.
|
||||
struct Config {
|
||||
// ---- Geometria del sprite i col·lisió --------------------------
|
||||
// `sprite_*` són la mida del frame (sprite 12×24 amb perspectiva
|
||||
// falsa). La caixa de col·lisió és un quadrat `collision_size`
|
||||
// concèntric amb el quadrat inferior del sprite (`base_size`).
|
||||
// Els offsets es calculen al load.
|
||||
int sprite_w{0};
|
||||
int sprite_h{0};
|
||||
int base_size{0};
|
||||
int collision_size{0};
|
||||
int collision_padding{0};
|
||||
int collision_x_offset{0};
|
||||
int collision_y_offset{0};
|
||||
|
||||
// Caixa de "contacte" més petita que la caixa de col·lisió: s'usa per
|
||||
// a les interaccions actor-actor (player↔player, player↔momia/trampa),
|
||||
// on volem que el contacte sigui precís i no dispari a "distància"
|
||||
// de la vora del sprite. La caixa de col·lisió gran continua usant-se
|
||||
// per xocar amb murs i sarcòfags (per mantenir el carril del
|
||||
// passadís). Sense `hit_size` al YAML, s'iguala a `collision_size` i
|
||||
// el comportament és idèntic a l'anterior.
|
||||
inline static int hit_size = 0;
|
||||
inline static int hit_x_offset = 0;
|
||||
inline static int hit_y_offset = 0;
|
||||
// Caixa de contacte actor-actor (més petita que la col·lisió).
|
||||
int hit_size{0};
|
||||
int hit_x_offset{0};
|
||||
int hit_y_offset{0};
|
||||
|
||||
// Alçada del cos del jugador en píxels (camp `collision.height` del
|
||||
// YAML, mateixa convenció que entitats com mummy o rolling_rock).
|
||||
// L'usen els filtres de col·lisió per altura: si un altre actor ens
|
||||
// creua mentre `jump_z_` és superior a aquest valor, no col·lisionem.
|
||||
inline static int body_height = 0;
|
||||
// Alçada del cos en píxels (filtre de col·lisió per altura).
|
||||
int body_height{0};
|
||||
|
||||
// Radi de la llum emesa pel jugador (en píxels del canvas), llegit
|
||||
// del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }`
|
||||
// del YAML. 0 = no emet. Es declara estàtic com la resta de configs
|
||||
// del jugador (immunity, jump, ...): tots els jugadors del mateix YAML
|
||||
// comparteixen el valor. `light_flicker` activa la modulació "tipus
|
||||
// foc" del radi; `light_color_*` són el tint quan la sala usa mode
|
||||
// additive (en subtractive s'ignora); `light_offset_*` desplaça el
|
||||
// centre del cercle respecte el centre del sprite (Y creix avall:
|
||||
// `y: -3` puja la llum 3 px).
|
||||
inline static int light_radius = 0;
|
||||
inline static bool light_flicker = false;
|
||||
inline static std::uint8_t light_color_r = 255;
|
||||
inline static std::uint8_t light_color_g = 255;
|
||||
inline static std::uint8_t light_color_b = 255;
|
||||
inline static int light_offset_x = 0;
|
||||
inline static int light_offset_y = 0;
|
||||
// Paràmetres del flicker resolts per al jugador (combinació del
|
||||
// global + override del YAML del personatge + override de sala).
|
||||
// Només té efecte si `light_flicker` és true.
|
||||
inline static Lighting::FlickerConfig flicker_params{};
|
||||
// ---- Emissió de llum -----------------------------------------
|
||||
// Llegit del bloc opcional `light:` del YAML.
|
||||
// `radius` → 0 = no emet.
|
||||
// `flicker` → activa modulació tipus foc.
|
||||
// `color` → tint en mode additive (ignorat en subtractive).
|
||||
// `offset` → desplaçament del centre respecte el sprite (Y avall).
|
||||
int light_radius{0};
|
||||
bool light_flicker{false};
|
||||
std::uint8_t light_color_r{255};
|
||||
std::uint8_t light_color_g{255};
|
||||
std::uint8_t light_color_b{255};
|
||||
int light_offset_x{0};
|
||||
int light_offset_y{0};
|
||||
// Paràmetres del flicker resolts (global + override del YAML +
|
||||
// override de sala). Només té efecte si `light_flicker` és true.
|
||||
Lighting::FlickerConfig flicker_params{};
|
||||
|
||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||
static void loadFrom(const std::string& yaml_path);
|
||||
// ---- Sub-structs de gameplay ---------------------------------
|
||||
|
||||
// Vides amb què arrenca un jugador (camp `lives:` del YAML del
|
||||
// jugador). El consumeix el HUD per inicialitzar el comptador del
|
||||
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
|
||||
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
|
||||
// Inèrcia: amb `brake_time_ms == 0` el comportament és instantani.
|
||||
struct InertiaConfig {
|
||||
int brake_time_ms{0};
|
||||
int threshold_ms{0};
|
||||
};
|
||||
|
||||
// Gir de 180°: amb `duration_ms == 0` els canvis de direcció són
|
||||
// instantanis. `Cardinal` ha d'estar visible — declarada a sobre.
|
||||
struct TurnConfig {
|
||||
int duration_ms{0};
|
||||
Cardinal w_to_e{Cardinal::SOUTH};
|
||||
Cardinal e_to_w{Cardinal::NORTH};
|
||||
Cardinal n_to_s{Cardinal::EAST};
|
||||
Cardinal s_to_n{Cardinal::WEST};
|
||||
};
|
||||
|
||||
// Salt: amb `impulse_px_per_s == 0` el botó no té efecte.
|
||||
struct JumpConfig {
|
||||
float impulse_px_per_s{0.0F};
|
||||
float gravity_px_per_s2{0.0F};
|
||||
int landing_stun_ms{0};
|
||||
std::string sound_key;
|
||||
std::string land_sound_key;
|
||||
};
|
||||
|
||||
// HURT (estat post-impacte). Amb `initial_speed_px_per_s == 0`
|
||||
// entrada/sortida instantànies (sense moviment).
|
||||
struct HurtConfig {
|
||||
float initial_speed_px_per_s{0.0F};
|
||||
int brake_time_ms{0};
|
||||
std::string sound_key;
|
||||
};
|
||||
|
||||
// Látigo: hitbox `range_tiles`, swing `swing_ms`, cooldown
|
||||
// `cooldown_ms`. Per ara `damage` només es llegeix.
|
||||
struct WhipConfig {
|
||||
float range_tiles{0.0F};
|
||||
int swing_ms{0};
|
||||
int cooldown_ms{0};
|
||||
int damage{0};
|
||||
std::string sound_key;
|
||||
};
|
||||
|
||||
// Mort/respawn: `respawn_delay_ms` entre l'animació i la
|
||||
// reaparició; `initial_lives` és el comptador d'intents.
|
||||
struct DeathConfig {
|
||||
int respawn_delay_ms{0};
|
||||
int initial_lives{3};
|
||||
};
|
||||
|
||||
// Immunitat post-spawn. `duration_ms == 0` desactiva la mecànica.
|
||||
// `palette_key` (basename del .pal) és la paleta plana del blink.
|
||||
struct ImmunityConfig {
|
||||
float duration_ms{0.0F};
|
||||
int blink_period_ms{0};
|
||||
std::string palette_key;
|
||||
};
|
||||
|
||||
InertiaConfig inertia{};
|
||||
TurnConfig turn{};
|
||||
JumpConfig jump{};
|
||||
HurtConfig hurt{};
|
||||
WhipConfig whip{};
|
||||
DeathConfig death{};
|
||||
ImmunityConfig immunity{};
|
||||
|
||||
// Llig tot el YAML del personatge i retorna un `Config` poblat.
|
||||
// Substitueix l'antic `Player::loadFrom` que mutava estàtics.
|
||||
// Llança si manca el bloc `collision:` (obligatori).
|
||||
[[nodiscard]] static auto loadFromYaml(const std::string& yaml_path) -> Config;
|
||||
|
||||
// Aplica l'override de sala (bloc `lighting.emitters.player` del
|
||||
// YAML del mapa) sobre els camps de llum d'aquesta Config. Es
|
||||
// crida després de `loadFromYaml` i abans de passar la Config al
|
||||
// ctor de Player (o a `setConfig` en transicions de sala).
|
||||
void applyRoomLightOverride();
|
||||
};
|
||||
|
||||
Player(Config cfg, const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
||||
|
||||
// Reemplaça el `Config` actual del Player. Cridat per GameScene al
|
||||
// canvi de sala per re-aplicar valors base + override de la sala nova.
|
||||
// No toca cap estat de joc (lives, posició, FSM): només la config.
|
||||
void setConfig(Config cfg);
|
||||
|
||||
// Accés a la config viva (lectura). El callsites que abans accedien
|
||||
// a estàtics fan servir `getters d'instància` (sota); aquest és per
|
||||
// codi que vol l'objecte sencer.
|
||||
[[nodiscard]] auto getConfig() const -> const Config& { return cfg_; }
|
||||
|
||||
// Vides amb què arrenca un jugador (camp `lives:` del YAML). El
|
||||
// consumeix el HUD per inicialitzar el marcador i `resetForJoin`
|
||||
// per a re-entrades mid-partida.
|
||||
[[nodiscard]] auto getInitialLives() const -> int { return cfg_.death.initial_lives; }
|
||||
|
||||
// Geometria del cos — instance getters dels camps de `cfg_` que
|
||||
// alguns callsites externs (game_scene) consulten directament.
|
||||
[[nodiscard]] auto getBodyHeight() const -> int { return cfg_.body_height; }
|
||||
[[nodiscard]] auto getSpriteW() const -> int { return cfg_.sprite_w; }
|
||||
[[nodiscard]] auto getSpriteH() const -> int { return cfg_.sprite_h; }
|
||||
[[nodiscard]] auto getCollisionSize() const -> int { return cfg_.collision_size; }
|
||||
[[nodiscard]] auto getCollisionYOffset() const -> int { return cfg_.collision_y_offset; }
|
||||
|
||||
// Emissió de llum (mateix paper que `Entity::getLightRadius` per a
|
||||
// les entitats). El centre puja amb el salt: durant `jump_z_>0` el
|
||||
// sprite es dibuixa amb `pos_.y - jump_z_`, i la llum ha de seguir-lo
|
||||
// per a que el cercle "salti" amb el jugador, no es quedi al sòl.
|
||||
[[nodiscard]] static auto getLightRadius() -> int { return light_radius; }
|
||||
[[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; }
|
||||
[[nodiscard]] static auto getLightColorR() -> std::uint8_t { return light_color_r; }
|
||||
[[nodiscard]] static auto getLightColorG() -> std::uint8_t { return light_color_g; }
|
||||
[[nodiscard]] static auto getLightColorB() -> std::uint8_t { return light_color_b; }
|
||||
[[nodiscard]] static auto getLightOffsetX() -> int { return light_offset_x; }
|
||||
[[nodiscard]] static auto getLightOffsetY() -> int { return light_offset_y; }
|
||||
[[nodiscard]] static auto getFlickerParams() -> const Lighting::FlickerConfig& { return flicker_params; }
|
||||
[[nodiscard]] auto getLightRadius() const -> int { return cfg_.light_radius; }
|
||||
[[nodiscard]] auto getLightFlicker() const -> bool { return cfg_.light_flicker; }
|
||||
[[nodiscard]] auto getLightColorR() const -> std::uint8_t { return cfg_.light_color_r; }
|
||||
[[nodiscard]] auto getLightColorG() const -> std::uint8_t { return cfg_.light_color_g; }
|
||||
[[nodiscard]] auto getLightColorB() const -> std::uint8_t { return cfg_.light_color_b; }
|
||||
[[nodiscard]] auto getLightOffsetX() const -> int { return cfg_.light_offset_x; }
|
||||
[[nodiscard]] auto getLightOffsetY() const -> int { return cfg_.light_offset_y; }
|
||||
[[nodiscard]] auto getFlickerParams() const -> const Lighting::FlickerConfig& { return cfg_.flicker_params; }
|
||||
// Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador
|
||||
// local) amb un offset gran per evitar col·lisions amb el contador
|
||||
// d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases
|
||||
@@ -173,8 +273,6 @@ class Player : public AnimatedSprite {
|
||||
[[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; }
|
||||
[[nodiscard]] auto getLightCenter() const -> SDL_FPoint;
|
||||
|
||||
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
||||
|
||||
// Update en una sola crida: manté l'API anterior per als tests i
|
||||
// escenaris amb un sol jugador. Internament és equivalent a cridar
|
||||
// `tick(dt)` seguit d'`applyMovement(dt)`.
|
||||
@@ -275,7 +373,7 @@ class Player : public AnimatedSprite {
|
||||
// que coincideix amb aquest mètode i s'usa específicament per
|
||||
// marcar la posició de la sombra a nivell de sòl.
|
||||
[[nodiscard]] auto getFeetY() const -> float {
|
||||
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
|
||||
return getPosY() + static_cast<float>(cfg_.collision_y_offset + cfg_.collision_size);
|
||||
}
|
||||
|
||||
// Alies semàntic: els peus del jugador al sòl. Coincideix amb
|
||||
@@ -484,7 +582,7 @@ class Player : public AnimatedSprite {
|
||||
// arbitrària (p. ex. la posició proposada per `tryMove` abans
|
||||
// d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora
|
||||
// de Player.
|
||||
[[nodiscard]] static auto hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect;
|
||||
[[nodiscard]] auto hitBoxFromBase(const SDL_FRect& base) const -> SDL_FRect;
|
||||
|
||||
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
|
||||
// caixa al callback de col·lisió. La base del Player és més petita
|
||||
@@ -505,6 +603,15 @@ class Player : public AnimatedSprite {
|
||||
void renderShadow() const;
|
||||
|
||||
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(); }
|
||||
@@ -568,6 +675,11 @@ class Player : public AnimatedSprite {
|
||||
// efecte immediat sense haver de tornar a entrar a la GameScene.
|
||||
void refreshControlsFromOptions();
|
||||
|
||||
// Tot el config del personatge (geometria, llum, jump, hurt, whip,
|
||||
// death, immunity, turn, inertia). Llegit del YAML per instància
|
||||
// (P1 i P2 poden tenir valors diferents) i mogut al ctor.
|
||||
Config cfg_;
|
||||
|
||||
int id_{-1};
|
||||
Controls controls_;
|
||||
|
||||
@@ -639,26 +751,6 @@ class Player : public AnimatedSprite {
|
||||
static auto facingSuffix(Facing facing) -> const char*;
|
||||
static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*;
|
||||
|
||||
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
||||
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
|
||||
// un altre jugador (friendly fire).
|
||||
public:
|
||||
enum class Cardinal : std::uint8_t { NONE,
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
private:
|
||||
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
||||
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
||||
// el comportament és instantani: soltar input para el jugador en sec.
|
||||
struct InertiaConfig {
|
||||
int brake_time_ms{0}; // Temps en ms perquè la velocitat caiga de ±SPEED a 0.
|
||||
int threshold_ms{0}; // Temps mínim en moviment continu per activar la inèrcia.
|
||||
};
|
||||
static InertiaConfig inertia_config;
|
||||
|
||||
// Integra un eix. Si hi ha input en aquest eix, la velocitat física
|
||||
// salta a aquest valor (acceleració instantània). Si no, i hi ha
|
||||
// input en l'altre eix (`any_input == true`), la velocitat d'aquest
|
||||
@@ -666,127 +758,7 @@ class Player : public AnimatedSprite {
|
||||
// soltant l'input — no volem que arrossegue en diagonal. Només quan
|
||||
// l'usuari deixa anar tot l'input (`any_input == false`) s'activa la
|
||||
// inèrcia de frenada (si `motion_time_ms` ha passat el llindar).
|
||||
static void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input);
|
||||
|
||||
// Configuració del gir de 180°, llegida del bloc `turn:` del YAML del
|
||||
// player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica
|
||||
// queda desactivada i els canvis de direcció són instantanis.
|
||||
struct TurnConfig {
|
||||
int duration_ms{0};
|
||||
Cardinal w_to_e{Cardinal::SOUTH};
|
||||
Cardinal e_to_w{Cardinal::NORTH};
|
||||
Cardinal n_to_s{Cardinal::EAST};
|
||||
Cardinal s_to_n{Cardinal::WEST};
|
||||
};
|
||||
// Definida out-of-class al .cpp: dins de la classe, els initializers
|
||||
// per defecte de TurnConfig encara no són visibles per al compilador.
|
||||
static TurnConfig turn_config;
|
||||
|
||||
// Configuració del salt llegida del bloc `jump:` del YAML del player.
|
||||
// Si `impulse_px_per_s == 0` (o el bloc no existeix) la mecànica del
|
||||
// salt queda desactivada — el botó no té efecte.
|
||||
struct JumpConfig {
|
||||
float impulse_px_per_s{0.0F}; // Velocitat vertical inicial.
|
||||
float gravity_px_per_s2{0.0F}; // Acceleració descendent constant.
|
||||
int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta).
|
||||
std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci).
|
||||
std::string land_sound_key; // Clau del so d'aterratge (buit = silenci).
|
||||
};
|
||||
static JumpConfig jump_config;
|
||||
|
||||
// Llig el bloc `collision:` del YAML i n'omple les caixes (size/base/
|
||||
// hit) + `body_height`. Llança si el bloc no existeix. Membre estàtic
|
||||
// per accedir als estàtics privats sense exposar-los.
|
||||
static void readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path);
|
||||
|
||||
// Llig el bloc `jump:` del YAML i n'omple `jump_config`. Si el bloc
|
||||
// manca, deixa la mecànica del salt desactivada.
|
||||
static void readJumpBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració de l'estat HURT (estat post-impacte). Si
|
||||
// `initial_speed_px_per_s == 0` el jugador entra i surt
|
||||
// instantàniament (cap moviment).
|
||||
struct HurtConfig {
|
||||
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
||||
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
||||
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/audio/sounds/`).
|
||||
// Mateixa convenció que la resta de sons del jugador: el basename val
|
||||
// de clau al Resource::Cache i de path. Buit o absent = entrada
|
||||
// silenciosa.
|
||||
std::string sound_key;
|
||||
};
|
||||
static HurtConfig hurt_config;
|
||||
|
||||
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
||||
static void readHurtBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració del látigo del jugador llegida del bloc `whip:` del
|
||||
// YAML. `range_tiles` defineix la longitud de la hitbox abans de
|
||||
// retallar; `swing_ms` és la durada de l'animació d'atac (des
|
||||
// d'`enterAttacking` fins a desbloquejar el cooldown); `cooldown_ms`
|
||||
// afegeix una fase posterior durant la qual el jugador segueix
|
||||
// bloquejat però la hitbox ja no fa dany. `damage` queda llegit per
|
||||
// a futur; per ara els objectius reben sempre `enterHurt`.
|
||||
struct WhipConfig {
|
||||
float range_tiles{0.0F};
|
||||
int swing_ms{0};
|
||||
int cooldown_ms{0};
|
||||
int damage{0};
|
||||
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
|
||||
// `data/audio/sounds/`). Convenció: el basename val tant de clau al
|
||||
// Resource::Cache com de path. Buit o absent = swing silenciós.
|
||||
std::string sound_key;
|
||||
};
|
||||
static WhipConfig whip_config;
|
||||
|
||||
// Llig el bloc `whip:` del YAML i n'omple `whip_config`. Si manca el
|
||||
// bloc, els camps queden a 0 i el jugador no pot atacar (el flanc
|
||||
// d'input es perd silenciosament).
|
||||
static void readWhipBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració de mort/respawn llegida del bloc `death:` del YAML.
|
||||
// `respawn_delay_ms` és la pausa entre la fi de `death_spinning` i la
|
||||
// reaparició al spawn; `initial_lives` és el comptador d'intents amb
|
||||
// què naix cada Player. Si el bloc manca, defaults segurs (0/3).
|
||||
struct DeathConfig {
|
||||
int respawn_delay_ms{0};
|
||||
int initial_lives{3};
|
||||
};
|
||||
static DeathConfig death_config;
|
||||
|
||||
// Llig el bloc `death:` del YAML i n'omple `death_config`. Si manca,
|
||||
// `respawn_delay_ms` queda a 0 (el respawn és immediat al final de
|
||||
// l'animació) i `initial_lives` es manté al default (3).
|
||||
static void readDeathBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració de la immunitat post-spawn llegida del bloc `immunity:`
|
||||
// del YAML. `duration_ms` és el temps per defecte que dura cada
|
||||
// immunitat (l'invoca `respawn`). `blink_period_ms` és el mig-cicle del
|
||||
// parpadeig visual: cada `blink_period_ms` ms, el render alterna
|
||||
// entre la paleta normal i la paleta `palette_key`. Si `duration_ms`
|
||||
// és 0 (o el bloc manca) la immunitat queda desactivada i el
|
||||
// comportament és l'antic.
|
||||
struct ImmunityConfig {
|
||||
float duration_ms{0.0F};
|
||||
int blink_period_ms{0};
|
||||
// Clau al Resource::Cache de la paleta plana usada al blink (basename
|
||||
// del .pal a `data/entities/player/`). Buida = no hi ha alternança visual,
|
||||
// només la finestra lògica d'immunitat (que sense feedback visual
|
||||
// pot ser confusa, però no és incorrecte).
|
||||
std::string palette_key;
|
||||
};
|
||||
static ImmunityConfig immunity_config;
|
||||
|
||||
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
|
||||
// hi ha `palette`, registra la paleta corresponent al Resource::Cache
|
||||
// (path: `data/entities/player/<palette_key>.pal`). Mateix patró que els altres
|
||||
// readers — `loadFrom` el crida al final.
|
||||
static void readImmunityBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Llig el bloc opcional `light: { radius: N, flicker: bool }` del
|
||||
// YAML i pobla `light_radius` / `light_flicker`. Si manca, els
|
||||
// valors queden a 0 / false (jugador sense llum).
|
||||
static void readLightBlock(const fkyaml::node& yaml);
|
||||
void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) const;
|
||||
|
||||
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
|
||||
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
|
||||
@@ -992,10 +964,16 @@ class Player : public AnimatedSprite {
|
||||
// Extret de `update` per acotar la complexitat cognitiva (clang-tidy).
|
||||
void updateFacingAndTurn(float delta_time, float vx, float vy);
|
||||
static auto cardinalFromInput(float vx, float vy) -> Cardinal;
|
||||
static auto cardinalFromString(const std::string& s) -> Cardinal;
|
||||
static auto opposite(Cardinal c) -> Cardinal;
|
||||
static auto intermediateFor(Cardinal target) -> Cardinal;
|
||||
auto intermediateFor(Cardinal target) const -> Cardinal;
|
||||
|
||||
public:
|
||||
// Helper pur: tradueix un string YAML ("north", "south", ...) al
|
||||
// Cardinal corresponent. Públic perquè `Config::loadFromYaml` el crida
|
||||
// des d'un helper de la TU anònima a player.cpp.
|
||||
static auto cardinalFromString(const std::string& s) -> Cardinal;
|
||||
|
||||
private:
|
||||
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
||||
// Player només en guarda un punter per publicar events.
|
||||
EventBus* bus_{nullptr};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -171,9 +165,10 @@ namespace {
|
||||
// velocitats horitzontals i els deixem rebotar fins a aterrar
|
||||
// — els dos cauen en stun, com a càstig per abusar del salt.
|
||||
if (A_AIR) {
|
||||
// cppcheck-suppress knownConditionTrueFalse
|
||||
const auto BODY_H = static_cast<float>(Player::body_height);
|
||||
// cppcheck-suppress knownConditionTrueFalse
|
||||
// Usem el body_height d'A (els dos haurien de ser idèntics en
|
||||
// multijugador local actual; si en algun moment difereixen entre
|
||||
// P1 i P2, prenem el del primer participant per consistència).
|
||||
const auto BODY_H = static_cast<float>(a->getBodyHeight());
|
||||
if (BODY_H <= 0.0F) { return; }
|
||||
if (std::fabs(a->getJumpZ() - b->getJumpZ()) >= BODY_H) { return; }
|
||||
// Saltadors estàtics horitzontalment (cas degenerat: tots dos
|
||||
@@ -232,37 +227,14 @@ namespace {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Override de sala per a la llum del jugador. Si el YAML de la sala
|
||||
// declara `lighting.emitters.players`, substitueix els valors carregats
|
||||
// per `Player::loadFrom`. Extret aquí per no inflar la complexitat
|
||||
// cognitiva del ctor de GameScene (clang-tidy: cognitive-complexity).
|
||||
// No s'aplica una "tercera capa" per-instància al jugador perquè no
|
||||
// neix com a spawn del nivell; el dia d'un ítem "linterna" aquell
|
||||
// sistema mutarà els mateixos camps en runtime — ortogonal a aquest
|
||||
// override.
|
||||
void applyRoomLightOverrideToPlayer() {
|
||||
const auto* room = Map::getRoomEmitterOverride("player");
|
||||
if (room == nullptr) { return; }
|
||||
if (!room->enabled) {
|
||||
Player::light_radius = 0;
|
||||
Player::light_flicker = false;
|
||||
return;
|
||||
}
|
||||
if (room->radius.has_value()) { Player::light_radius = *room->radius; }
|
||||
if (room->flicker.has_value()) { Player::light_flicker = *room->flicker; }
|
||||
if (room->color.has_value()) {
|
||||
Player::light_color_r = (*room->color)[0];
|
||||
Player::light_color_g = (*room->color)[1];
|
||||
Player::light_color_b = (*room->color)[2];
|
||||
}
|
||||
if (room->offset.has_value()) {
|
||||
Player::light_offset_x = (*room->offset)[0];
|
||||
Player::light_offset_y = (*room->offset)[1];
|
||||
}
|
||||
if (room->flicker_wave_amplitude_ratio.has_value()) { Player::flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; }
|
||||
if (room->flicker_wave_frequency_hz.has_value()) { Player::flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; }
|
||||
if (room->flicker_noise_amplitude_ratio.has_value()) { Player::flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; }
|
||||
if (room->flicker_noise_frequency_hz.has_value()) { Player::flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; }
|
||||
// Carrega la config d'un jugador des del seu YAML i aplica l'override
|
||||
// de sala (si el bloc `lighting.emitters.player` el reescriu). Helper
|
||||
// compartit entre ctor i `loadRoom` per fer la mateixa cosa als dos
|
||||
// moments: arrencada de partida i transició de sala.
|
||||
auto loadPlayerConfigWithRoomOverride(const std::string& yaml_path) -> Player::Config {
|
||||
auto cfg = Player::Config::loadFromYaml(yaml_path);
|
||||
cfg.applyRoomLightOverride();
|
||||
return cfg;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -315,14 +287,14 @@ GameScene::GameScene(Config config)
|
||||
const std::string ANIM_P1 = load_player_assets(config_.player_sprite, config_.player_yaml);
|
||||
const std::string ANIM_P2 = load_player_assets(config_.player2_sprite, config_.player2_yaml);
|
||||
|
||||
// Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada
|
||||
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
||||
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
||||
// són `static inline`, compartits per tots els jugadors; carregar des
|
||||
// del YAML de P1 és suficient (els dos personatges comparteixen
|
||||
// geometria — morcus és un repaletat de sam).
|
||||
Player::loadFrom(config_.player_yaml);
|
||||
applyRoomLightOverrideToPlayer();
|
||||
// Geometria de col·lisió i tota la resta de config del jugador
|
||||
// (caixa 16×16, base 12, jump, hurt, llum, etc.) carregada per
|
||||
// instància des del YAML del personatge corresponent. Cada Player
|
||||
// rep el seu `Config` al ctor (moure'l avall). L'override de sala
|
||||
// s'aplica aquí abans de moure'l perquè el jugador ja arrenqui
|
||||
// amb la llum correcta per a la sala actual.
|
||||
Player::Config cfg_p1 = loadPlayerConfigWithRoomOverride(config_.player_yaml);
|
||||
Player::Config cfg_p2 = loadPlayerConfigWithRoomOverride(config_.player2_yaml);
|
||||
|
||||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||||
@@ -418,8 +390,9 @@ GameScene::GameScene(Config config)
|
||||
// Spawn lligat a la porta: tots els jugadors apareixen davant del
|
||||
// passadís de sortida. `computeSpawn` deriva les coordenades de
|
||||
// `Map::door_col` perquè canviar la columna de la porta al YAML
|
||||
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
|
||||
const auto SPAWN = computeSpawn();
|
||||
// moga automàticament l'spawn — no hi ha cap valor hardcoded. P1
|
||||
// dicta el centrament (consistent amb el comportament pre-refactor).
|
||||
const auto SPAWN = computeSpawn(cfg_p1);
|
||||
const bool BOTH_ACTIVE = (USE_P1 && USE_P2);
|
||||
|
||||
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||||
@@ -445,8 +418,8 @@ GameScene::GameScene(Config config)
|
||||
if (USE_P1) { p1_x = SPAWN.p1_x + TILE_F; }
|
||||
if (USE_P2) { p2_x = SPAWN.p2_x - TILE_F; }
|
||||
}
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||
players_.emplace_back(std::make_unique<Player>(std::move(cfg_p1), ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
players_.emplace_back(std::make_unique<Player>(std::move(cfg_p2), ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||
|
||||
// Tots els slots arrenquen DISABLED. Els actius els reactivarà el
|
||||
// callback de fi de música d'INTRO via `completeIntro()` (spawn aeri
|
||||
@@ -499,10 +472,12 @@ GameScene::GameScene(Config config)
|
||||
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||||
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||||
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||||
// Vides inicials del HUD = `initial_lives` del YAML del jugador
|
||||
// (`Player::loadFrom` ja ha omplert `death_config` abans d'aquí).
|
||||
// Sense això, el marcador es queda fixe a 3 i el YAML perd efecte.
|
||||
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, Player::getInitialLives());
|
||||
// Vides inicials del HUD = `initial_lives` del Config del P1.
|
||||
// (Cada Player té el seu propi Config post-refactor a per-instance;
|
||||
// el HUD agafa el de P1 com a referència — historicament era el
|
||||
// valor compartit per tots els jugadors). Si en algun futur P1 i P2
|
||||
// arrenquen amb vides diferents, caldria un HUD per slot.
|
||||
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, players_[0]->getInitialLives());
|
||||
// Activem el bloc HUD per als slots que arranquen en partida. Els
|
||||
// inactius queden mostrant el prompt "POLSA START / PER JUGAR" via
|
||||
// `refreshJoinPrompt` cada frame. Tots dos s'haurien de poder
|
||||
@@ -564,19 +539,19 @@ GameScene::~GameScene() {
|
||||
}
|
||||
}
|
||||
|
||||
auto GameScene::computeSpawn() -> PlayerSpawn {
|
||||
auto GameScene::computeSpawn(const Player::Config& cfg) -> PlayerSpawn {
|
||||
// Centre de la columna del passadís en píxels canvas. La caixa de
|
||||
// col·lisió del jugador (12 px) cap dins del passadís (16 px) amb 2
|
||||
// px de marge a banda i banda; alinear el sprite al centre de la
|
||||
// columna garanteix que la base del jugador i la del passadís
|
||||
// coincideixin verticalment.
|
||||
const float DOOR_CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
|
||||
const float CENTER_TL_X = DOOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
||||
const float CENTER_TL_X = DOOR_CENTER_X - (static_cast<float>(cfg.sprite_w) / 2.0F);
|
||||
|
||||
// Y: centre del corredor exterior superior (mateix valor que abans;
|
||||
// el corredor és simètric respecte a `playfield_y`).
|
||||
const float CORRIDOR_CENTER_Y = static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
||||
const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
||||
const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(cfg.collision_y_offset) - (static_cast<float>(cfg.collision_size) / 2.0F);
|
||||
|
||||
// Mode BOTH: P1 a -tile_size, P2 a +tile_size respecte al centre.
|
||||
// Els callsites individuals decideixen si usar aquests valors o si
|
||||
@@ -594,14 +569,18 @@ void GameScene::loadRoom(const std::string& yaml_path) {
|
||||
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
|
||||
config_.map_yaml = yaml_path;
|
||||
Map::loadFrom(yaml_path);
|
||||
// Refresquem els statics del Player a partir del YAML i tornem a
|
||||
// aplicar l'override d'aquesta sala. Sense això, una sala amb
|
||||
// `players: false` deixa `light_radius=0` i, en transicionar a una
|
||||
// altra sala que no toqui el camp (o digui `players: true`), els
|
||||
// jugadors no tornen a emetre — l'override actual és "additiu"
|
||||
// sobre els statics existents, no un reset.
|
||||
Player::loadFrom(config_.player_yaml);
|
||||
applyRoomLightOverrideToPlayer();
|
||||
// Refresquem la Config de cada jugador llegint el seu YAML i aplicant
|
||||
// l'override d'aquesta sala. Sense això, una sala amb `players: false`
|
||||
// deixaria `light_radius=0` al jugador i, en transicionar a una altra
|
||||
// sala que no toqui el camp (o digui `players: true`), els jugadors
|
||||
// no tornarien a emetre — l'override és "additiu" sobre el Config
|
||||
// actual, no un reset, així que cal partir sempre del YAML.
|
||||
if (!players_.empty() && players_[0] != nullptr) {
|
||||
players_[0]->setConfig(loadPlayerConfigWithRoomOverride(config_.player_yaml));
|
||||
}
|
||||
if (players_.size() >= 2 && players_[1] != nullptr) {
|
||||
players_[1]->setConfig(loadPlayerConfigWithRoomOverride(config_.player2_yaml));
|
||||
}
|
||||
// Tilemap pre-blita els tiles a un buffer ARGB intern al ctor — sense
|
||||
// reconstruir-lo, la imatge de la sala anterior es queda fixada per a
|
||||
// sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor
|
||||
@@ -705,7 +684,7 @@ void GameScene::respawnPlayersInstant() {
|
||||
// música de sala, sense caiguda aèria). Reusem `computeSpawn` i el
|
||||
// mateix patró de `triggerPlayerSpawn` però cridem `respawn(x, y)`
|
||||
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
|
||||
const auto SPAWN = computeSpawn();
|
||||
const auto SPAWN = computeSpawn(players_[0]->getConfig());
|
||||
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
||||
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||
@@ -736,7 +715,7 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
||||
|
||||
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
||||
// la lògica ja queda preparada per a l'itinerari).
|
||||
const auto SPAWN = computeSpawn();
|
||||
const auto SPAWN = computeSpawn(players_[0]->getConfig());
|
||||
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
||||
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
|
||||
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
||||
@@ -841,7 +820,7 @@ void GameScene::joinPlayer(std::size_t slot_index) {
|
||||
// porta de sortida. Mai el centre (en single-player la cinemàtica
|
||||
// inicial sí que centra el slot actiu, però en un join mid-partida
|
||||
// volem el costat per coherència visual amb la regla 2).
|
||||
const auto SPAWN = computeSpawn();
|
||||
const auto SPAWN = computeSpawn(players_[0]->getConfig());
|
||||
const float X = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x;
|
||||
players_[slot_index]->setSpawnPoint(X, SPAWN.y);
|
||||
players_[slot_index]->resetForJoin();
|
||||
@@ -1055,22 +1034,16 @@ void GameScene::applyLightingPass() {
|
||||
// serà tocar `Map::light_level` per sales concretes.
|
||||
std::vector<Lighting::Emitter> emitters;
|
||||
emitters.reserve(players_.size() + entity_manager_.entityCount());
|
||||
const int PLAYER_R = Player::getLightRadius();
|
||||
// Quan el YAML del jugador no declara `light:`, `PLAYER_R` és 0 i
|
||||
// `Lighting::apply` salta el cercle al filtre intern. No fem un
|
||||
// gating extern aquí (els jugadors són només 1-2 actors i el cost
|
||||
// d'afegir-los al vector és menyspreable).
|
||||
const bool PLAYER_FLICKER = Player::getLightFlicker();
|
||||
const Lighting::Color PLAYER_COLOR{
|
||||
.r = Player::getLightColorR(),
|
||||
.g = Player::getLightColorG(),
|
||||
.b = Player::getLightColorB(),
|
||||
};
|
||||
const Lighting::FlickerConfig PLAYER_FP = Player::getFlickerParams();
|
||||
// Cada jugador té el seu propi config (post-refactor a per-instance):
|
||||
// radi, color, flicker_params, etc. poden diferir entre P1 i P2.
|
||||
// Quan el YAML d'un jugador no declara `light:`, el radi és 0 i
|
||||
// `Lighting::apply` salta el cercle al filtre intern.
|
||||
for (const auto& p : players_) {
|
||||
if (p->isOutOfPlay()) { continue; }
|
||||
const int R = p->getLightRadius();
|
||||
const auto C = p->getLightCenter();
|
||||
emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER, .color = PLAYER_COLOR, .flicker_params = PLAYER_FP});
|
||||
const Lighting::Color COL{.r = p->getLightColorR(), .g = p->getLightColorG(), .b = p->getLightColorB()};
|
||||
emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = p->getLightSeed(), .flicker = p->getLightFlicker(), .color = COL, .flicker_params = p->getFlickerParams()});
|
||||
}
|
||||
for (const auto* e : entity_manager_.getEntities()) {
|
||||
const int R = e->getLightRadius();
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
#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"
|
||||
#include "game/map/paint_layer.hpp"
|
||||
#include "game/map/tilemap.hpp"
|
||||
#include "game/scenes/game_over_fsm.hpp"
|
||||
@@ -21,7 +21,6 @@
|
||||
#endif
|
||||
|
||||
class Hud;
|
||||
class Player;
|
||||
class Text;
|
||||
|
||||
class GameScene : public Scene {
|
||||
@@ -382,7 +381,11 @@ class GameScene : public Scene {
|
||||
float p2_x{0.0F};
|
||||
float y{0.0F};
|
||||
};
|
||||
[[nodiscard]] static auto computeSpawn() -> PlayerSpawn;
|
||||
// Calcula la posició de spawn dels jugadors usant la geometria del
|
||||
// sprite/col·lisió del `Config` passat (típicament la del P1, que
|
||||
// dicta el centrament — si en algun futur P2 té geometria diferent,
|
||||
// el seu propi spawn pot recalcular-se amb el seu `cfg`).
|
||||
[[nodiscard]] static auto computeSpawn(const Player::Config& cfg) -> PlayerSpawn;
|
||||
|
||||
// Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria,
|
||||
// porta tancada), entitats (sarcs/porta/momies des de zero), neteja
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
|
||||
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user