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 | ||
|
|
8eba52353b | ||
|
|
15a4016407 | ||
|
|
2272d3a37a | ||
|
|
3a42c7958a | ||
|
|
dacc4a9909 | ||
|
|
771ff1f6cd | ||
|
|
24609d6c5c | ||
|
|
9d0dde38f9 | ||
|
|
b76a3eb1f3 | ||
|
|
a779533d61 |
No files matched your search
+5
-2
@@ -65,6 +65,7 @@ set(ENTITY_SOURCES
|
|||||||
set(APP_SOURCES
|
set(APP_SOURCES
|
||||||
source/main.cpp
|
source/main.cpp
|
||||||
source/core/system/director.cpp
|
source/core/system/director.cpp
|
||||||
|
source/core/system/relaunch.cpp
|
||||||
source/core/audio/audio.cpp
|
source/core/audio/audio.cpp
|
||||||
source/core/audio/audio_adapter.cpp
|
source/core/audio/audio_adapter.cpp
|
||||||
source/core/audio/audio_effects.cpp
|
source/core/audio/audio_effects.cpp
|
||||||
@@ -74,7 +75,6 @@ set(APP_SOURCES
|
|||||||
source/core/ui/ui_config.cpp
|
source/core/ui/ui_config.cpp
|
||||||
source/core/input/key_config.cpp
|
source/core/input/key_config.cpp
|
||||||
source/core/input/global_inputs.cpp
|
source/core/input/global_inputs.cpp
|
||||||
source/core/input/input_state.cpp
|
|
||||||
source/core/input/gamepad_manager.cpp
|
source/core/input/gamepad_manager.cpp
|
||||||
source/core/input/gamepad_bindings.cpp
|
source/core/input/gamepad_bindings.cpp
|
||||||
source/core/input/define_buttons.cpp
|
source/core/input/define_buttons.cpp
|
||||||
@@ -131,6 +131,7 @@ set(APP_SOURCES
|
|||||||
# al TilemapWriter. Només Debug.
|
# al TilemapWriter. Només Debug.
|
||||||
source/game/map/map_writer.cpp
|
source/game/map/map_writer.cpp
|
||||||
source/game/map/paint_layer.cpp
|
source/game/map/paint_layer.cpp
|
||||||
|
source/game/map/room_yaml.cpp
|
||||||
source/game/levels/level_manager.cpp
|
source/game/levels/level_manager.cpp
|
||||||
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
|
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
|
||||||
# En Release els fitxers compilen a object buit i l'enllaçador no
|
# En Release els fitxers compilen a object buit i l'enllaçador no
|
||||||
@@ -204,7 +205,7 @@ add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
|||||||
set_source_files_properties(
|
set_source_files_properties(
|
||||||
source/external/stb_image_write_impl.cpp
|
source/external/stb_image_write_impl.cpp
|
||||||
source/external/stb_vorbis_impl.cpp
|
source/external/stb_vorbis_impl.cpp
|
||||||
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations"
|
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations;-Wno-tautological-compare"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT APPLE AND GLSLC_EXE)
|
if(NOT APPLE AND GLSLC_EXE)
|
||||||
@@ -374,6 +375,7 @@ if(BUILD_TESTS)
|
|||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
|
source/game/map/room_yaml.cpp
|
||||||
source/utils/log.cpp
|
source/utils/log.cpp
|
||||||
${ENTITY_SOURCES_BASE}
|
${ENTITY_SOURCES_BASE}
|
||||||
)
|
)
|
||||||
@@ -405,6 +407,7 @@ if(BUILD_TESTS)
|
|||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
source/game/gameplay_config.cpp
|
source/game/gameplay_config.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
|
source/game/map/room_yaml.cpp
|
||||||
source/utils/log.cpp
|
source/utils/log.cpp
|
||||||
${ENTITY_SOURCES_BASE}
|
${ENTITY_SOURCES_BASE}
|
||||||
source/game/entities/concrete/whip_attack.cpp
|
source/game/entities/concrete/whip_attack.cpp
|
||||||
|
|||||||
@@ -27,8 +27,15 @@ ifeq ($(OS),Windows_NT)
|
|||||||
else
|
else
|
||||||
NULDEV := /dev/null
|
NULDEV := /dev/null
|
||||||
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
|
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
|
||||||
|
# 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 :=
|
CMAKE_GEN :=
|
||||||
endif
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
# VERSION (única font de veritat: source/utils/defines.hpp).
|
# VERSION (única font de veritat: source/utils/defines.hpp).
|
||||||
# Lazy (=): només es calcula quan s'invoca un target que la usa
|
# Lazy (=): només es calcula quan s'invoca un target que la usa
|
||||||
@@ -84,7 +91,7 @@ else
|
|||||||
MKDIR := mkdir -p
|
MKDIR := mkdir -p
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: all debug release _windows_release _macos_release _linux_release run run-debug clean rebuild show-version pack controllerdb format format-check tidy tidy-fix cppcheck hooks-install help
|
.PHONY: all debug release _windows-release _macos-release _linux-release run run-debug clean rebuild show-version pack controllerdb format format-check tidy tidy-fix cppcheck hooks-install help
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓ
|
# COMPILACIÓ
|
||||||
@@ -115,19 +122,19 @@ run-debug: debug
|
|||||||
# resultat a $(DIST_DIR)/.
|
# resultat a $(DIST_DIR)/.
|
||||||
release:
|
release:
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
@"$(MAKE)" _windows_release
|
@"$(MAKE)" _windows-release
|
||||||
else
|
else
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
@$(MAKE) _macos_release
|
@$(MAKE) _macos-release
|
||||||
else
|
else
|
||||||
@$(MAKE) _linux_release
|
@$(MAKE) _linux-release
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RELEASE — Linux (.tar.gz)
|
# RELEASE — Linux (.tar.gz)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
_linux_release:
|
_linux-release:
|
||||||
@echo "Creando release para Linux - Version: $(VERSION)"
|
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||||
|
|
||||||
# Compila Release (genera shaders, resources.pack i binari)
|
# Compila Release (genera shaders, resources.pack i binari)
|
||||||
@@ -158,7 +165,7 @@ _linux_release:
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RELEASE — Windows (.zip)
|
# RELEASE — Windows (.zip)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
_windows_release:
|
_windows-release:
|
||||||
@echo off
|
@echo off
|
||||||
@echo Creando release para Windows - Version: $(VERSION)
|
@echo Creando release para Windows - Version: $(VERSION)
|
||||||
|
|
||||||
@@ -191,7 +198,7 @@ _windows_release:
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RELEASE — macOS (.dmg per Intel + Apple Silicon)
|
# RELEASE — macOS (.dmg per Intel + Apple Silicon)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
_macos_release:
|
_macos-release:
|
||||||
@echo "Creando release para macOS - Version: $(VERSION)"
|
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||||
|
|
||||||
# Verificar/instal·lar create-dmg si cal
|
# Verificar/instal·lar create-dmg si cal
|
||||||
|
|||||||
@@ -12,8 +12,13 @@
|
|||||||
# mid_ratio..1.0 → outer_intensity (anell exterior)
|
# mid_ratio..1.0 → outer_intensity (anell exterior)
|
||||||
# fora del radi → light_level de la sala
|
# fora del radi → light_level de la sala
|
||||||
#
|
#
|
||||||
# Les intensitats són factors 0..1 que es multipliquen sobre el píxel
|
# Hi ha dues corbes paral·leles, una per mode, perquè la mateixa escala
|
||||||
# final del canvas. inner=1.0 vol dir "píxel sense alterar".
|
# 0..1 vol dir coses molt diferents:
|
||||||
|
# `gradient` → subtractive: 1.0 = "no fosqueix" el píxel
|
||||||
|
# (l'emissor "trenca" una sala fosca).
|
||||||
|
# `gradient_additive` → additive: 1.0 = "afegeix el color sencer"
|
||||||
|
# (satura). Valors molt més baixos donen un
|
||||||
|
# halo cromàtic subtil sobre una sala normal.
|
||||||
gradient:
|
gradient:
|
||||||
inner_ratio: 0.5
|
inner_ratio: 0.5
|
||||||
mid_ratio: 0.85
|
mid_ratio: 0.85
|
||||||
@@ -21,16 +26,31 @@ gradient:
|
|||||||
mid_intensity: 0.8
|
mid_intensity: 0.8
|
||||||
outer_intensity: 0.6
|
outer_intensity: 0.6
|
||||||
|
|
||||||
|
# Corba del mode additive. Valors petits a propòsit: només volem un
|
||||||
|
# toc de tint cromàtic prop de l'emissor — fora del nucli, la sala
|
||||||
|
# original ha de quedar pràcticament intacta. Si pugeu massa l'inner,
|
||||||
|
# els colors de l'emissor "inunden" la sala (additiu satura ràpid).
|
||||||
|
gradient_additive:
|
||||||
|
inner_ratio: 0.5
|
||||||
|
mid_ratio: 0.85
|
||||||
|
inner_intensity: 0.15
|
||||||
|
mid_intensity: 0.10
|
||||||
|
outer_intensity: 0.05
|
||||||
|
|
||||||
# Modulació del radi quan una entitat declara `flicker: true` al seu
|
# Modulació del radi quan una entitat declara `flicker: true` al seu
|
||||||
# bloc `light:`. El radi efectiu per frame és:
|
# bloc `light:`. El radi efectiu per frame és:
|
||||||
# r_eff = r_base + A1·sin(2π·f1·t + φ1) + A2·sin(2π·f2·t + φ2)
|
# r_eff = r_base + (r_amp1·r_base)·sin(2π·f1·t + φ1) + (r_amp2·r_base)·sin(2π·f2·t + φ2)
|
||||||
# on (φ1, φ2) es deriven de la posició de l'emissor per a que cada
|
# on (φ1, φ2) es deriven de la seed de l'emissor per a que cada
|
||||||
# antorxa balancegi en una fase diferent (sense estat per emissor).
|
# antorxa balancegi en una fase diferent (sense estat per emissor).
|
||||||
# Combinant dues sinusoides amb freqüències incommensurables s'obté
|
# Les amplituds són **ràtios** sobre el radi base: així el feel del
|
||||||
# un patró "fluid" que el ull llegeix com a foc; pujar `noise_*`
|
# flicker és independent del radi (una torxa de 24 px oscil·la el
|
||||||
# aporta més inquietud, baixar-ho fa la flama més serena.
|
# mateix % que una de 40 px). Combinant dues sinusoides amb freqüències
|
||||||
|
# incommensurables s'obté un patró "fluid" que el ull llegeix com a
|
||||||
|
# foc; pujar `noise_*` aporta més inquietud, baixar-ho fa la flama
|
||||||
|
# més serena. Aquests són els defaults globals; cada tipus d'entitat
|
||||||
|
# (o cada spawn / sala) pot sobreescriure'ls al seu `light.flicker_params:`.
|
||||||
flicker:
|
flicker:
|
||||||
wave_amplitude_px: 2
|
wave_amplitude_ratio: 0.05 # 5% del radi (per a radi 40 → ±2 px, feel actual)
|
||||||
wave_frequency_hz: 3.0
|
wave_frequency_hz: 3.0
|
||||||
noise_amplitude_px: 1
|
noise_amplitude_ratio: 0.025 # 2.5% (per a radi 40 → ±1 px, feel actual)
|
||||||
noise_frequency_hz: 7.0
|
noise_frequency_hz: 7.0
|
||||||
+43
-7
@@ -9,13 +9,6 @@ colors:
|
|||||||
notification_bg: 64
|
notification_bg: 64
|
||||||
exit_text: 71
|
exit_text: 71
|
||||||
exit_bg: 2
|
exit_bg: 2
|
||||||
menu_bg: 2
|
|
||||||
menu_border: 232
|
|
||||||
menu_title: 237
|
|
||||||
menu_label: 58
|
|
||||||
menu_value: 232
|
|
||||||
menu_cursor: 71
|
|
||||||
menu_disabled: 16
|
|
||||||
hud_bg: 9
|
hud_bg: 9
|
||||||
hud_flash: 63
|
hud_flash: 63
|
||||||
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
|
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
|
||||||
@@ -27,6 +20,49 @@ colors:
|
|||||||
room_text: 55 # or brillant (#FFDA00, = hud_center.value)
|
room_text: 55 # or brillant (#FFDA00, = hud_center.value)
|
||||||
room_text_outline: 10 # marró cremat (#482400, = hud_center.shadow)
|
room_text_outline: 10 # marró cremat (#482400, = hud_center.shadow)
|
||||||
|
|
||||||
|
# Tema actiu del menú de servei. Ha de coincidir amb una clau de
|
||||||
|
# `menu_themes`; si manca o no existeix, cada slot cau al default
|
||||||
|
# compilat a Defaults::UI (= tema "classic"). Canviar de tema requereix
|
||||||
|
# reiniciar el joc — la càrrega de paleta es fa només en arrencar.
|
||||||
|
menu_theme: egyptian
|
||||||
|
|
||||||
|
# Conjunts de colors del menú de servei. Cada tema defineix els 7 slots
|
||||||
|
# (sense prefix `menu_` perquè ja som dins del bloc del tema). Si manca
|
||||||
|
# alguna clau dins d'un tema, cau al default compilat. Per afegir un
|
||||||
|
# tema nou, només cal una nova entrada aquí i posar-ne el nom a
|
||||||
|
# `menu_theme`.
|
||||||
|
menu_themes:
|
||||||
|
# Tema històric (defaults compilats). Cian + verd llima + magenta —
|
||||||
|
# paleta arcade clàssica, freda i amb molt contrast.
|
||||||
|
classic:
|
||||||
|
bg: 2 # marró-vermell fosc (#480000)
|
||||||
|
border: 232 # cian brillant (#00B6FF)
|
||||||
|
title: 237 # lavanda (#B6B6FF)
|
||||||
|
title_outline: 64 # navy fosc (#0000AA) — halo fred del títol
|
||||||
|
label: 58 # verd llima (#48FF00)
|
||||||
|
label_outline: 17 # verd molt fosc (#244800) — bord same-family del label
|
||||||
|
value: 232 # cian brillant (#00B6FF) — coincideix amb el border
|
||||||
|
value_outline: 64 # navy fosc (#0000AA) — halo cian
|
||||||
|
cursor: 71 # magenta-vermell (#FF0055)
|
||||||
|
cursor_outline: 1 # granat fosc (#240000) — halo càlid del cursor
|
||||||
|
disabled: 16 # verd fosc (#004800)
|
||||||
|
# Tema "egípci": família or/ambre/marró coherent amb el HUD i el
|
||||||
|
# rètol de transició de sala (room_text). El bg comparteix to amb
|
||||||
|
# `room_text_outline` i `hud_center.shadow` perquè el menú "creixi"
|
||||||
|
# del mateix món visual que el joc.
|
||||||
|
egyptian:
|
||||||
|
bg: 10 # marró cremat (#482400)
|
||||||
|
border: 55 # or brillant (#FFDA00)
|
||||||
|
title: 127 # groc llimona viu (#FFFF55)
|
||||||
|
title_outline: 19 # oliva fosc (#6D4800) — halo càlid del groc
|
||||||
|
label: 47 # ambre (#FFB600)
|
||||||
|
label_outline: 11 # marró (#6D2400) — bord càlid del label
|
||||||
|
value: 191 # crema clar (#FFFFAA)
|
||||||
|
value_outline: 19 # oliva fosc (#6D4800) — recolza el crema
|
||||||
|
cursor: 23 # vermilló (#FF4800)
|
||||||
|
cursor_outline: 9 # marró molt fosc (#240000) — halo profund del cursor
|
||||||
|
disabled: 19 # oliva fosc (#6D4800)
|
||||||
|
|
||||||
timings:
|
timings:
|
||||||
notification_ms: 4000
|
notification_ms: 4000
|
||||||
exit_ms: 4000
|
exit_ms: 4000
|
||||||
|
|||||||
File renamed without changes.
@@ -4,7 +4,7 @@ editable: true
|
|||||||
# Sub-picker de frames a l'editor: 6 variants (atlas 2×3 de sub-sprites 16×32).
|
# Sub-picker de frames a l'editor: 6 variants (atlas 2×3 de sub-sprites 16×32).
|
||||||
editable_variants: true
|
editable_variants: true
|
||||||
sprite:
|
sprite:
|
||||||
path: ../debris/column.gif
|
path: column.gif
|
||||||
size: [16, 32]
|
size: [16, 32]
|
||||||
# Ancoratge "feet": el tile [col, row] del spawn és la base de la columna.
|
# Ancoratge "feet": el tile [col, row] del spawn és la base de la columna.
|
||||||
# El sprite de 16×32 sobresurt 16 px cap amunt sense afectar col·lisió ni
|
# El sprite de 16×32 sobresurt 16 px cap amunt sense afectar col·lisió ni
|
||||||
@@ -19,4 +19,4 @@ flags:
|
|||||||
collides: true
|
collides: true
|
||||||
interactable: false
|
interactable: false
|
||||||
animations:
|
animations:
|
||||||
default: { frames: [0], fps: 1, loop: false }
|
default: { frames: [0], frame_ms: 1000, loop: false }
|
||||||
@@ -21,9 +21,9 @@ flags:
|
|||||||
collides: true
|
collides: true
|
||||||
interactable: true
|
interactable: true
|
||||||
animations:
|
animations:
|
||||||
closed: { frames: [1], fps: 1, loop: true }
|
closed: { frames: [1], frame_ms: 1000, loop: true }
|
||||||
opening: { frames: [0], fps: 6, loop: false }
|
opening: { frames: [0], frame_ms: 167, loop: false }
|
||||||
open: { frames: [0], fps: 1, loop: true }
|
open: { frames: [0], frame_ms: 1000, loop: true }
|
||||||
behaviour:
|
behaviour:
|
||||||
states: [CLOSED, OPENING, OPEN]
|
states: [CLOSED, OPENING, OPEN]
|
||||||
initial_state: CLOSED
|
initial_state: CLOSED
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ flags:
|
|||||||
# apropiada via `setVariant` abans del primer tick.
|
# apropiada via `setVariant` abans del primer tick.
|
||||||
# jump: frames 0..6 (7 frames) → reproduït al despegue.
|
# jump: frames 0..6 (7 frames) → reproduït al despegue.
|
||||||
# land: frames 7..12 (6 frames) → reproduït a l'aterratge.
|
# land: frames 7..12 (6 frames) → reproduït a l'aterratge.
|
||||||
# fps: 10 = 100 ms per frame.
|
# frame_ms: 67 ≈ 15 fps (1000 / 67 ≈ 14.9 fps).
|
||||||
animations:
|
animations:
|
||||||
jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 15, loop: false }
|
jump: { frames: [0, 1, 2, 3, 4, 5, 6], frame_ms: 67, loop: false }
|
||||||
land: { frames: [7, 8, 9, 10, 11, 12], fps: 15, loop: false }
|
land: { frames: [7, 8, 9, 10, 11, 12], frame_ms: 67, loop: false }
|
||||||
@@ -13,8 +13,8 @@ flags:
|
|||||||
collides: false
|
collides: false
|
||||||
interactable: false
|
interactable: false
|
||||||
animations:
|
animations:
|
||||||
inactive: { frames: [0], fps: 1, loop: true }
|
inactive: { frames: [0], frame_ms: 1000, loop: true }
|
||||||
active: { frames: [0], fps: 6, loop: true }
|
active: { frames: [0], frame_ms: 167, loop: true }
|
||||||
cycle:
|
cycle:
|
||||||
active_duration: 0.3
|
active_duration: 0.3
|
||||||
inactive_duration: 2.0
|
inactive_duration: 2.0
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ flags:
|
|||||||
# lectura genèrica de flags.
|
# lectura genèrica de flags.
|
||||||
triggers_on_overlap: true
|
triggers_on_overlap: true
|
||||||
animations:
|
animations:
|
||||||
idle: { frames: [0], fps: 1, loop: true }
|
idle: { frames: [0], frame_ms: 1000, loop: true }
|
||||||
stats:
|
stats:
|
||||||
damage: 0
|
damage: 0
|
||||||
@@ -32,11 +32,18 @@ flags:
|
|||||||
# no decideix flip per direcció (només selecciona quina animació mostrar).
|
# no decideix flip per direcció (només selecciona quina animació mostrar).
|
||||||
# La momia no té variants `stand_*` perquè mai no para.
|
# La momia no té variants `stand_*` perquè mai no para.
|
||||||
animations:
|
animations:
|
||||||
walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
walk_south: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true }
|
||||||
walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true }
|
walk_north: { frames: [4, 5, 6, 7], frame_ms: 125, loop: true }
|
||||||
walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true }
|
walk_east: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true }
|
||||||
walk_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true }
|
walk_west: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true, flip_h: true }
|
||||||
raise: { frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], fps: 8 , loop: false }
|
# Alçament: els 15 primers frames passen ràpid (125 ms cadascun) i el
|
||||||
|
# darrer aguanta 1 s perquè el jugador pugui veure la mòmia ja alçada
|
||||||
|
# abans que comenci a deambular. `frame_ms` accepta una llista per-frame
|
||||||
|
# que mapeja 1:1 contra `frames`.
|
||||||
|
raise:
|
||||||
|
frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
|
||||||
|
frame_ms: [125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 1000]
|
||||||
|
loop: false
|
||||||
ai:
|
ai:
|
||||||
type: corridor_wanderer
|
type: corridor_wanderer
|
||||||
direction_choice: random
|
direction_choice: random
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ flags:
|
|||||||
collides: true
|
collides: true
|
||||||
interactable: false
|
interactable: false
|
||||||
animations:
|
animations:
|
||||||
roll: { frames: [0], fps: 10, loop: true }
|
roll: { frames: [0], frame_ms: 100, loop: true }
|
||||||
ai:
|
ai:
|
||||||
type: straight_line
|
type: straight_line
|
||||||
initial_direction: right
|
initial_direction: right
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ flags:
|
|||||||
collides: true
|
collides: true
|
||||||
interactable: true
|
interactable: true
|
||||||
animations:
|
animations:
|
||||||
closed: { frames: [0], fps: 1, loop: true }
|
closed: { frames: [0], frame_ms: 1000, loop: true }
|
||||||
opening: { frames: [1, 2], fps: 6, loop: false }
|
opening: { frames: [1, 2], frame_ms: 167, loop: false }
|
||||||
open: { frames: [3], fps: 1, loop: true }
|
open: { frames: [3], frame_ms: 1000, loop: true }
|
||||||
behaviour:
|
behaviour:
|
||||||
states: [CLOSED, OPENING, OPEN]
|
states: [CLOSED, OPENING, OPEN]
|
||||||
initial_state: CLOSED
|
initial_state: CLOSED
|
||||||
|
|||||||
File renamed without changes.
@@ -6,7 +6,7 @@ editable: true
|
|||||||
# instància col·locada.
|
# instància col·locada.
|
||||||
editable_variants: true
|
editable_variants: true
|
||||||
sprite:
|
sprite:
|
||||||
path: ../debris/stone.gif
|
path: stone.gif
|
||||||
size: [16, 16]
|
size: [16, 16]
|
||||||
tile_size: [1, 1]
|
tile_size: [1, 1]
|
||||||
collision:
|
collision:
|
||||||
@@ -19,4 +19,4 @@ flags:
|
|||||||
# Animació "default" amb un sol frame: el spawn YAML pot sobreescriure-la
|
# Animació "default" amb un sol frame: el spawn YAML pot sobreescriure-la
|
||||||
# via `frame: N` per fixar la variant escollida al editor.
|
# via `frame: N` per fixar la variant escollida al editor.
|
||||||
animations:
|
animations:
|
||||||
default: { frames: [0], fps: 1, loop: false }
|
default: { frames: [0], frame_ms: 1000, loop: false }
|
||||||
@@ -15,15 +15,19 @@ flags:
|
|||||||
# El torch.gif és un strip vertical 16x64 amb 4 frames apilats.
|
# El torch.gif és un strip vertical 16x64 amb 4 frames apilats.
|
||||||
# El render clipa cada frame per `anim.currentFrame()` × 16 píxels en Y.
|
# El render clipa cada frame per `anim.currentFrame()` × 16 píxels en Y.
|
||||||
animations:
|
animations:
|
||||||
idle: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
idle: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true }
|
||||||
|
|
||||||
# Cercle de llum centrat a l'antorxa. El radi és en píxels del canvas;
|
# Cercle de llum centrat a l'antorxa. El radi és en píxels del canvas;
|
||||||
# el gradient global (data/config/lighting.yaml) hi posa la forma (3
|
# el gradient global (data/config/lighting.yaml) hi posa la forma (3
|
||||||
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame
|
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame
|
||||||
# (suma de dues sinusoides amb fase derivada de la posició) per a
|
# (suma de dues sinusoides amb fase derivada de la posició) per a
|
||||||
# que el cercle "respiri" com una flama; els paràmetres del flicker
|
# que el cercle "respiri" com una flama; els paràmetres del flicker
|
||||||
# també viuen al config global. Omet el bloc per fer una antorxa
|
# també viuen al config global. `offset: [x, y]` desplaça el centre
|
||||||
# "apagada".
|
# del cercle: la flama del sprite no està al centre del bounding box
|
||||||
|
# sinó cap a dalt, així que pujem la llum 4 px (Y creix cap avall)
|
||||||
|
# per a que el halo "neixi" de la flama, no del peu. Omet el bloc
|
||||||
|
# per fer una antorxa "apagada".
|
||||||
light:
|
light:
|
||||||
radius: 40
|
radius: 40
|
||||||
flicker: true
|
flicker: true
|
||||||
|
offset: [0, -4]
|
||||||
@@ -13,8 +13,8 @@ flags:
|
|||||||
collides: false
|
collides: false
|
||||||
interactable: false
|
interactable: false
|
||||||
animations:
|
animations:
|
||||||
inactive: { frames: [0], fps: 1, loop: true }
|
inactive: { frames: [0], frame_ms: 1000, loop: true }
|
||||||
active: { frames: [0], fps: 6, loop: true }
|
active: { frames: [0], frame_ms: 167, loop: true }
|
||||||
cycle:
|
cycle:
|
||||||
active_duration: 0.5
|
active_duration: 0.5
|
||||||
inactive_duration: 1.5
|
inactive_duration: 1.5
|
||||||
|
|||||||
@@ -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
|
||||||
+10
-64
@@ -1,39 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
|
room: 2
|
||||||
# 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ó base de la sala (0..1). 1.0 = sala totalment il·luminada
|
|
||||||
# (com hui, sense efecte visual); 0.0 = completament negra fora del radi
|
|
||||||
# d'un emissor. El gradient dels cercles d'emissor viu a
|
|
||||||
# data/config/lighting.yaml. Omet aquesta clau per deixar la sala neta.
|
|
||||||
light_level: 0.25
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
@@ -88,12 +53,16 @@ data:
|
|||||||
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
||||||
# superior esquerre per verificar render d'una entitat decorativa.
|
# superior esquerre per verificar render d'una entitat decorativa.
|
||||||
entities:
|
entities:
|
||||||
|
- type: torch
|
||||||
|
position: [4, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [8, 2]
|
position: [8, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [12, 2]
|
position: [12, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [16, 2]
|
position: [16, 2]
|
||||||
|
- type: sarcophagus
|
||||||
|
sarc_position: [0, 0]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [1, 0]
|
sarc_position: [1, 0]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
@@ -120,6 +89,8 @@ entities:
|
|||||||
sarc_position: [0, 3]
|
sarc_position: [0, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [1, 3]
|
sarc_position: [1, 3]
|
||||||
|
- type: sarcophagus
|
||||||
|
sarc_position: [2, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 3]
|
sarc_position: [3, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
@@ -130,33 +101,8 @@ entities:
|
|||||||
sarc_position: [2, 4]
|
sarc_position: [2, 4]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 4]
|
sarc_position: [3, 4]
|
||||||
- type: torch
|
|
||||||
position: [4, 2]
|
|
||||||
- type: door
|
- type: door
|
||||||
- type: column
|
|
||||||
position: [3, 4]
|
|
||||||
frame: 2
|
|
||||||
- type: stone
|
|
||||||
position: [5, 4]
|
|
||||||
frame: 16
|
|
||||||
- type: stone
|
|
||||||
position: [4, 4]
|
|
||||||
frame: 19
|
|
||||||
- type: column
|
|
||||||
position: [13, 10]
|
|
||||||
frame: 4
|
|
||||||
- type: stone
|
|
||||||
position: [12, 10]
|
|
||||||
frame: 13
|
|
||||||
- type: stone
|
|
||||||
position: [11, 10]
|
|
||||||
frame: 23
|
|
||||||
- type: hole
|
|
||||||
position: [5, 9]
|
|
||||||
- type: hole
|
|
||||||
position: [9, 11]
|
|
||||||
- type: stone
|
|
||||||
position: [10, 10]
|
|
||||||
frame: 18
|
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 13]
|
position: [10, 7]
|
||||||
|
- type: mummy
|
||||||
|
position: [7, 9]
|
||||||
+66
-38
@@ -1,33 +1,42 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
|
room: 1
|
||||||
# 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
|
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
|
||||||
|
# valors declarats al seu YAML de tipus (comportament històric).
|
||||||
#
|
#
|
||||||
# Format del tilemap:
|
# level → atenuació base de la sala (0..1). 1.0 = res
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
# d'efecte, 0.0 = negre fora del radi dels emissors.
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
# El gradient dels emissors viu a
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
# data/config/lighting.yaml.
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
# emitters.<tipus> → override per tipus d'entitat. Tres formes:
|
||||||
|
# false → apaga el tipus
|
||||||
pyramid: 1
|
# true → defaults del tipus
|
||||||
room: 2
|
# { radius: N, flicker: B } → camps presents
|
||||||
tileset: data/tilesets/pyramid_1.gif
|
# sobreescriuen el
|
||||||
tile_size: 16
|
# default; els
|
||||||
origin: [-8, 0]
|
# absents cauen al
|
||||||
|
# default del tipus.
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
# Plural per llegir-se natural ("totes les momies de la sala"). Per a
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
# casos particulars, un spawn pot dur el seu propi `light:` (mateixes
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
# formes) que té prioritat sobre l'override de sala. La forma antiga
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
# (`light_level:` plà a l'arrel) també segueix sent vàlida per
|
||||||
# per desactivar el pintat per a aquest jugador.
|
# retrocompatibilitat.
|
||||||
player1_paint_tile: 0
|
lighting:
|
||||||
player2_paint_tile: 1
|
# Sala fosca de proves. La piràmide declara additive per defecte; aquí
|
||||||
|
# tornem a subtractive perquè volem la dinàmica "negre amb forats de
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
# llum". `level: 0.15` és l'atenuació base de la sala fora dels emissors.
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
mode: subtractive
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
level: 0.15
|
||||||
# s'anul·len (sense push, sense moviment).
|
emitters:
|
||||||
push_factor: 0.30
|
players: true
|
||||||
|
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).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
@@ -82,16 +91,12 @@ data:
|
|||||||
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
||||||
# superior esquerre per verificar render d'una entitat decorativa.
|
# superior esquerre per verificar render d'una entitat decorativa.
|
||||||
entities:
|
entities:
|
||||||
- type: torch
|
|
||||||
position: [4, 2]
|
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [8, 2]
|
position: [8, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [12, 2]
|
position: [12, 2]
|
||||||
- type: torch
|
- type: torch
|
||||||
position: [16, 2]
|
position: [16, 2]
|
||||||
- type: sarcophagus
|
|
||||||
sarc_position: [0, 0]
|
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [1, 0]
|
sarc_position: [1, 0]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
@@ -118,8 +123,6 @@ entities:
|
|||||||
sarc_position: [0, 3]
|
sarc_position: [0, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [1, 3]
|
sarc_position: [1, 3]
|
||||||
- type: sarcophagus
|
|
||||||
sarc_position: [2, 3]
|
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 3]
|
sarc_position: [3, 3]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
@@ -130,8 +133,33 @@ entities:
|
|||||||
sarc_position: [2, 4]
|
sarc_position: [2, 4]
|
||||||
- type: sarcophagus
|
- type: sarcophagus
|
||||||
sarc_position: [3, 4]
|
sarc_position: [3, 4]
|
||||||
|
- type: torch
|
||||||
|
position: [4, 2]
|
||||||
- type: door
|
- type: door
|
||||||
|
- type: column
|
||||||
|
position: [3, 4]
|
||||||
|
frame: 2
|
||||||
|
- type: stone
|
||||||
|
position: [5, 4]
|
||||||
|
frame: 16
|
||||||
|
- type: stone
|
||||||
|
position: [4, 4]
|
||||||
|
frame: 19
|
||||||
|
- type: column
|
||||||
|
position: [13, 10]
|
||||||
|
frame: 4
|
||||||
|
- type: stone
|
||||||
|
position: [12, 10]
|
||||||
|
frame: 13
|
||||||
|
- type: stone
|
||||||
|
position: [11, 10]
|
||||||
|
frame: 23
|
||||||
|
- type: hole
|
||||||
|
position: [5, 9]
|
||||||
|
- type: hole
|
||||||
|
position: [9, 11]
|
||||||
|
- type: stone
|
||||||
|
position: [10, 10]
|
||||||
|
frame: 18
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 7]
|
position: [10, 13]
|
||||||
- type: mummy
|
|
||||||
position: [7, 9]
|
|
||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 1
|
|
||||||
room: 3
|
room: 3
|
||||||
tileset: data/tilesets/pyramid_1.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 1
|
|
||||||
room: 4
|
room: 4
|
||||||
tileset: data/tilesets/pyramid_1.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 1
|
|
||||||
room: 5
|
room: 5
|
||||||
tileset: data/tilesets/pyramid_1.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 1
|
|
||||||
room: 6
|
room: 6
|
||||||
tileset: data/tilesets/pyramid_1.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -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
|
room: 1
|
||||||
tileset: data/tilesets/pyramid_2.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 2
|
|
||||||
room: 2
|
room: 2
|
||||||
tileset: data/tilesets/pyramid_2.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 2
|
|
||||||
room: 3
|
room: 3
|
||||||
tileset: data/tilesets/pyramid_2.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 2
|
|
||||||
room: 4
|
room: 4
|
||||||
tileset: data/tilesets/pyramid_2.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 2
|
|
||||||
room: 5
|
room: 5
|
||||||
tileset: data/tilesets/pyramid_2.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_2.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 2
|
|
||||||
room: 6
|
room: 6
|
||||||
tileset: data/tilesets/pyramid_2.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -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
|
room: 1
|
||||||
tileset: data/tilesets/pyramid_3.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 3
|
|
||||||
room: 2
|
room: 2
|
||||||
tileset: data/tilesets/pyramid_3.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 3
|
|
||||||
room: 3
|
room: 3
|
||||||
tileset: data/tilesets/pyramid_3.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 3
|
|
||||||
room: 4
|
room: 4
|
||||||
tileset: data/tilesets/pyramid_3.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 3
|
|
||||||
room: 5
|
room: 5
|
||||||
tileset: data/tilesets/pyramid_3.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_3.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 3
|
|
||||||
room: 6
|
room: 6
|
||||||
tileset: data/tilesets/pyramid_3.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -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
|
room: 1
|
||||||
tileset: data/tilesets/pyramid_4.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 4
|
|
||||||
room: 2
|
room: 2
|
||||||
tileset: data/tilesets/pyramid_4.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 4
|
|
||||||
room: 3
|
room: 3
|
||||||
tileset: data/tilesets/pyramid_4.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 4
|
|
||||||
room: 4
|
room: 4
|
||||||
tileset: data/tilesets/pyramid_4.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 4
|
|
||||||
room: 5
|
room: 5
|
||||||
tileset: data/tilesets/pyramid_4.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_4.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 4
|
|
||||||
room: 6
|
room: 6
|
||||||
tileset: data/tilesets/pyramid_4.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -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
|
room: 1
|
||||||
tileset: data/tilesets/pyramid_5.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 5
|
|
||||||
room: 2
|
room: 2
|
||||||
tileset: data/tilesets/pyramid_5.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 5
|
|
||||||
room: 3
|
room: 3
|
||||||
tileset: data/tilesets/pyramid_5.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 5
|
|
||||||
room: 4
|
room: 4
|
||||||
tileset: data/tilesets/pyramid_5.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 5
|
|
||||||
room: 5
|
room: 5
|
||||||
tileset: data/tilesets/pyramid_5.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -1,33 +1,4 @@
|
|||||||
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_5.gif.
|
|
||||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
|
||||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
|
||||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
|
||||||
#
|
|
||||||
# Format del tilemap:
|
|
||||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
|
||||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
|
||||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
|
||||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
|
||||||
|
|
||||||
pyramid: 5
|
|
||||||
room: 6
|
room: 6
|
||||||
tileset: data/tilesets/pyramid_5.gif
|
|
||||||
tile_size: 16
|
|
||||||
origin: [-8, 0]
|
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
|
||||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
|
||||||
# per desactivar el pintat per a aquest jugador.
|
|
||||||
player1_paint_tile: 0
|
|
||||||
player2_paint_tile: 1
|
|
||||||
|
|
||||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
|
||||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
|
||||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
|
||||||
# s'anul·len (sense push, sense moviment).
|
|
||||||
push_factor: 0.30
|
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||||
|
|||||||
@@ -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 {
|
layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||||
float vignette_strength;
|
float vignette_strength;
|
||||||
float chroma_strength;
|
float chroma_min; // intensitat mínima de l'aberració cromàtica
|
||||||
float scanline_strength;
|
float scanline_strength;
|
||||||
float screen_height;
|
float screen_height;
|
||||||
float mask_strength;
|
float mask_strength;
|
||||||
@@ -27,7 +27,7 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
|
|||||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||||
float time; // seconds since SDL init
|
float time; // seconds since SDL init
|
||||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||||
float pad2; // padding per tancar vec4 #2
|
float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
|
||||||
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||||||
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
|
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
|
||||||
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
|
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
|
||||||
@@ -35,6 +35,19 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
|
|||||||
float pad3; // padding per tancar a 64 bytes (4 × vec4)
|
float pad3; // padding per tancar a 64 bytes (4 × vec4)
|
||||||
} u;
|
} u;
|
||||||
|
|
||||||
|
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||||||
|
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
|
||||||
|
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
|
||||||
|
float sampleBilinearX(vec2 uv_target, int channel) {
|
||||||
|
vec2 tex_size = vec2(textureSize(scene, 0));
|
||||||
|
float px = uv_target.x * tex_size.x - 0.5;
|
||||||
|
float p_floor = floor(px);
|
||||||
|
float f = px - p_floor;
|
||||||
|
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
|
||||||
|
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
|
||||||
|
return mix(c0[channel], c1[channel], f);
|
||||||
|
}
|
||||||
|
|
||||||
// YCbCr helpers for NTSC bleeding
|
// YCbCr helpers for NTSC bleeding
|
||||||
vec3 rgb_to_ycc(vec3 rgb) {
|
vec3 rgb_to_ycc(vec3 rgb) {
|
||||||
return vec3(
|
return vec3(
|
||||||
@@ -92,10 +105,14 @@ void main() {
|
|||||||
colour = base;
|
colour = base;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aberración cromática (drift animado con time para efecto NTSC real)
|
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
|
||||||
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3));
|
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
|
||||||
colour.r = texture(scene, uv + vec2(ca, 0.0)).r;
|
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
|
||||||
colour.b = texture(scene, uv - vec2(ca, 0.0)).b;
|
if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
|
||||||
|
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
|
||||||
|
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
|
||||||
|
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
|
||||||
|
}
|
||||||
|
|
||||||
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||||||
if (u.gamma_strength > 0.0) {
|
if (u.gamma_strength > 0.0) {
|
||||||
|
|||||||
@@ -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)
|
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::pauseMusic() {
|
void Audio::pauseMusic() {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||||
engine_->pauseMusic();
|
engine_->pauseMusic();
|
||||||
@@ -98,7 +97,6 @@ void Audio::pauseMusic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::resumeMusic() {
|
void Audio::resumeMusic() {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
||||||
engine_->resumeMusic();
|
engine_->resumeMusic();
|
||||||
@@ -106,14 +104,12 @@ void Audio::resumeMusic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::stopMusic() {
|
void Audio::stopMusic() {
|
||||||
if (music_enabled_) {
|
if (music_enabled_) {
|
||||||
engine_->stopMusic();
|
engine_->stopMusic();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const)
|
|
||||||
void Audio::setMusicSpeed(float ratio) {
|
void Audio::setMusicSpeed(float ratio) {
|
||||||
if (music_enabled_) {
|
if (music_enabled_) {
|
||||||
engine_->setMusicSpeed(ratio);
|
engine_->setMusicSpeed(ratio);
|
||||||
@@ -121,7 +117,6 @@ void Audio::setMusicSpeed(float ratio) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reprodueix un so per nom
|
// Reprodueix un so per nom
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::playSound(const std::string& name, Group group) {
|
void Audio::playSound(const std::string& name, Group group) {
|
||||||
if (sound_enabled_) {
|
if (sound_enabled_) {
|
||||||
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||||
@@ -129,7 +124,6 @@ void Audio::playSound(const std::string& name, Group group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reprodueix un so per punter directe
|
// Reprodueix un so per punter directe
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::playSound(Ja::Sound* sound, Group group) {
|
void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||||
if (sound_enabled_ && sound != nullptr) {
|
if (sound_enabled_ && sound != nullptr) {
|
||||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
@@ -141,7 +135,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
|||||||
// processa tot el sample amb el ratio des del primer pull del callback.
|
// processa tot el sample amb el ratio des del primer pull del callback.
|
||||||
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
||||||
// la crida al ratio — sense efectes col·laterals.
|
// la crida al ratio — sense efectes col·laterals.
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const)
|
|
||||||
void Audio::playSound(const std::string& name, Group group, float speed) {
|
void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||||
if (!sound_enabled_) { return; }
|
if (!sound_enabled_) { return; }
|
||||||
auto* sound = AudioResource::getSound(name);
|
auto* sound = AudioResource::getSound(name);
|
||||||
@@ -155,7 +148,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) {
|
|||||||
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
|
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
|
||||||
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
|
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
|
||||||
// sec — l'usuari sent el so encara que la cua no s'apliqui.
|
// sec — l'usuari sent el so encara que la cua no s'apliqui.
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
|
|
||||||
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
||||||
if (!sound_enabled_) { return; }
|
if (!sound_enabled_) { return; }
|
||||||
auto* sound = AudioResource::getSound(name);
|
auto* sound = AudioResource::getSound(name);
|
||||||
@@ -175,7 +167,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset
|
|||||||
|
|
||||||
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
||||||
// fallback que playSoundWithEcho.
|
// fallback que playSoundWithEcho.
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
|
|
||||||
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
||||||
if (!sound_enabled_) { return; }
|
if (!sound_enabled_) { return; }
|
||||||
auto* sound = AudioResource::getSound(name);
|
auto* sound = AudioResource::getSound(name);
|
||||||
@@ -194,7 +185,6 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Atura tots els sons
|
// Atura tots els sons
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::stopAllSounds() {
|
void Audio::stopAllSounds() {
|
||||||
if (sound_enabled_) {
|
if (sound_enabled_) {
|
||||||
engine_->stopChannel(-1);
|
engine_->stopChannel(-1);
|
||||||
@@ -202,7 +192,6 @@ void Audio::stopAllSounds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fa una fosa de sortida de la música
|
// Fa una fosa de sortida de la música
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::fadeOutMusic(int milliseconds) {
|
void Audio::fadeOutMusic(int milliseconds) {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||||
engine_->fadeOutMusic(milliseconds);
|
engine_->fadeOutMusic(milliseconds);
|
||||||
@@ -212,7 +201,6 @@ void Audio::fadeOutMusic(int milliseconds) {
|
|||||||
// Registra un callback que el motor dispararà quan la pista actual acabi de
|
// Registra un callback que el motor dispararà quan la pista actual acabi de
|
||||||
// drenar (times == 0 + stream buit). S'executa al mateix thread que
|
// drenar (times == 0 + stream buit). S'executa al mateix thread que
|
||||||
// Audio::update (render loop); els consumidors no poden fer I/O blocant.
|
// Audio::update (render loop); els consumidors no poden fer I/O blocant.
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
|
|
||||||
void Audio::setOnMusicEnded(std::function<void()> callback) {
|
void Audio::setOnMusicEnded(std::function<void()> callback) {
|
||||||
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
|
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
|
||||||
}
|
}
|
||||||
@@ -251,13 +239,11 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Estableix el volum dels sons (float 0.0..1.0)
|
// Estableix el volum dels sons (float 0.0..1.0)
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::setSoundVolume(float sound_volume, Group group) {
|
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||||
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estableix el volum de la música (float 0.0..1.0)
|
// Estableix el volum de la música (float 0.0..1.0)
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
|
||||||
void Audio::setMusicVolume(float music_volume) {
|
void Audio::setMusicVolume(float music_volume) {
|
||||||
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#include <functional> // Per a std::function
|
#include <functional> // Per a std::function
|
||||||
#include <memory> // Per a std::unique_ptr
|
#include <memory> // Per a std::unique_ptr
|
||||||
#include <string> // Per a string
|
#include <string> // Per a string
|
||||||
#include <utility> // Per a move
|
|
||||||
|
|
||||||
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
|
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
|
||||||
// tres símbols (Music/Sound per al punter que exposa la API i Engine per al
|
// tres símbols (Music/Sound per al punter que exposa la API i Engine per al
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
// (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi).
|
// (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi).
|
||||||
#define STB_VORBIS_HEADER_ONLY
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
// clang-format off
|
// clang-format off
|
||||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
// NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis és single-file: la macro de dalt limita aquest TU a només-declaracions; la implementació viu a external/stb_vorbis_impl.cpp.
|
||||||
#include "external/stb_vorbis.c"
|
#include "external/stb_vorbis.c"
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
|
|||||||
@@ -198,42 +198,50 @@ void DefineButtons::render() const {
|
|||||||
|
|
||||||
// Títol: nom del mando.
|
// Títol: nom del mando.
|
||||||
const std::string TITLE = pad_name_;
|
const std::string TITLE = pad_name_;
|
||||||
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
|
||||||
CENTER_X,
|
CENTER_X,
|
||||||
cursor_y,
|
cursor_y,
|
||||||
TITLE,
|
TITLE,
|
||||||
TITLE_KERNING,
|
TITLE_KERNING,
|
||||||
UiConfig::get().menuTitle());
|
UiConfig::get().menuTitle(),
|
||||||
|
1,
|
||||||
|
UiConfig::get().menuTitleOutline());
|
||||||
cursor_y += TITLE_H + LINE_GAP;
|
cursor_y += TITLE_H + LINE_GAP;
|
||||||
|
|
||||||
if (message_shown_) {
|
if (message_shown_) {
|
||||||
// Pantalla final: missatge de configuració completa.
|
// Pantalla final: missatge de configuració completa.
|
||||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
|
||||||
CENTER_X,
|
CENTER_X,
|
||||||
cursor_y,
|
cursor_y,
|
||||||
Locale::get().text("menu.values.define_buttons_complete"),
|
Locale::get().text("menu.values.define_buttons_complete"),
|
||||||
ITEM_KERNING,
|
ITEM_KERNING,
|
||||||
UiConfig::get().menuValue());
|
UiConfig::get().menuValue(),
|
||||||
|
1,
|
||||||
|
UiConfig::get().menuValueOutline());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instrucció + acció pendent. Replica visualment el WindowMessage de
|
// Instrucció + acció pendent. Replica visualment el WindowMessage de
|
||||||
// coffee_crisis_arcade_edition.
|
// coffee_crisis_arcade_edition.
|
||||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
|
||||||
CENTER_X,
|
CENTER_X,
|
||||||
cursor_y,
|
cursor_y,
|
||||||
Locale::get().text("menu.values.press_button"),
|
Locale::get().text("menu.values.press_button"),
|
||||||
ITEM_KERNING,
|
ITEM_KERNING,
|
||||||
UiConfig::get().menuLabel());
|
UiConfig::get().menuLabel(),
|
||||||
|
1,
|
||||||
|
UiConfig::get().menuLabelOutline());
|
||||||
cursor_y += ITEM_H + LINE_GAP;
|
cursor_y += ITEM_H + LINE_GAP;
|
||||||
|
|
||||||
if (index_ < sequence_.size()) {
|
if (index_ < sequence_.size()) {
|
||||||
const auto& current = sequence_[index_];
|
const auto& current = sequence_[index_];
|
||||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
|
||||||
CENTER_X,
|
CENTER_X,
|
||||||
cursor_y,
|
cursor_y,
|
||||||
Locale::get().text(current.label_key),
|
Locale::get().text(current.label_key),
|
||||||
ITEM_KERNING,
|
ITEM_KERNING,
|
||||||
UiConfig::get().menuCursor());
|
UiConfig::get().menuCursor(),
|
||||||
|
1,
|
||||||
|
UiConfig::get().menuCursorOutline());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "core/input/gamepad_bindings.hpp"
|
#include "core/input/gamepad_bindings.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|||||||
@@ -14,9 +14,8 @@
|
|||||||
//
|
//
|
||||||
// El polling real de botons i eixos NO es fa a través d'aquesta classe:
|
// El polling real de botons i eixos NO es fa a través d'aquesta classe:
|
||||||
// els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a
|
// els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a
|
||||||
// multi-jugador futur) i el passen a `InputState::getGamepadButton/Axis`,
|
// multi-jugador futur) i el passen directament a
|
||||||
// que ho filtra pel flag de bloqueig de la UI (mateixa estratègia que
|
// `SDL_GetGamepadButton`/`SDL_GetGamepadAxis`.
|
||||||
// amb el teclat).
|
|
||||||
class GamepadManager {
|
class GamepadManager {
|
||||||
public:
|
public:
|
||||||
static auto get() -> GamepadManager&;
|
static auto get() -> GamepadManager&;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
#include "core/input/key_config.hpp"
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/notification_presets.hpp"
|
#include "core/rendering/notification_presets.hpp"
|
||||||
#include "core/rendering/notifier.hpp"
|
|
||||||
#include "core/rendering/render_info.hpp"
|
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/rendering/screenshot.hpp"
|
#include "core/rendering/screenshot.hpp"
|
||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
|
|||||||
@@ -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 "core/input/key_config.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
class Surface;
|
class Surface;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include "core/rendering/surface.hpp"
|
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
|
||||||
namespace Effects::Shadow {
|
namespace Effects::Shadow {
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ namespace Lighting {
|
|||||||
return buf_;
|
return buf_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuració global, inicialitzada amb els defaults compilats.
|
// Corba activa del mode subtractive. loadConfig() la reemplaça
|
||||||
// loadConfig() la reemplaça si troba el fitxer.
|
// si troba el bloc `gradient:` al YAML.
|
||||||
auto activeConfig() -> GradientConfig& {
|
auto activeSubtractiveConfig() -> GradientConfig& {
|
||||||
static GradientConfig cfg_{
|
static GradientConfig cfg_{
|
||||||
.inner_ratio = Defaults::Lighting::INNER_RATIO,
|
.inner_ratio = Defaults::Lighting::INNER_RATIO,
|
||||||
.mid_ratio = Defaults::Lighting::MID_RATIO,
|
.mid_ratio = Defaults::Lighting::MID_RATIO,
|
||||||
@@ -39,11 +39,27 @@ namespace Lighting {
|
|||||||
return cfg_;
|
return cfg_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Corba activa del mode additive. Defaults baixos a propòsit:
|
||||||
|
// en additive la intensitat és l'alfa del color afegit, així
|
||||||
|
// que valors petits (0.05..0.15) donen el "halo subtil" que
|
||||||
|
// sol voler-se. loadConfig() la reemplaça si troba el bloc
|
||||||
|
// `gradient_additive:` al YAML.
|
||||||
|
auto activeAdditiveConfig() -> GradientConfig& {
|
||||||
|
static GradientConfig cfg_{
|
||||||
|
.inner_ratio = Defaults::Lighting::INNER_RATIO_ADDITIVE,
|
||||||
|
.mid_ratio = Defaults::Lighting::MID_RATIO_ADDITIVE,
|
||||||
|
.inner_intensity = Defaults::Lighting::INNER_INTENSITY_ADDITIVE,
|
||||||
|
.mid_intensity = Defaults::Lighting::MID_INTENSITY_ADDITIVE,
|
||||||
|
.outer_intensity = Defaults::Lighting::OUTER_INTENSITY_ADDITIVE,
|
||||||
|
};
|
||||||
|
return cfg_;
|
||||||
|
}
|
||||||
|
|
||||||
auto activeFlicker() -> FlickerConfig& {
|
auto activeFlicker() -> FlickerConfig& {
|
||||||
static FlickerConfig fc_{
|
static FlickerConfig fc_{
|
||||||
.wave_amplitude_px = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_PX,
|
.wave_amplitude_ratio = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_RATIO,
|
||||||
.wave_frequency_hz = Defaults::Lighting::FLICKER_WAVE_FREQUENCY_HZ,
|
.wave_frequency_hz = Defaults::Lighting::FLICKER_WAVE_FREQUENCY_HZ,
|
||||||
.noise_amplitude_px = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_PX,
|
.noise_amplitude_ratio = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_RATIO,
|
||||||
.noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ,
|
.noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ,
|
||||||
};
|
};
|
||||||
return fc_;
|
return fc_;
|
||||||
@@ -54,13 +70,16 @@ namespace Lighting {
|
|||||||
// identificador estable de l'emissor (no canvia amb la posició) —
|
// identificador estable de l'emissor (no canvia amb la posició) —
|
||||||
// així el patró no "deriva" quan l'emissor es mou. El factor
|
// així el patró no "deriva" quan l'emissor es mou. El factor
|
||||||
// ~φ (golden ratio) dispersa les fases per a seeds consecutives.
|
// ~φ (golden ratio) dispersa les fases per a seeds consecutives.
|
||||||
|
// Les amplituds són ràtios sobre el radi base, multiplicades aquí
|
||||||
|
// → mateix percentatge d'oscil·lació per a qualsevol mida.
|
||||||
auto computeFlickerRadius(int base_radius, int seed, float t_s, const FlickerConfig& fc) -> int {
|
auto computeFlickerRadius(int base_radius, int seed, float t_s, const FlickerConfig& fc) -> int {
|
||||||
constexpr float TWO_PI = 2.0F * std::numbers::pi_v<float>;
|
constexpr float TWO_PI = 2.0F * std::numbers::pi_v<float>;
|
||||||
constexpr float GOLDEN_FRACTION = 0.6180339F;
|
constexpr float GOLDEN_FRACTION = 0.6180339F;
|
||||||
const float PHASE = static_cast<float>(seed) * GOLDEN_FRACTION;
|
const float PHASE = static_cast<float>(seed) * GOLDEN_FRACTION;
|
||||||
const float W1 = std::sin((TWO_PI * fc.wave_frequency_hz * t_s) + PHASE);
|
const float W1 = std::sin((TWO_PI * fc.wave_frequency_hz * t_s) + PHASE);
|
||||||
const float W2 = std::sin((TWO_PI * fc.noise_frequency_hz * t_s) + (PHASE * 2.7F));
|
const float W2 = std::sin((TWO_PI * fc.noise_frequency_hz * t_s) + (PHASE * 2.7F));
|
||||||
const float DELTA = (fc.wave_amplitude_px * W1) + (fc.noise_amplitude_px * W2);
|
const auto R_F = static_cast<float>(base_radius);
|
||||||
|
const float DELTA = (fc.wave_amplitude_ratio * R_F * W1) + (fc.noise_amplitude_ratio * R_F * W2);
|
||||||
const int R = base_radius + static_cast<int>(std::lround(DELTA));
|
const int R = base_radius + static_cast<int>(std::lround(DELTA));
|
||||||
return std::max(1, R);
|
return std::max(1, R);
|
||||||
}
|
}
|
||||||
@@ -133,9 +152,9 @@ namespace Lighting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pasada final: pixel = pixel * lum / 255 per canal. El canal alpha
|
// Pasada final del mode subtractive: pixel = pixel * lum / 255
|
||||||
// es preserva (la resta del pipeline assumeix opacitat total al
|
// per canal. El canal alpha es preserva (la resta del pipeline
|
||||||
// canvas; veure Rendering::OPAQUE_ALPHA).
|
// assumeix opacitat total al canvas; veure Rendering::OPAQUE_ALPHA).
|
||||||
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
|
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
|
||||||
for (int y = 0; y < height; ++y) {
|
for (int y = 0; y < height; ++y) {
|
||||||
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||||
@@ -158,12 +177,100 @@ namespace Lighting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atenuació ambient del mode additive: aplica `pixel × level` per
|
||||||
|
// canvas abans del bucle d'emissors. Si `level == 255` (sala a
|
||||||
|
// 1.0) salta com a no-op — el cas comú quan només volem el tint
|
||||||
|
// dels emissors sense oscurir la sala.
|
||||||
|
void darkenCanvas(std::uint32_t* canvas, int width, int height, std::uint8_t level) {
|
||||||
|
if (level == 255) { return; }
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const std::uint32_t SRC = px[x];
|
||||||
|
const std::uint8_t SR = (SRC >> 16) & 0xFF;
|
||||||
|
const std::uint8_t SG = (SRC >> 8) & 0xFF;
|
||||||
|
const std::uint8_t SB = SRC & 0xFF;
|
||||||
|
const auto R = static_cast<std::uint8_t>((SR * level) / 255);
|
||||||
|
const auto G = static_cast<std::uint8_t>((SG * level) / 255);
|
||||||
|
const auto B = static_cast<std::uint8_t>((SB * level) / 255);
|
||||||
|
px[x] = (SRC & 0xFF000000U) |
|
||||||
|
(static_cast<std::uint32_t>(R) << 16) |
|
||||||
|
(static_cast<std::uint32_t>(G) << 8) |
|
||||||
|
static_cast<std::uint32_t>(B);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pasada del mode additive per a un emissor: suma `color × intensity`
|
||||||
|
// a cada píxel cobert pel cercle, saturant a 255 per canal. A
|
||||||
|
// diferència del subtractive, NO usa buffer intermig (no cal max())
|
||||||
|
// perquè la suma és commutativa; emissors solapats acumulen
|
||||||
|
// contribucions de manera natural. Si dues llums de colors diferents
|
||||||
|
// es solapen, el resultat tendeix al blanc en la zona de coincidència
|
||||||
|
// (suma física): el solape s'aclareix més que un sol emissor.
|
||||||
|
void rasterizeEmitterAdditive(const Emitter& e, const EmitterRings& rings, std::uint32_t* canvas, int width, int height) {
|
||||||
|
const int R = e.radius;
|
||||||
|
const int CX = static_cast<int>(e.cx);
|
||||||
|
const int CY = static_cast<int>(e.cy);
|
||||||
|
const int X0 = std::max(0, CX - R);
|
||||||
|
const int Y0 = std::max(0, CY - R);
|
||||||
|
const int X1 = std::min(width - 1, CX + R);
|
||||||
|
const int Y1 = std::min(height - 1, CY + R);
|
||||||
|
for (int y = Y0; y <= Y1; ++y) {
|
||||||
|
const int DY = y - CY;
|
||||||
|
const int DY_SQ = DY * DY;
|
||||||
|
if (DY_SQ > rings.radius_sq) { continue; }
|
||||||
|
std::uint32_t* row = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||||
|
for (int x = X0; x <= X1; ++x) {
|
||||||
|
const int DX = x - CX;
|
||||||
|
const int DIST_SQ = (DX * DX) + DY_SQ;
|
||||||
|
if (DIST_SQ > rings.radius_sq) { continue; }
|
||||||
|
std::uint8_t intensity = rings.outer;
|
||||||
|
if (DIST_SQ <= rings.inner_sq) {
|
||||||
|
intensity = rings.inner;
|
||||||
|
} else if (DIST_SQ <= rings.mid_sq) {
|
||||||
|
intensity = rings.mid;
|
||||||
|
}
|
||||||
|
// contrib_channel = emitter_color_channel × intensity / 255
|
||||||
|
// (intensity ja és 0..255 quantitzat a `toByte`).
|
||||||
|
const int CR = (e.color.r * intensity) / 255;
|
||||||
|
const int CG = (e.color.g * intensity) / 255;
|
||||||
|
const int CB = (e.color.b * intensity) / 255;
|
||||||
|
const std::uint32_t SRC = row[x];
|
||||||
|
const int SR = static_cast<int>((SRC >> 16) & 0xFF);
|
||||||
|
const int SG = static_cast<int>((SRC >> 8) & 0xFF);
|
||||||
|
const int SB = static_cast<int>(SRC & 0xFF);
|
||||||
|
const auto NR = static_cast<std::uint32_t>(std::min(255, SR + CR));
|
||||||
|
const auto NG = static_cast<std::uint32_t>(std::min(255, SG + CG));
|
||||||
|
const auto NB = static_cast<std::uint32_t>(std::min(255, SB + CB));
|
||||||
|
row[x] = (SRC & 0xFF000000U) | (NR << 16) | (NG << 8) | NB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto config() -> const GradientConfig& {
|
auto config() -> const GradientConfig& {
|
||||||
return activeConfig();
|
return activeSubtractiveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto flickerConfig() -> const FlickerConfig& {
|
||||||
|
return activeFlicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Llegeix els 5 camps de `GradientConfig` d'un node YAML — qualsevol
|
||||||
|
// camp absent conserva el valor compilat dels defaults. Compartit
|
||||||
|
// entre els blocs `gradient:` i `gradient_additive:`.
|
||||||
|
void readGradient(const fkyaml::node& g, GradientConfig& cfg) {
|
||||||
|
readField(g, "inner_ratio", cfg.inner_ratio);
|
||||||
|
readField(g, "mid_ratio", cfg.mid_ratio);
|
||||||
|
readField(g, "inner_intensity", cfg.inner_intensity);
|
||||||
|
readField(g, "mid_intensity", cfg.mid_intensity);
|
||||||
|
readField(g, "outer_intensity", cfg.outer_intensity);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void loadConfig(const std::string& yaml_path) {
|
void loadConfig(const std::string& yaml_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
if (bytes.empty()) {
|
if (bytes.empty()) {
|
||||||
@@ -175,20 +282,17 @@ namespace Lighting {
|
|||||||
const auto* end = begin + bytes.size();
|
const auto* end = begin + bytes.size();
|
||||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
if (yaml.contains("gradient")) {
|
if (yaml.contains("gradient")) {
|
||||||
const auto& g = yaml["gradient"];
|
readGradient(yaml["gradient"], activeSubtractiveConfig());
|
||||||
auto& cfg = activeConfig();
|
}
|
||||||
readField(g, "inner_ratio", cfg.inner_ratio);
|
if (yaml.contains("gradient_additive")) {
|
||||||
readField(g, "mid_ratio", cfg.mid_ratio);
|
readGradient(yaml["gradient_additive"], activeAdditiveConfig());
|
||||||
readField(g, "inner_intensity", cfg.inner_intensity);
|
|
||||||
readField(g, "mid_intensity", cfg.mid_intensity);
|
|
||||||
readField(g, "outer_intensity", cfg.outer_intensity);
|
|
||||||
}
|
}
|
||||||
if (yaml.contains("flicker")) {
|
if (yaml.contains("flicker")) {
|
||||||
const auto& f = yaml["flicker"];
|
const auto& f = yaml["flicker"];
|
||||||
auto& fc = activeFlicker();
|
auto& fc = activeFlicker();
|
||||||
readField(f, "wave_amplitude_px", fc.wave_amplitude_px);
|
readField(f, "wave_amplitude_ratio", fc.wave_amplitude_ratio);
|
||||||
readField(f, "wave_frequency_hz", fc.wave_frequency_hz);
|
readField(f, "wave_frequency_hz", fc.wave_frequency_hz);
|
||||||
readField(f, "noise_amplitude_px", fc.noise_amplitude_px);
|
readField(f, "noise_amplitude_ratio", fc.noise_amplitude_ratio);
|
||||||
readField(f, "noise_frequency_hz", fc.noise_frequency_hz);
|
readField(f, "noise_frequency_hz", fc.noise_frequency_hz);
|
||||||
}
|
}
|
||||||
} catch (const std::exception& ex) {
|
} catch (const std::exception& ex) {
|
||||||
@@ -196,39 +300,77 @@ namespace Lighting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> emitters) {
|
namespace {
|
||||||
if (canvas == nullptr || width <= 0 || height <= 0) { return; }
|
|
||||||
|
|
||||||
// Atajo: si la sala està totalment il·luminada i no hi ha
|
// Helper: emissor "patxat" amb el radi efectiu (post-flicker) per
|
||||||
// emissors, la pasada seria un `pixel * 1.0` global — no cal
|
// a aquest frame. Mantenim l'`Emitter` del caller intacte i la
|
||||||
// tocar res. És el camí curt habitual mentre no posem
|
// resta de camps (color, seed, flicker_params, ...) propagats.
|
||||||
// `light_level` a les sales.
|
auto withEffectiveRadius(const Emitter& e, int r_eff) -> Emitter {
|
||||||
const std::uint8_t ROOM = toByte(room_light);
|
return Emitter{
|
||||||
if (ROOM == 255 && emitters.empty()) { return; }
|
.cx = e.cx,
|
||||||
|
.cy = e.cy,
|
||||||
|
.radius = r_eff,
|
||||||
|
.seed = e.seed,
|
||||||
|
.flicker = e.flicker,
|
||||||
|
.color = e.color,
|
||||||
|
.flicker_params = e.flicker_params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camí del mode subtractive (històric): omple un buffer de
|
||||||
|
// luminància amb `max(room, emitter_intensity)` i finalment
|
||||||
|
// multiplica el canvas per ell. Color de l'emissor ignorat.
|
||||||
|
void applySubtractive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span<const Emitter> emitters) {
|
||||||
auto& lum = luminanceBuffer();
|
auto& lum = luminanceBuffer();
|
||||||
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||||
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
|
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
|
||||||
std::ranges::fill(lum, ROOM);
|
std::ranges::fill(lum, room);
|
||||||
|
|
||||||
const auto& cfg = activeConfig();
|
const auto& cfg = activeSubtractiveConfig();
|
||||||
const auto& fc = activeFlicker();
|
|
||||||
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||||
for (const auto& e : emitters) {
|
for (const auto& e : emitters) {
|
||||||
if (e.radius <= 0) { continue; }
|
if (e.radius <= 0) { continue; }
|
||||||
const int R_EFF = e.flicker
|
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius;
|
||||||
? computeFlickerRadius(e.radius, e.seed, T_S, fc)
|
|
||||||
: e.radius;
|
|
||||||
const auto RINGS = computeRings(R_EFF, cfg);
|
const auto RINGS = computeRings(R_EFF, cfg);
|
||||||
// El rasteritzador llegeix el radi de l'`Emitter` per al
|
const Emitter EFF = withEffectiveRadius(e, R_EFF);
|
||||||
// càlcul de la bounding box, així que li passem un emissor
|
|
||||||
// "patxat" amb el radi efectiu d'aquest frame. Mantenim el
|
|
||||||
// valor original al `Emitter` del caller intacte.
|
|
||||||
const Emitter EFF{.cx = e.cx, .cy = e.cy, .radius = R_EFF, .seed = e.seed, .flicker = e.flicker};
|
|
||||||
rasterizeEmitter(EFF, RINGS, lum.data(), width, height);
|
rasterizeEmitter(EFF, RINGS, lum.data(), width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
modulateCanvas(canvas, lum.data(), width, height);
|
modulateCanvas(canvas, lum.data(), width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Camí del mode additive: atenua el canvas per `room` (si <255)
|
||||||
|
// i després suma `color × intensity` per emissor directament al
|
||||||
|
// canvas, sense buffer intermig.
|
||||||
|
void applyAdditive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span<const Emitter> emitters) {
|
||||||
|
darkenCanvas(canvas, width, height, room);
|
||||||
|
const auto& cfg = activeAdditiveConfig();
|
||||||
|
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||||
|
for (const auto& e : emitters) {
|
||||||
|
if (e.radius <= 0) { continue; }
|
||||||
|
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius;
|
||||||
|
const auto RINGS = computeRings(R_EFF, cfg);
|
||||||
|
const Emitter EFF = withEffectiveRadius(e, R_EFF);
|
||||||
|
rasterizeEmitterAdditive(EFF, RINGS, canvas, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters) {
|
||||||
|
if (canvas == nullptr || width <= 0 || height <= 0) { return; }
|
||||||
|
|
||||||
|
// Atajo per a tots dos modes: sala totalment "neutra" + cap
|
||||||
|
// emissor → la pasada no canviaria res al canvas.
|
||||||
|
// subtractive: room=1.0 i sense emissors → pixel * 1.0 global.
|
||||||
|
// additive: room=1.0 i sense emissors → cap aportació.
|
||||||
|
const std::uint8_t ROOM = toByte(room_light);
|
||||||
|
if (ROOM == 255 && emitters.empty()) { return; }
|
||||||
|
|
||||||
|
if (mode == Mode::ADDITIVE) {
|
||||||
|
applyAdditive(canvas, width, height, ROOM, emitters);
|
||||||
|
} else {
|
||||||
|
applySubtractive(canvas, width, height, ROOM, emitters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Lighting
|
} // namespace Lighting
|
||||||
@@ -7,36 +7,64 @@
|
|||||||
// Sistema d'il·luminació CPU. Treballa sobre el mateix canvas ARGB del
|
// Sistema d'il·luminació CPU. Treballa sobre el mateix canvas ARGB del
|
||||||
// joc (320×240) que tot el pipeline de render: després que GameScene
|
// joc (320×240) que tot el pipeline de render: després que GameScene
|
||||||
// haja pintat tilemap, paint layer, sombres i actors, una pasada final
|
// haja pintat tilemap, paint layer, sombres i actors, una pasada final
|
||||||
// multiplica cada píxel del canvas per un factor de luminància 0..1
|
// modifica cada píxel del canvas en funció de la "llum de la sala" i
|
||||||
// derivat de la combinació de "llum de la sala" + cercles emissors al
|
// dels cercles emissors al voltant de certes entitats.
|
||||||
// voltant de certes entitats.
|
|
||||||
//
|
//
|
||||||
// Forma de cada emissor (radial, 3 anells configurables al fitxer
|
// Dos modes seleccionables per la sala (`lighting.mode` al YAML):
|
||||||
// data/config/lighting.yaml; veure `GradientConfig`):
|
//
|
||||||
|
// SUBTRACTIVE (mode històric, default si no es declara mode):
|
||||||
|
// out = src × max(room_light, max_emitter_intensity_at_pixel)
|
||||||
|
// Els emissors "trenquen" una capa fosca multiplicativa. Tot el
|
||||||
|
// trip cromàtic dels emissors s'ignora — només la intensitat
|
||||||
|
// compta. Pensat per a sales fosques (cripta, pirámide).
|
||||||
|
//
|
||||||
|
// ADDITIVE (mode nou):
|
||||||
|
// out = saturate( (src × room_light) + Σ (emitter_color × intensity) )
|
||||||
|
// El canvas s'enfosqueix opcionalment per `room_light` (atenuació
|
||||||
|
// ambient) i els emissors hi sumen color cap a sobre. Les intensitats
|
||||||
|
// dels anells es llegeixen com a "alfa" del color afegit (0..1).
|
||||||
|
// Pensat per a sales clares on les antorxes aporten halo cromàtic
|
||||||
|
// sense restar contrast — o per a sales lleument fosques (0.85, p.ex.)
|
||||||
|
// on el halo de l'antorxa es veu càlid en lloc de "blanquinós".
|
||||||
|
//
|
||||||
|
// Forma del cercle de cada emissor (radial, 3 anells configurables al
|
||||||
|
// fitxer data/config/lighting.yaml; veure `GradientConfig`):
|
||||||
// 0..inner_ratio del radi → inner_intensity
|
// 0..inner_ratio del radi → inner_intensity
|
||||||
// inner_ratio..mid_ratio → mid_intensity
|
// inner_ratio..mid_ratio → mid_intensity
|
||||||
// mid_ratio..1.0 → outer_intensity
|
// mid_ratio..1.0 → outer_intensity
|
||||||
// fora del radi → light_level de la sala
|
|
||||||
//
|
//
|
||||||
// Combinació entre emissors: per a cada píxel es pren el màxim del
|
// La corba viu en dos blocs separats al YAML perquè la mateixa escala
|
||||||
// llum de sala i de la intensitat aportada per cada emissor que el
|
// 0..1 té significats molt diferents per mode:
|
||||||
// solapa — no se "sumen" intensitats. Coherent amb la idea de "centre
|
// `gradient:` → corba del mode subtractive (1.0 = "no fosqueix")
|
||||||
// 1.0": el píxel que cau dins de qualsevol nucli no s'enfosqueix.
|
// `gradient_additive:` → corba del mode additive (1.0 = "afegeix
|
||||||
|
// el color sencer i satura"). Es vol molt més
|
||||||
|
// baixa per a no inundar la sala de color.
|
||||||
|
// Cada mode llegeix la seva pròpia corba — no es comparteixen valors.
|
||||||
|
//
|
||||||
|
// Combinació entre emissors:
|
||||||
|
// SUBTRACTIVE → per a cada píxel es pren el màxim de la intensitat
|
||||||
|
// aportada per cada emissor que el solapa (no se sumen intensitats).
|
||||||
|
// ADDITIVE → per a cada píxel se sumen totes les contribucions
|
||||||
|
// (`color × intensity`), saturant a 255 per canal. Dues llums de
|
||||||
|
// colors diferents que solapen produeixen la suma natural (orange +
|
||||||
|
// blue → near-white) — comportament físic.
|
||||||
namespace Lighting {
|
namespace Lighting {
|
||||||
|
|
||||||
// Posició al canvas (en píxels, mateixa unitat que la resta del
|
// Mode de composició. La sala el selecciona via `lighting.mode:
|
||||||
// motor) i radi efectiu de l'emissor. El centre és el del sprite
|
// additive | subtractive` (default subtractive si manca, retrocompat).
|
||||||
// visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`).
|
enum class Mode : std::uint8_t {
|
||||||
// `flicker` activa la modulació del radi per frame; `seed` és un
|
SUBTRACTIVE,
|
||||||
// identificador estable per emissor (no canvia amb la posició) del
|
ADDITIVE,
|
||||||
// qual es deriva la fase del flicker — així els emissors mòbils
|
};
|
||||||
// mantenen un patró propi que no "deriva" pel moviment.
|
|
||||||
struct Emitter {
|
// Color emès. Default blanc (sense modificació quan s'usa en mode
|
||||||
float cx{0.0F};
|
// additive). En mode subtractive s'ignora — la pasada multiplicativa
|
||||||
float cy{0.0F};
|
// només té un canal (intensitat). El YAML accepta `color: [r, g, b]`
|
||||||
int radius{0};
|
// a qualsevol nivell de la cadena de resolució (tipus, sala, spawn).
|
||||||
int seed{0};
|
struct Color {
|
||||||
bool flicker{false};
|
std::uint8_t r{255};
|
||||||
|
std::uint8_t g{255};
|
||||||
|
std::uint8_t b{255};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forma del gradient. Compartida per tots els emissors per evitar
|
// Forma del gradient. Compartida per tots els emissors per evitar
|
||||||
@@ -53,30 +81,70 @@ namespace Lighting {
|
|||||||
// forma final del radi és la suma d'una sinusoide "respiració"
|
// forma final del radi és la suma d'una sinusoide "respiració"
|
||||||
// (wave_*) i una sinusoide secundària de freqüència més alta
|
// (wave_*) i una sinusoide secundària de freqüència més alta
|
||||||
// (noise_*) que aporta la sensació d'inquietud. Les fases es
|
// (noise_*) que aporta la sensació d'inquietud. Les fases es
|
||||||
// deriven de la posició de cada emissor — sense estat per emissor.
|
// deriven de la seed estable de cada emissor — sense estat per
|
||||||
|
// emissor. Les amplituds són **ràtios** sobre el radi base (p.ex.
|
||||||
|
// 0.05 = 5% del radi): així una torxa de radi 40 i una de radi 24
|
||||||
|
// tenen el mateix percentatge d'oscil·lació visual, no els mateixos
|
||||||
|
// píxels (que es notarien més en la petita).
|
||||||
struct FlickerConfig {
|
struct FlickerConfig {
|
||||||
float wave_amplitude_px{2.0F};
|
float wave_amplitude_ratio{0.05F};
|
||||||
float wave_frequency_hz{3.0F};
|
float wave_frequency_hz{3.0F};
|
||||||
float noise_amplitude_px{1.0F};
|
float noise_amplitude_ratio{0.025F};
|
||||||
float noise_frequency_hz{7.0F};
|
float noise_frequency_hz{7.0F};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Posició al canvas (en píxels, mateixa unitat que la resta del
|
||||||
|
// motor) i radi efectiu de l'emissor. El centre és el del sprite
|
||||||
|
// visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`).
|
||||||
|
// `flicker` activa la modulació del radi per frame; `seed` és un
|
||||||
|
// identificador estable per emissor (no canvia amb la posició) del
|
||||||
|
// qual es deriva la fase del flicker — així els emissors mòbils
|
||||||
|
// mantenen un patró propi que no "deriva" pel moviment. `color`
|
||||||
|
// només té efecte en mode additive.
|
||||||
|
struct Emitter {
|
||||||
|
float cx{0.0F};
|
||||||
|
float cy{0.0F};
|
||||||
|
int radius{0};
|
||||||
|
int seed{0};
|
||||||
|
bool flicker{false};
|
||||||
|
Color color{};
|
||||||
|
// Paràmetres del flicker resolts per a aquest emissor (combinació
|
||||||
|
// global + override del tipus/sala/spawn). Només té sentit si
|
||||||
|
// `flicker == true`; en cas contrari s'ignoren. Per defecte
|
||||||
|
// s'inicialitza amb els defaults compilats — el caller (game_scene)
|
||||||
|
// hi posa el resolt correcte.
|
||||||
|
FlickerConfig flicker_params{};
|
||||||
|
};
|
||||||
|
|
||||||
// Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la
|
// Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la
|
||||||
// configuració global. Qualsevol clau absent cau als Defaults::Lighting.
|
// configuració global. Qualsevol clau absent cau als Defaults::Lighting.
|
||||||
// No es crida més d'una vegada en un run normal; tornar-la a cridar
|
// No es crida més d'una vegada en un run normal; tornar-la a cridar
|
||||||
// simplement reemplaça la configuració activa.
|
// simplement reemplaça la configuració activa.
|
||||||
void loadConfig(const std::string& yaml_path);
|
void loadConfig(const std::string& yaml_path);
|
||||||
|
|
||||||
// Configuració activa (per a debug/inspectors). Inicialitzada amb els
|
// Configuració activa (per a debug/inspectors). Retorna sempre la
|
||||||
|
// corba subtractive — la corba additive viu en paral·lel al cpp i,
|
||||||
|
// si algun dia es necessita inspeccionar des de fora, caldrà afegir
|
||||||
|
// una variant `configAdditive()`. Inicialitzada amb els
|
||||||
// Defaults::Lighting fins que loadConfig() es crida.
|
// Defaults::Lighting fins que loadConfig() es crida.
|
||||||
[[nodiscard]] auto config() -> const GradientConfig&;
|
[[nodiscard]] auto config() -> const GradientConfig&;
|
||||||
|
|
||||||
// Aplica la pasada d'il·luminació al canvas in-place. `room_light`
|
// Defaults globals del flicker (data/config/lighting.yaml, bloc
|
||||||
// és l'atenuació base de la sala (0..1, 1 = sense atenuació). El
|
// `flicker:`). Els callers del construct path d'entitats el llegeixen
|
||||||
// span d'emissors pot ser buit — en aquest cas tota la sala queda
|
// per inicialitzar la `FlickerConfig` de cada emissor, i aplicar a
|
||||||
// multiplicada per `room_light`. Aquesta funció no fa cap allocació
|
// sobre els overrides per tipus/sala/spawn.
|
||||||
// si es crida amb la mateixa mida de canvas múltiples cops: manté
|
[[nodiscard]] auto flickerConfig() -> const FlickerConfig&;
|
||||||
// un buffer intern reutilitzable.
|
|
||||||
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> emitters);
|
// Aplica la pasada d'il·luminació al canvas in-place. `mode`
|
||||||
|
// selecciona la composició (vegeu el comentari de la capçalera).
|
||||||
|
// `room_light` és l'atenuació ambient de la sala (0..1, 1 = sense
|
||||||
|
// atenuació): en subtractive és el "terra" de la intensitat fora
|
||||||
|
// dels emissors; en additive és la multiplicació prèvia del canvas
|
||||||
|
// abans de sumar els emissors (es pot deixar a 1.0 per "només tint",
|
||||||
|
// o baixar per combinar oscuriment ambient + halo cromàtic). El
|
||||||
|
// span d'emissors pot ser buit. La funció no fa cap allocació si
|
||||||
|
// es crida amb la mateixa mida de canvas múltiples cops: manté un
|
||||||
|
// buffer intern reutilitzable (només l'usa el mode subtractive).
|
||||||
|
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters);
|
||||||
|
|
||||||
} // namespace Lighting
|
} // namespace Lighting
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/rendering/hud_constants.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/rendering/text.hpp"
|
#include "core/rendering/text.hpp"
|
||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/rendering/hud_constants.hpp"
|
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "utils/defines.hpp"
|
|
||||||
|
|
||||||
class Text;
|
class Text;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <ranges>
|
|
||||||
|
|
||||||
#include "core/rendering/canvas.hpp"
|
#include "core/rendering/canvas.hpp"
|
||||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace Screenshot {
|
|||||||
// Estat de mòdul: ruta base on s'escriu la subcarpeta
|
// Estat de mòdul: ruta base on s'escriu la subcarpeta
|
||||||
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
|
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
|
||||||
// si el Director no ha pogut establir la ruta per plataforma).
|
// si el Director no ha pogut establir la ruta per plataforma).
|
||||||
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables)
|
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) -- estat de mòdul (singleton-like): s'estableix una vegada al startup via setBaseDir() i es llegeix N vegades; encapsulat dins del namespace anònim, no és accessible des de fora.
|
||||||
|
|
||||||
// Construeix la ruta del directori on van les captures:
|
// Construeix la ruta del directori on van les captures:
|
||||||
// `<base>/screenshots/`. Si la base és buida, retorna
|
// `<base>/screenshots/`. Si la base és buida, retorna
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct PostVOut {
|
|||||||
|
|
||||||
struct PostFXUniforms {
|
struct PostFXUniforms {
|
||||||
float vignette_strength;
|
float vignette_strength;
|
||||||
float chroma_strength;
|
float chroma_min;
|
||||||
float scanline_strength;
|
float scanline_strength;
|
||||||
float screen_height;
|
float screen_height;
|
||||||
float mask_strength;
|
float mask_strength;
|
||||||
@@ -32,7 +32,7 @@ struct PostFXUniforms {
|
|||||||
float pixel_scale;
|
float pixel_scale;
|
||||||
float time;
|
float time;
|
||||||
float flicker;
|
float flicker;
|
||||||
float pad2;
|
float chroma_max;
|
||||||
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||||||
float scan_dark_ratio;
|
float scan_dark_ratio;
|
||||||
float scan_dark_floor;
|
float scan_dark_floor;
|
||||||
@@ -40,6 +40,18 @@ struct PostFXUniforms {
|
|||||||
float pad3;
|
float pad3;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||||||
|
// NEAREST quan l'offset de chroma és subpíxel.
|
||||||
|
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
|
||||||
|
float2 tex_size = float2(scene.get_width(), scene.get_height());
|
||||||
|
float px = uv_target.x * tex_size.x - 0.5f;
|
||||||
|
float p_floor = floor(px);
|
||||||
|
float f = px - p_floor;
|
||||||
|
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
|
||||||
|
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
|
||||||
|
return mix(c0[channel], c1[channel], f);
|
||||||
|
}
|
||||||
|
|
||||||
static float3 rgb_to_ycc(float3 rgb) {
|
static float3 rgb_to_ycc(float3 rgb) {
|
||||||
return float3(
|
return float3(
|
||||||
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
|
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
|
||||||
@@ -94,9 +106,14 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
|||||||
colour = base;
|
colour = base;
|
||||||
}
|
}
|
||||||
|
|
||||||
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f));
|
// Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
|
||||||
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r;
|
// queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
|
||||||
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b;
|
// NEAREST sampler amb offsets subpíxel.
|
||||||
|
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
|
||||||
|
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
|
||||||
|
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
|
||||||
|
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
|
||||||
|
}
|
||||||
|
|
||||||
if (u.gamma_strength > 0.0f) {
|
if (u.gamma_strength > 0.0f) {
|
||||||
float3 lin = pow(colour, float3(2.4f));
|
float3 lin = pow(colour, float3(2.4f));
|
||||||
|
|||||||
@@ -234,7 +234,8 @@ namespace Rendering {
|
|||||||
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
|
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
|
||||||
uniforms_.vignette_strength = params.vignette;
|
uniforms_.vignette_strength = params.vignette;
|
||||||
uniforms_.scanline_strength = params.scanlines;
|
uniforms_.scanline_strength = params.scanlines;
|
||||||
uniforms_.chroma_strength = params.chroma;
|
uniforms_.chroma_min = params.chroma_min;
|
||||||
|
uniforms_.chroma_max = params.chroma_max;
|
||||||
uniforms_.mask_strength = params.mask;
|
uniforms_.mask_strength = params.mask;
|
||||||
uniforms_.gamma_strength = params.gamma;
|
uniforms_.gamma_strength = params.gamma;
|
||||||
uniforms_.curvature = params.curvature;
|
uniforms_.curvature = params.curvature;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
struct PostFXUniforms {
|
struct PostFXUniforms {
|
||||||
// vec4 #0
|
// vec4 #0
|
||||||
float vignette_strength;
|
float vignette_strength;
|
||||||
float chroma_strength;
|
float chroma_min;
|
||||||
float scanline_strength;
|
float scanline_strength;
|
||||||
float screen_height;
|
float screen_height;
|
||||||
// vec4 #1
|
// vec4 #1
|
||||||
@@ -27,7 +27,7 @@ struct PostFXUniforms {
|
|||||||
float pixel_scale;
|
float pixel_scale;
|
||||||
float time;
|
float time;
|
||||||
float flicker;
|
float flicker;
|
||||||
float pad2;
|
float chroma_max;
|
||||||
// vec4 #3 — paràmetres de scanlines exposats al preset YAML
|
// vec4 #3 — paràmetres de scanlines exposats al preset YAML
|
||||||
float scan_dark_ratio;
|
float scan_dark_ratio;
|
||||||
float scan_dark_floor;
|
float scan_dark_floor;
|
||||||
@@ -92,7 +92,7 @@ namespace Rendering {
|
|||||||
|
|
||||||
PostFXUniforms uniforms_{
|
PostFXUniforms uniforms_{
|
||||||
.vignette_strength = 0.0F,
|
.vignette_strength = 0.0F,
|
||||||
.chroma_strength = 0.0F,
|
.chroma_min = 0.0F,
|
||||||
.scanline_strength = 0.0F,
|
.scanline_strength = 0.0F,
|
||||||
.screen_height = 0.0F,
|
.screen_height = 0.0F,
|
||||||
.mask_strength = 0.0F,
|
.mask_strength = 0.0F,
|
||||||
@@ -102,7 +102,7 @@ namespace Rendering {
|
|||||||
.pixel_scale = 1.0F,
|
.pixel_scale = 1.0F,
|
||||||
.time = 0.0F,
|
.time = 0.0F,
|
||||||
.flicker = 0.0F,
|
.flicker = 0.0F,
|
||||||
.pad2 = 0.0F,
|
.chroma_max = 0.0F,
|
||||||
// Defaults canònics: 2/3 brillant + 1/3 fosca, brillantor 42%, suavitzat complet (1 px físic).
|
// Defaults canònics: 2/3 brillant + 1/3 fosca, brillantor 42%, suavitzat complet (1 px físic).
|
||||||
.scan_dark_ratio = 0.333F,
|
.scan_dark_ratio = 0.333F,
|
||||||
.scan_dark_floor = 0.42F,
|
.scan_dark_floor = 0.42F,
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -19,7 +19,10 @@ namespace Rendering {
|
|||||||
struct PostFXParams {
|
struct PostFXParams {
|
||||||
float vignette{0.0F};
|
float vignette{0.0F};
|
||||||
float scanlines{0.0F};
|
float scanlines{0.0F};
|
||||||
float chroma{0.0F};
|
// Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
|
||||||
|
// queda estàtica. Permet un "floor" perquè la imatge mai sigui lliure de chroma.
|
||||||
|
float chroma_min{0.0F};
|
||||||
|
float chroma_max{0.0F};
|
||||||
float mask{0.0F};
|
float mask{0.0F};
|
||||||
float gamma{0.0F};
|
float gamma{0.0F};
|
||||||
float curvature{0.0F};
|
float curvature{0.0F};
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Surface::Surface(const std::string& file_path) {
|
|||||||
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
|
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
printWithDots("Surface", getFileName(file_path), "[ LOADED ]");
|
printStatus("Surface", getFileName(file_path), StatusKind::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Surface::clear(Uint8 color) {
|
void Surface::clear(Uint8 color) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printWithDots("Text File", getFileName(file_path), "[ LOADED ]");
|
printStatus("Text File", getFileName(file_path), StatusKind::SUCCESS);
|
||||||
return tf;
|
return tf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ namespace Resource {
|
|||||||
throw std::runtime_error("Cache: cannot load animation " + file_path);
|
throw std::runtime_error("Cache: cannot load animation " + file_path);
|
||||||
}
|
}
|
||||||
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
|
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
|
||||||
printWithDots("Animation", getFileName(file_path), "[ LOADED ]");
|
printStatus("Animation", getFileName(file_path), StatusKind::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Cache::loadPalette(const std::string& name, const std::string& file_path) {
|
void Cache::loadPalette(const std::string& name, const std::string& file_path) {
|
||||||
auto palette = PaletteLoader::loadFromFile(file_path);
|
auto palette = PaletteLoader::loadFromFile(file_path);
|
||||||
palettes_.emplace_back(PaletteResource{.name = name, .palette = palette});
|
palettes_.emplace_back(PaletteResource{.name = name, .palette = palette});
|
||||||
printWithDots("Palette", getFileName(file_path), "[ LOADED ]");
|
printStatus("Palette", getFileName(file_path), StatusKind::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
|
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
|
||||||
@@ -83,7 +83,7 @@ namespace Resource {
|
|||||||
throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path);
|
throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path);
|
||||||
}
|
}
|
||||||
musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)});
|
musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)});
|
||||||
printWithDots("Music", getFileName(file_path), "[ LOADED ]");
|
printStatus("Music", getFileName(file_path), StatusKind::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Cache::loadSound(const std::string& name, const std::string& file_path) {
|
void Cache::loadSound(const std::string& name, const std::string& file_path) {
|
||||||
@@ -96,7 +96,7 @@ namespace Resource {
|
|||||||
throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path);
|
throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path);
|
||||||
}
|
}
|
||||||
sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)});
|
sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)});
|
||||||
printWithDots("Sound", getFileName(file_path), "[ LOADED ]");
|
printStatus("Sound", getFileName(file_path), StatusKind::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Cache::getMusic(const std::string& name) -> Ja::Music* {
|
auto Cache::getMusic(const std::string& name) -> Ja::Music* {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
#include "utils/log.hpp"
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
@@ -19,12 +20,7 @@ namespace Resource {
|
|||||||
auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t {
|
auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t {
|
||||||
// djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai
|
// djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai
|
||||||
// reutilitzem eines de diagnòstic entre projectes.
|
// reutilitzem eines de diagnòstic entre projectes.
|
||||||
std::uint32_t checksum = 0x12345678;
|
return std::accumulate(data.begin(), data.end(), std::uint32_t{0x12345678}, [](std::uint32_t acc, unsigned char b) { return ((acc << 5) + acc) + b; });
|
||||||
for (unsigned char b : data) {
|
|
||||||
// cppcheck-suppress useStlAlgorithm
|
|
||||||
checksum = ((checksum << 5) + checksum) + b;
|
|
||||||
}
|
|
||||||
return checksum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) {
|
void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) {
|
||||||
@@ -151,7 +147,7 @@ namespace Resource {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// cppcheck-suppress useStlAlgorithm
|
// cppcheck-suppress useStlAlgorithm -- bucle amb early-return en error (addFile fallit) + filtre amb continue: no es pot expressar net amb std::ranges::all_of perquè la lògica combina filtratge i acumulació amb sortida prematura.
|
||||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
|
||||||
if (!entry.is_regular_file()) { continue; }
|
if (!entry.is_regular_file()) { continue; }
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <variant>
|
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/audio/sound_effects_config.hpp"
|
#include "core/audio/sound_effects_config.hpp"
|
||||||
@@ -24,7 +23,6 @@
|
|||||||
#include "core/rendering/render_info.hpp"
|
#include "core/rendering/render_info.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/rendering/screenshot.hpp"
|
#include "core/rendering/screenshot.hpp"
|
||||||
#include "core/rendering/surface.hpp"
|
|
||||||
#include "core/rendering/text.hpp"
|
#include "core/rendering/text.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
@@ -35,8 +33,8 @@
|
|||||||
#include "game/gameplay_config.hpp"
|
#include "game/gameplay_config.hpp"
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
#include "game/editor/editor_config.hpp"
|
#include "game/editor/editor_config.hpp"
|
||||||
#endif
|
|
||||||
#include "game/levels/level_manager.hpp"
|
#include "game/levels/level_manager.hpp"
|
||||||
|
#endif
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/scenes/game_scene.hpp"
|
#include "game/scenes/game_scene.hpp"
|
||||||
#include "game/scenes/intro_scene.hpp"
|
#include "game/scenes/intro_scene.hpp"
|
||||||
@@ -354,10 +352,10 @@ Director::~Director() {
|
|||||||
|
|
||||||
void Director::createSystemFolder() {
|
void Director::createSystemFolder() {
|
||||||
// Carpeta de configuració per plataforma:
|
// Carpeta de configuració per plataforma:
|
||||||
// - Windows: %APPDATA%\jailgames\aee_arcade\
|
// - Windows: %APPDATA%\jailgames\aee_arcade
|
||||||
// - macOS: ~/Library/Application Support/jailgames/aee_arcade/
|
// - macOS: ~/Library/Application Support/jailgames/aee_arcade
|
||||||
// - Linux: $XDG_CONFIG_HOME/jailgames/aee_arcade/ (defecte
|
// - Linux: $XDG_CONFIG_HOME/jailgames/aee_arcade (defecte
|
||||||
// ~/.config/jailgames/aee_arcade/)
|
// ~/.config/jailgames/aee_arcade)
|
||||||
// No fem servir SDL_GetPrefPath: a Linux apunta a XDG_DATA_HOME
|
// No fem servir SDL_GetPrefPath: a Linux apunta a XDG_DATA_HOME
|
||||||
// (~/.local/share), que és per a dades, no per a configuració.
|
// (~/.local/share), que és per a dades, no per a configuració.
|
||||||
std::string base;
|
std::string base;
|
||||||
@@ -446,9 +444,12 @@ void Director::processGamepadEvents() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Director::processMenuInput() {
|
void Director::processMenuInput() {
|
||||||
// Intercepta input abans que l'escena vegi els events. Si el menú està
|
// Intercepta input abans que l'escena vegi els events. Amb el menú
|
||||||
// obert, consumeix TOTS els KEY_DOWN perquè ni GlobalInputs (F1–F8,
|
// obert, `handleEvent` consumeix tot KEY_DOWN excepte les F-keys
|
||||||
// F11, F12) ni el jugador els veuen. F9 obre el menú quan està tancat.
|
// (F1–F12) i ESC, que són hotkeys d'UI omnipresents i passen avall
|
||||||
|
// a GlobalInputs (ESC manté la doble pulsació per a confirmar l'exit
|
||||||
|
// independentment del context). Quan el menú està tancat, la branca
|
||||||
|
// de baix detecta `menu_toggle` (F12 per defecte) i obre el menú.
|
||||||
// Erase invalida iteradors, per això actualitzem `it` a cada branca.
|
// Erase invalida iteradors, per això actualitzem `it` a cada branca.
|
||||||
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
||||||
if (ServiceMenu::get().isOpen() && ServiceMenu::get().handleEvent(*it)) {
|
if (ServiceMenu::get().isOpen() && ServiceMenu::get().handleEvent(*it)) {
|
||||||
@@ -465,16 +466,6 @@ void Director::processMenuInput() {
|
|||||||
|
|
||||||
void Director::switchSceneIfRequested() {
|
void Director::switchSceneIfRequested() {
|
||||||
if (!current_scene_) { return; }
|
if (!current_scene_) { return; }
|
||||||
// Prioritat al menú: si un item del ServiceMenu ha encuat una
|
|
||||||
// transició (típicament "Reiniciar" → LOGO amb reset), s'aplica
|
|
||||||
// primer. Substitueix l'`ExitRequest` que pugui haver publicat
|
|
||||||
// l'escena en aquest mateix frame — l'usuari ha cancel·lat l'estat
|
|
||||||
// de joc explícitament i no ens interessa mantenir-ne la sortida.
|
|
||||||
if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) {
|
|
||||||
Log::info("Director", "scene transition (menu) → {}", toString(menu_req->next));
|
|
||||||
current_scene_ = buildScene(menu_req->next, menu_req->params);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// L'escena ha publicat un `ExitRequest` dins de `exit_request_`.
|
// L'escena ha publicat un `ExitRequest` dins de `exit_request_`.
|
||||||
// Director el desempaqueta i construeix la nova escena via
|
// Director el desempaqueta i construeix la nova escena via
|
||||||
// `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el
|
// `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el
|
||||||
@@ -497,10 +488,8 @@ auto Director::buildScene(SceneId id, const SceneParams& params) -> std::unique_
|
|||||||
// ctors antics no l'utilitzen directament. Quan totes estiguen
|
// ctors antics no l'utilitzen directament. Quan totes estiguen
|
||||||
// migrades a `ExitRequest`, aquestes lambdes desapareixeran.
|
// migrades a `ExitRequest`, aquestes lambdes desapareixeran.
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case SceneId::LOGO: {
|
case SceneId::LOGO:
|
||||||
const auto& p = std::get<LogoParams>(params);
|
return std::make_unique<LogoScene>(logo_full_surface_);
|
||||||
return std::make_unique<LogoScene>(logo_full_surface_, p.reset);
|
|
||||||
}
|
|
||||||
case SceneId::TITLE:
|
case SceneId::TITLE:
|
||||||
return std::make_unique<TitleScene>(placeholder_text_, copyright_text_, title_bg_surface_, game_logo_surface_, mini_logo_surface_);
|
return std::make_unique<TitleScene>(placeholder_text_, copyright_text_, title_bg_surface_, game_logo_surface_, mini_logo_surface_);
|
||||||
case SceneId::INTRO: {
|
case SceneId::INTRO: {
|
||||||
|
|||||||
@@ -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_value_(Defaults::UI::MENU_VALUE),
|
||||||
menu_cursor_(Defaults::UI::MENU_CURSOR),
|
menu_cursor_(Defaults::UI::MENU_CURSOR),
|
||||||
menu_disabled_(Defaults::UI::MENU_DISABLED),
|
menu_disabled_(Defaults::UI::MENU_DISABLED),
|
||||||
|
menu_title_outline_(Defaults::UI::MENU_TITLE_OUTLINE),
|
||||||
|
menu_label_outline_(Defaults::UI::MENU_LABEL_OUTLINE),
|
||||||
|
menu_value_outline_(Defaults::UI::MENU_VALUE_OUTLINE),
|
||||||
|
menu_cursor_outline_(Defaults::UI::MENU_CURSOR_OUTLINE),
|
||||||
hud_bg_(Defaults::UI::HUD_BG),
|
hud_bg_(Defaults::UI::HUD_BG),
|
||||||
hud_p1_label_(Defaults::UI::HUD_P1_LABEL),
|
hud_p1_label_(Defaults::UI::HUD_P1_LABEL),
|
||||||
hud_p1_value_(Defaults::UI::HUD_P1_VALUE),
|
hud_p1_value_(Defaults::UI::HUD_P1_VALUE),
|
||||||
@@ -215,19 +219,38 @@ void UiConfig::load(const std::string& file_path) {
|
|||||||
readColorIndex(c, "notification_bg", notification_bg_);
|
readColorIndex(c, "notification_bg", notification_bg_);
|
||||||
readColorIndex(c, "exit_text", exit_text_);
|
readColorIndex(c, "exit_text", exit_text_);
|
||||||
readColorIndex(c, "exit_bg", exit_bg_);
|
readColorIndex(c, "exit_bg", exit_bg_);
|
||||||
readColorIndex(c, "menu_bg", menu_bg_);
|
|
||||||
readColorIndex(c, "menu_border", menu_border_);
|
|
||||||
readColorIndex(c, "menu_title", menu_title_);
|
|
||||||
readColorIndex(c, "menu_label", menu_label_);
|
|
||||||
readColorIndex(c, "menu_value", menu_value_);
|
|
||||||
readColorIndex(c, "menu_cursor", menu_cursor_);
|
|
||||||
readColorIndex(c, "menu_disabled", menu_disabled_);
|
|
||||||
readColorIndex(c, "hud_bg", hud_bg_);
|
readColorIndex(c, "hud_bg", hud_bg_);
|
||||||
readColorIndex(c, "hud_flash", hud_flash_);
|
readColorIndex(c, "hud_flash", hud_flash_);
|
||||||
readColorIndex(c, "room_text", room_text_);
|
readColorIndex(c, "room_text", room_text_);
|
||||||
readColorIndex(c, "room_text_outline", room_text_outline_);
|
readColorIndex(c, "room_text_outline", room_text_outline_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tema actiu del menú: `menu_theme` és el nom del bloc dins de
|
||||||
|
// `menu_themes` que aporta els 7 slots de color. Si qualsevol
|
||||||
|
// de les dues claus manca o el nom no existeix, els slots
|
||||||
|
// queden amb el default compilat (= tema "classic" hardcoded
|
||||||
|
// a Defaults::UI). Per-key dins del tema també és opcional.
|
||||||
|
if (yaml.contains("menu_theme") && yaml.contains("menu_themes")) {
|
||||||
|
const auto THEME_NAME = yaml["menu_theme"].get_value<std::string>();
|
||||||
|
const auto& themes = yaml["menu_themes"];
|
||||||
|
if (themes.contains(THEME_NAME)) {
|
||||||
|
const auto& t = themes[THEME_NAME];
|
||||||
|
readColorIndex(t, "bg", menu_bg_);
|
||||||
|
readColorIndex(t, "border", menu_border_);
|
||||||
|
readColorIndex(t, "title", menu_title_);
|
||||||
|
readColorIndex(t, "label", menu_label_);
|
||||||
|
readColorIndex(t, "value", menu_value_);
|
||||||
|
readColorIndex(t, "cursor", menu_cursor_);
|
||||||
|
readColorIndex(t, "disabled", menu_disabled_);
|
||||||
|
readColorIndex(t, "title_outline", menu_title_outline_);
|
||||||
|
readColorIndex(t, "label_outline", menu_label_outline_);
|
||||||
|
readColorIndex(t, "value_outline", menu_value_outline_);
|
||||||
|
readColorIndex(t, "cursor_outline", menu_cursor_outline_);
|
||||||
|
} else {
|
||||||
|
Log::warn("UiConfig", "menu_theme '{}' no existeix a menu_themes — s'usen defaults", THEME_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Famílies de color del HUD: blocs `hud_p1`, `hud_center`,
|
// Famílies de color del HUD: blocs `hud_p1`, `hud_center`,
|
||||||
// `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i
|
// `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i
|
||||||
// cada clau són opcionals — el que manqui cau al default
|
// cada clau són opcionals — el que manqui cau al default
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ class UiConfig {
|
|||||||
[[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; }
|
[[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; }
|
||||||
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
|
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
|
||||||
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
|
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
|
||||||
|
[[nodiscard]] auto menuTitleOutline() const -> Uint8 { return menu_title_outline_; }
|
||||||
|
[[nodiscard]] auto menuLabelOutline() const -> Uint8 { return menu_label_outline_; }
|
||||||
|
[[nodiscard]] auto menuValueOutline() const -> Uint8 { return menu_value_outline_; }
|
||||||
|
[[nodiscard]] auto menuCursorOutline() const -> Uint8 { return menu_cursor_outline_; }
|
||||||
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
|
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
|
||||||
// Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada
|
// Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada
|
||||||
// família té label (etiqueta apagada), value (valor destacat) i
|
// família té label (etiqueta apagada), value (valor destacat) i
|
||||||
@@ -214,6 +218,10 @@ class UiConfig {
|
|||||||
Uint8 menu_value_;
|
Uint8 menu_value_;
|
||||||
Uint8 menu_cursor_;
|
Uint8 menu_cursor_;
|
||||||
Uint8 menu_disabled_;
|
Uint8 menu_disabled_;
|
||||||
|
Uint8 menu_title_outline_;
|
||||||
|
Uint8 menu_label_outline_;
|
||||||
|
Uint8 menu_value_outline_;
|
||||||
|
Uint8 menu_cursor_outline_;
|
||||||
Uint8 hud_bg_;
|
Uint8 hud_bg_;
|
||||||
Uint8 hud_p1_label_;
|
Uint8 hud_p1_label_;
|
||||||
Uint8 hud_p1_value_;
|
Uint8 hud_p1_value_;
|
||||||
|
|||||||
@@ -345,9 +345,7 @@ namespace {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// cppcheck-suppress constParameterReference
|
// cppcheck-suppress constParameterReference -- Tilemap& mutable per simetria amb `toggle()` i per futures invalidacions de caches d'edició; cppcheck no pot anticipar que les futures mutacions estan previstes en el contracte.
|
||||||
// Tilemap& mutable per simetria amb `toggle()` i per futures
|
|
||||||
// invalidacions de caches d'edició, encara que `save()` sigui const.
|
|
||||||
void Editor::exit(Tilemap& tilemap, const EntityManager& entities) {
|
void Editor::exit(Tilemap& tilemap, const EntityManager& entities) {
|
||||||
if (!active_) { return; }
|
if (!active_) { return; }
|
||||||
// Constraint del nivell: cal exactament una porta i com a mínim un
|
// Constraint del nivell: cal exactament una porta i com a mínim un
|
||||||
|
|||||||
@@ -1,14 +1,82 @@
|
|||||||
#include "game/entities/animation_component.hpp"
|
#include "game/entities/animation_component.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <iterator>
|
||||||
|
#include <stdexcept>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop)
|
namespace {
|
||||||
: AnimationComponent(std::move(frames), fps, loop, false, false) {}
|
|
||||||
|
|
||||||
AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop, bool flip_h, bool flip_v)
|
// Quan el caller no especifica temps, considerem un frame "estàtic" de
|
||||||
|
// 1s. El valor és arbitrari: amb 1 frame el component mai no avança,
|
||||||
|
// així que el speed concret és inert. Amb N frames és el fallback que
|
||||||
|
// assumeix la factory (mateix valor que el default del format antic).
|
||||||
|
constexpr float K_DEFAULT_FRAME_SECONDS = 1.0F;
|
||||||
|
|
||||||
|
// Si el YAML declara temps <= 0 per a algun frame, el saturem a aquest
|
||||||
|
// mínim per a evitar bucles infinits a `tick` (que avancen frame
|
||||||
|
// mentre `accumulator >= speeds[i]`). 1 ms és prou petit per a no
|
||||||
|
// alterar mai una temporització real i prou gran per a no col·lapsar.
|
||||||
|
constexpr float K_MIN_FRAME_SECONDS = 0.001F;
|
||||||
|
|
||||||
|
auto sanitizeSpeed(float seconds) -> float {
|
||||||
|
return seconds > 0.0F ? seconds : K_MIN_FRAME_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expandeix una llista de temps a la mida de `frame_count`:
|
||||||
|
// - mida = N: 1:1.
|
||||||
|
// - mida < N: emplena la resta amb l'últim valor (o el default si
|
||||||
|
// la llista era buida).
|
||||||
|
// - mida > N: talla a N.
|
||||||
|
// Tots els valors es passen per `sanitizeSpeed`.
|
||||||
|
auto fitSpeeds(std::vector<float> speeds, std::size_t frame_count) -> std::vector<float> {
|
||||||
|
if (frame_count == 0) { return {}; }
|
||||||
|
if (speeds.empty()) {
|
||||||
|
return std::vector<float>(frame_count, K_DEFAULT_FRAME_SECONDS);
|
||||||
|
}
|
||||||
|
std::ranges::transform(speeds, speeds.begin(), sanitizeSpeed);
|
||||||
|
if (speeds.size() == frame_count) { return speeds; }
|
||||||
|
if (speeds.size() > frame_count) {
|
||||||
|
speeds.resize(frame_count);
|
||||||
|
return speeds;
|
||||||
|
}
|
||||||
|
const float TAIL = speeds.back();
|
||||||
|
speeds.resize(frame_count, TAIL);
|
||||||
|
return speeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llig `animations.<key>.frames` com a llista d'enters. Garanteix
|
||||||
|
// almenys 1 frame (fallback a [0]) per a poder construir sempre un
|
||||||
|
// component vàlid.
|
||||||
|
auto parseFrames(const fkyaml::node& anim) -> std::vector<int> {
|
||||||
|
std::vector<int> frames;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llig `animations.<key>.frame_ms` (escalar o llista) i retorna els
|
||||||
|
// temps en segons. Si és escalar, retorna una llista uniforme de
|
||||||
|
// `frame_count` elements; si és llista, copia tal qual (l'ajust de
|
||||||
|
// mida el farà `fitSpeeds` al constructor).
|
||||||
|
auto parseFrameMs(const fkyaml::node& anim, std::size_t frame_count) -> std::vector<float> {
|
||||||
|
std::vector<float> speeds_secs;
|
||||||
|
const auto& fm = anim["frame_ms"];
|
||||||
|
if (fm.is_sequence()) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return speeds_secs;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
AnimationComponent::AnimationComponent(std::vector<int> frames, std::vector<float> speeds, bool loop, bool flip_h, bool flip_v)
|
||||||
: frames_(std::move(frames)),
|
: frames_(std::move(frames)),
|
||||||
fps_(fps > 0.0F ? fps : 1.0F),
|
speeds_(fitSpeeds(std::move(speeds), frames_.size())),
|
||||||
loop_(loop),
|
loop_(loop),
|
||||||
flip_h_(flip_h),
|
flip_h_(flip_h),
|
||||||
flip_v_(flip_v) {
|
flip_v_(flip_v) {
|
||||||
@@ -19,13 +87,52 @@ AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool
|
|||||||
if (frames_.size() <= 1 && !loop_) { completed_ = true; }
|
if (frames_.size() <= 1 && !loop_) { completed_ = true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto AnimationComponent::uniform(std::vector<int> frames, float frame_ms, bool loop) -> AnimationComponent {
|
||||||
|
return uniform(std::move(frames), frame_ms, loop, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AnimationComponent::uniform(std::vector<int> frames, float frame_ms, bool loop, bool flip_h, bool flip_v) -> AnimationComponent {
|
||||||
|
const float SECS = sanitizeSpeed(frame_ms / 1000.0F);
|
||||||
|
std::vector<float> speeds(frames.size(), SECS);
|
||||||
|
return {std::move(frames), std::move(speeds), loop, flip_h, flip_v};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AnimationComponent::fromYaml(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||||
|
// Defaults conservadors per a quan el bloc `animations.<key>` no
|
||||||
|
// existeix: 1 frame loopant a 1s — visualment estàtic, semàntica neutra.
|
||||||
|
std::vector<int> frames = {0};
|
||||||
|
std::vector<float> speeds_secs; // buit ⇒ fitSpeeds emplenarà amb default
|
||||||
|
bool loop = true;
|
||||||
|
bool flip_h = false;
|
||||||
|
bool flip_v = false;
|
||||||
|
|
||||||
|
if (!yaml.contains("animations") || !yaml["animations"].contains(key)) {
|
||||||
|
return {std::move(frames), std::move(speeds_secs), loop, flip_h, flip_v};
|
||||||
|
}
|
||||||
|
const auto& anim = yaml["animations"][key];
|
||||||
|
|
||||||
|
// L'antic format `fps: N` ja no és suportat. Si apareix, peguem hard
|
||||||
|
// perquè la migració sigui visible (no silenciosa). El missatge inclou
|
||||||
|
// la clau d'animació per a facilitar localitzar el YAML problemàtic.
|
||||||
|
if (anim.contains("fps")) {
|
||||||
|
throw std::runtime_error("AnimationComponent: 'fps:' is no longer supported, use 'frame_ms:' instead (animation '" + key + "')");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anim.contains("frames")) { frames = parseFrames(anim); }
|
||||||
|
if (anim.contains("frame_ms")) { speeds_secs = parseFrameMs(anim, frames.size()); }
|
||||||
|
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
||||||
|
if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value<bool>(); }
|
||||||
|
if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value<bool>(); }
|
||||||
|
|
||||||
|
return {std::move(frames), std::move(speeds_secs), loop, flip_h, flip_v};
|
||||||
|
}
|
||||||
|
|
||||||
void AnimationComponent::tick(float delta_time) {
|
void AnimationComponent::tick(float delta_time) {
|
||||||
if (frames_.size() <= 1 || completed_) { return; }
|
if (frames_.size() <= 1 || completed_) { return; }
|
||||||
|
|
||||||
accumulator_ += delta_time;
|
accumulator_ += delta_time;
|
||||||
const float FRAME_TIME = 1.0F / fps_;
|
while (accumulator_ >= speeds_[static_cast<std::size_t>(current_)]) {
|
||||||
while (accumulator_ >= FRAME_TIME) {
|
accumulator_ -= speeds_[static_cast<std::size_t>(current_)];
|
||||||
accumulator_ -= FRAME_TIME;
|
|
||||||
++current_;
|
++current_;
|
||||||
if (static_cast<std::size_t>(current_) >= frames_.size()) {
|
if (static_cast<std::size_t>(current_) >= frames_.size()) {
|
||||||
if (loop_) {
|
if (loop_) {
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
// Component reutilitzable d'animació: qualsevol entitat que necessiti
|
// Component reutilitzable d'animació: qualsevol entitat que necessiti
|
||||||
// reproduir una seqüència de frames l'instancia. Es manté com a camp
|
// reproduir una seqüència de frames l'instancia. Es manté com a camp
|
||||||
// opcional dins d'Entity (veure `entity.hpp`) i NO hereta de cap classe
|
// opcional dins d'Entity (veure `entity.hpp`) i NO hereta de cap classe
|
||||||
@@ -22,16 +25,41 @@
|
|||||||
// canvien d'animació, sense decidir-lo per direcció. Així una mòmia que va
|
// canvien d'animació, sense decidir-lo per direcció. Així una mòmia que va
|
||||||
// cap a l'oest reprodueix `walk_west` (frames de l'est + `flip_h: true`)
|
// cap a l'oest reprodueix `walk_west` (frames de l'est + `flip_h: true`)
|
||||||
// sense que el codi de la mòmia toqui flip directament.
|
// sense que el codi de la mòmia toqui flip directament.
|
||||||
|
//
|
||||||
|
// El temps es modela com a `speeds_[i]` segons per frame (per-frame), no com
|
||||||
|
// a `fps` uniforme. Això permet expressar holds al YAML via `frame_ms: [..]`
|
||||||
|
// (llista) sense haver de repetir el mateix índex de frame. Per a casos
|
||||||
|
// uniformes la factory omple `speeds_` amb el mateix valor a tot el vector.
|
||||||
class AnimationComponent {
|
class AnimationComponent {
|
||||||
public:
|
public:
|
||||||
AnimationComponent() = default;
|
AnimationComponent() = default;
|
||||||
AnimationComponent(std::vector<int> frames, float fps, bool loop);
|
|
||||||
AnimationComponent(std::vector<int> frames, float fps, bool loop, bool flip_h, bool flip_v);
|
// Factory canònica: llig el bloc `animations.<key>` del YAML.
|
||||||
|
//
|
||||||
|
// Camps llegits:
|
||||||
|
// - `frames` : llista d'índexs sencers (obligatori per animar).
|
||||||
|
// - `frame_ms` : escalar ms (uniforme) o llista de ms (per-frame).
|
||||||
|
// Default 1000 ms (animació estàtica).
|
||||||
|
// - `loop` : bool (default true).
|
||||||
|
// - `flip_h` : bool (default false).
|
||||||
|
// - `flip_v` : bool (default false).
|
||||||
|
//
|
||||||
|
// Si la clau no existeix retorna un component "estàtic" (1 frame, loop
|
||||||
|
// true). Si el YAML conté `fps:`, llença `std::runtime_error`: el
|
||||||
|
// format antic ja no és suportat — cal migrar a `frame_ms:`.
|
||||||
|
static auto fromYaml(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent;
|
||||||
|
|
||||||
|
// Helper uniforme: construeix un component on tots els frames duren
|
||||||
|
// `frame_ms` mil·lisegons. Útil per a usos programàtics (tests,
|
||||||
|
// imposició d'un frame estàtic des del loader de nivell). Per al cas
|
||||||
|
// típic guiat per YAML, usar `fromYaml`.
|
||||||
|
static auto uniform(std::vector<int> frames, float frame_ms, bool loop) -> AnimationComponent;
|
||||||
|
static auto uniform(std::vector<int> frames, float frame_ms, bool loop, bool flip_h, bool flip_v) -> AnimationComponent;
|
||||||
|
|
||||||
// Avanç del temps. Si `frames_` està buit o té un sol element, no fa
|
// Avanç del temps. Si `frames_` està buit o té un sol element, no fa
|
||||||
// res. En mode loop, `current_frame_` rebota a 0 en passar del final;
|
// res. En mode loop, `current_` rebota a 0 en passar del final; en
|
||||||
// en mode no-loop, es queda al darrer frame i `completed()` passa a
|
// mode no-loop, es queda al darrer frame i `completed()` passa a true.
|
||||||
// true.
|
// Cada frame consumeix `speeds_[current_]` segons.
|
||||||
void tick(float delta_time);
|
void tick(float delta_time);
|
||||||
|
|
||||||
// Torna al primer frame i reinicia l'acumulador. Útil en transicions
|
// Torna al primer frame i reinicia l'acumulador. Útil en transicions
|
||||||
@@ -39,8 +67,7 @@ class AnimationComponent {
|
|||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
// Índex actual dins `frames_`. Si `frames_` està buit retorna 0 com
|
// Índex actual dins `frames_`. Si `frames_` està buit retorna 0 com
|
||||||
// a fallback tolerant (el loader ja ha emès warning si hi havia
|
// a fallback tolerant.
|
||||||
// frames fora de rang; aquí preferim no crashejar).
|
|
||||||
[[nodiscard]] auto currentFrame() const -> int;
|
[[nodiscard]] auto currentFrame() const -> int;
|
||||||
|
|
||||||
[[nodiscard]] auto frameCount() const -> std::size_t { return frames_.size(); }
|
[[nodiscard]] auto frameCount() const -> std::size_t { return frames_.size(); }
|
||||||
@@ -55,8 +82,10 @@ class AnimationComponent {
|
|||||||
[[nodiscard]] auto flipMode() const -> SDL_FlipMode;
|
[[nodiscard]] auto flipMode() const -> SDL_FlipMode;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
AnimationComponent(std::vector<int> frames, std::vector<float> speeds, bool loop, bool flip_h, bool flip_v);
|
||||||
|
|
||||||
std::vector<int> frames_;
|
std::vector<int> frames_;
|
||||||
float fps_{1.0F};
|
std::vector<float> speeds_; // segons per frame; size == frames_.size()
|
||||||
bool loop_{true};
|
bool loop_{true};
|
||||||
bool flip_h_{false};
|
bool flip_h_{false};
|
||||||
bool flip_v_{false};
|
bool flip_v_{false};
|
||||||
|
|||||||
@@ -1,45 +1,16 @@
|
|||||||
#include "game/entities/concrete/door.hpp"
|
#include "game/entities/concrete/door.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Carrega una animació d'un bloc concret (closed/opening/open) amb
|
|
||||||
// fallbacks tolerants. Si la clau no existeix, retorna una animació
|
|
||||||
// d'un sol frame loopant: no fa estat invàlid, simplement no hi ha res
|
|
||||||
// per reproduir.
|
|
||||||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
|
||||||
std::vector<int> frames = {0};
|
|
||||||
float fps = 1.0F;
|
|
||||||
bool loop = true;
|
|
||||||
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
|
||||||
const auto& anim = yaml["animations"][key];
|
|
||||||
if (anim.contains("frames")) {
|
|
||||||
frames.clear();
|
|
||||||
for (const auto& f : anim["frames"]) {
|
|
||||||
// cppcheck-suppress useStlAlgorithm
|
|
||||||
frames.push_back(f.get_value<int>());
|
|
||||||
}
|
|
||||||
if (frames.empty()) { frames.push_back(0); }
|
|
||||||
}
|
|
||||||
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
|
||||||
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
|
||||||
}
|
|
||||||
return {frames, fps, loop};
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Door::Door(const fkyaml::node& yaml)
|
Door::Door(const fkyaml::node& yaml)
|
||||||
: anim_closed_(buildAnimation(yaml, "closed")),
|
: anim_closed_(AnimationComponent::fromYaml(yaml, "closed")),
|
||||||
anim_opening_(buildAnimation(yaml, "opening")),
|
anim_opening_(AnimationComponent::fromYaml(yaml, "opening")),
|
||||||
anim_open_(buildAnimation(yaml, "open")) {
|
anim_open_(AnimationComponent::fromYaml(yaml, "open")) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("door");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("door");
|
||||||
collides_ = true;
|
collides_ = true;
|
||||||
interactable_ = true;
|
interactable_ = true;
|
||||||
@@ -99,7 +70,7 @@ void Door::open() {
|
|||||||
// placeholders defineixen `loop: false` explícitament al YAML, així
|
// placeholders defineixen `loop: false` explícitament al YAML, així
|
||||||
// que aquest fallback només actua si algú dissenya malament.
|
// que aquest fallback només actua si algú dissenya malament.
|
||||||
if (anim_.has_value() && anim_->frameCount() <= 1 && anim_->loops()) {
|
if (anim_.has_value() && anim_->frameCount() <= 1 && anim_->loops()) {
|
||||||
anim_ = AnimationComponent({0}, 6.0F, false);
|
anim_ = AnimationComponent::uniform({0}, 167.0F, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,12 @@
|
|||||||
#include "game/entities/concrete/dust.hpp"
|
#include "game/entities/concrete/dust.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Llig una animació opcional del bloc `animations` del YAML, amb
|
|
||||||
// valors per defecte segurs (1 frame, fps=1, sense loop). Si la
|
|
||||||
// clau no existeix, retornem una animació "ja completada" — així,
|
|
||||||
// un Dust spawnejat amb una variant inexistent al YAML mor al
|
|
||||||
// pròxim frame sense petar.
|
|
||||||
auto buildAnim(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
|
||||||
std::vector<int> frames = {0};
|
|
||||||
float fps = 1.0F;
|
|
||||||
bool loop = false;
|
|
||||||
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
|
||||||
const auto& anim = yaml["animations"][key];
|
|
||||||
if (anim.contains("frames")) {
|
|
||||||
frames.clear();
|
|
||||||
for (const auto& f : anim["frames"]) {
|
|
||||||
// cppcheck-suppress useStlAlgorithm
|
|
||||||
frames.push_back(f.get_value<int>());
|
|
||||||
}
|
|
||||||
if (frames.empty()) { frames.push_back(0); }
|
|
||||||
}
|
|
||||||
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
|
||||||
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
|
||||||
}
|
|
||||||
return {frames, fps, loop};
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Dust::Dust(const fkyaml::node& yaml)
|
Dust::Dust(const fkyaml::node& yaml)
|
||||||
: anim_jump_(buildAnim(yaml, "jump")),
|
: anim_jump_(AnimationComponent::fromYaml(yaml, "jump")),
|
||||||
anim_land_(buildAnim(yaml, "land")) {
|
anim_land_(AnimationComponent::fromYaml(yaml, "land")) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("dust");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("dust");
|
||||||
collides_ = false;
|
collides_ = false;
|
||||||
interactable_ = false;
|
interactable_ = false;
|
||||||
|
|||||||
@@ -29,36 +29,6 @@ namespace {
|
|||||||
return k_rng_;
|
return k_rng_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
|
|
||||||
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una
|
|
||||||
// animació d'1 frame loopant, neutra — la momia es podrà moure però
|
|
||||||
// visualment es quedarà estàtica. Llegeix `flip_h`/`flip_v` (default
|
|
||||||
// false) i els passa al component perquè el motor sàpiga aplicar-los
|
|
||||||
// al sprite quan aquesta animació estigui activa.
|
|
||||||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
|
||||||
std::vector<int> frames = {0};
|
|
||||||
float fps = 8.0F;
|
|
||||||
bool loop = true;
|
|
||||||
bool flip_h = false;
|
|
||||||
bool flip_v = false;
|
|
||||||
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
|
||||||
const auto& anim = yaml["animations"][key];
|
|
||||||
if (anim.contains("frames")) {
|
|
||||||
frames.clear();
|
|
||||||
for (const auto& f : anim["frames"]) {
|
|
||||||
// cppcheck-suppress useStlAlgorithm
|
|
||||||
frames.push_back(f.get_value<int>());
|
|
||||||
}
|
|
||||||
if (frames.empty()) { frames.push_back(0); }
|
|
||||||
}
|
|
||||||
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
|
||||||
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
|
||||||
if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value<bool>(); }
|
|
||||||
if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value<bool>(); }
|
|
||||||
}
|
|
||||||
return {frames, fps, loop, flip_h, flip_v};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca si el segment [prev, prev+delta] travessa una lane del patró
|
// Busca si el segment [prev, prev+delta] travessa una lane del patró
|
||||||
// `base + k·pitch` (k ∈ [0..max_idx]). Retorna el delta que cal aplicar
|
// `base + k·pitch` (k ∈ [0..max_idx]). Retorna el delta que cal aplicar
|
||||||
// per arribar exactament a la primera línia travessada. Iterem en el
|
// per arribar exactament a la primera línia travessada. Iterem en el
|
||||||
@@ -103,11 +73,11 @@ namespace {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Mummy::Mummy(const fkyaml::node& yaml)
|
Mummy::Mummy(const fkyaml::node& yaml)
|
||||||
: anim_walk_south_(buildAnimation(yaml, "walk_south")),
|
: anim_walk_south_(AnimationComponent::fromYaml(yaml, "walk_south")),
|
||||||
anim_walk_north_(buildAnimation(yaml, "walk_north")),
|
anim_walk_north_(AnimationComponent::fromYaml(yaml, "walk_north")),
|
||||||
anim_walk_east_(buildAnimation(yaml, "walk_east")),
|
anim_walk_east_(AnimationComponent::fromYaml(yaml, "walk_east")),
|
||||||
anim_walk_west_(buildAnimation(yaml, "walk_west")),
|
anim_walk_west_(AnimationComponent::fromYaml(yaml, "walk_west")),
|
||||||
anim_raise_(buildAnimation(yaml, "raise")) {
|
anim_raise_(AnimationComponent::fromYaml(yaml, "raise")) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
||||||
collides_ = true;
|
collides_ = true;
|
||||||
interactable_ = false;
|
interactable_ = false;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
@@ -21,26 +20,6 @@ namespace {
|
|||||||
return k_rng_;
|
return k_rng_;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
|
||||||
std::vector<int> frames = {0};
|
|
||||||
float fps = 1.0F;
|
|
||||||
bool loop = true;
|
|
||||||
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
|
||||||
const auto& anim = yaml["animations"][key];
|
|
||||||
if (anim.contains("frames")) {
|
|
||||||
frames.clear();
|
|
||||||
for (const auto& f : anim["frames"]) {
|
|
||||||
// cppcheck-suppress useStlAlgorithm
|
|
||||||
frames.push_back(f.get_value<int>());
|
|
||||||
}
|
|
||||||
if (frames.empty()) { frames.push_back(0); }
|
|
||||||
}
|
|
||||||
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
|
||||||
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
|
||||||
}
|
|
||||||
return {frames, fps, loop};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Llig `tile_size: [w, h]` del YAML. Fallback [1,1] si no existeix.
|
// Llig `tile_size: [w, h]` del YAML. Fallback [1,1] si no existeix.
|
||||||
void readTileSize(const fkyaml::node& yaml, int& w, int& h) {
|
void readTileSize(const fkyaml::node& yaml, int& w, int& h) {
|
||||||
w = 1;
|
w = 1;
|
||||||
@@ -61,9 +40,9 @@ Sarcophagus::Sarcophagus(const fkyaml::node& yaml, int anchor_col, int anchor_ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
|
Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
|
||||||
: anim_closed_(buildAnimation(yaml, "closed")),
|
: anim_closed_(AnimationComponent::fromYaml(yaml, "closed")),
|
||||||
anim_opening_(buildAnimation(yaml, "opening")),
|
anim_opening_(AnimationComponent::fromYaml(yaml, "opening")),
|
||||||
anim_open_(buildAnimation(yaml, "open")) {
|
anim_open_(AnimationComponent::fromYaml(yaml, "open")) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("sarcophagus");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("sarcophagus");
|
||||||
collides_ = true;
|
collides_ = true;
|
||||||
interactable_ = true;
|
interactable_ = true;
|
||||||
|
|||||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user