Compare commits

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

No files matched your search

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

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 164 KiB

-14
View File
@@ -90,7 +90,6 @@ void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossf
} }
// Pausa la música (l'estat el transiciona Engine::pauseMusic) // Pausa la música (l'estat el transiciona Engine::pauseMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::pauseMusic() { void Audio::pauseMusic() {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) { if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
engine_->pauseMusic(); engine_->pauseMusic();
@@ -98,7 +97,6 @@ void Audio::pauseMusic() {
} }
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic) // Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::resumeMusic() { void Audio::resumeMusic() {
if (music_enabled_ && getMusicState() == MusicState::PAUSED) { if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
engine_->resumeMusic(); engine_->resumeMusic();
@@ -106,14 +104,12 @@ void Audio::resumeMusic() {
} }
// Atura la música (l'estat el transiciona Engine::stopMusic) // Atura la música (l'estat el transiciona Engine::stopMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::stopMusic() { void Audio::stopMusic() {
if (music_enabled_) { if (music_enabled_) {
engine_->stopMusic(); engine_->stopMusic();
} }
} }
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::setMusicSpeed(float ratio) { void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) { if (music_enabled_) {
engine_->setMusicSpeed(ratio); engine_->setMusicSpeed(ratio);
@@ -121,7 +117,6 @@ void Audio::setMusicSpeed(float ratio) {
} }
// Reprodueix un so per nom // Reprodueix un so per nom
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(const std::string& name, Group group) { void Audio::playSound(const std::string& name, Group group) {
if (sound_enabled_) { if (sound_enabled_) {
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group)); engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
@@ -129,7 +124,6 @@ void Audio::playSound(const std::string& name, Group group) {
} }
// Reprodueix un so per punter directe // Reprodueix un so per punter directe
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(Ja::Sound* sound, Group group) { void Audio::playSound(Ja::Sound* sound, Group group) {
if (sound_enabled_ && sound != nullptr) { if (sound_enabled_ && sound != nullptr) {
engine_->playSound(sound, 0, static_cast<int>(group)); engine_->playSound(sound, 0, static_cast<int>(group));
@@ -141,7 +135,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
// processa tot el sample amb el ratio des del primer pull del callback. // processa tot el sample amb el ratio des del primer pull del callback.
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem // Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// la crida al ratio — sense efectes col·laterals. // la crida al ratio — sense efectes col·laterals.
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::playSound(const std::string& name, Group group, float speed) { void Audio::playSound(const std::string& name, Group group, float speed) {
if (!sound_enabled_) { return; } if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
@@ -155,7 +148,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) {
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no // Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound // existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
// sec — l'usuari sent el so encara que la cua no s'apliqui. // sec — l'usuari sent el so encara que la cua no s'apliqui.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) { void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; } if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
@@ -175,7 +167,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix // Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
// fallback que playSoundWithEcho. // fallback que playSoundWithEcho.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) { void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; } if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
@@ -194,7 +185,6 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
} }
// Atura tots els sons // Atura tots els sons
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::stopAllSounds() { void Audio::stopAllSounds() {
if (sound_enabled_) { if (sound_enabled_) {
engine_->stopChannel(-1); engine_->stopChannel(-1);
@@ -202,7 +192,6 @@ void Audio::stopAllSounds() {
} }
// Fa una fosa de sortida de la música // Fa una fosa de sortida de la música
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::fadeOutMusic(int milliseconds) { void Audio::fadeOutMusic(int milliseconds) {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) { if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
engine_->fadeOutMusic(milliseconds); engine_->fadeOutMusic(milliseconds);
@@ -212,7 +201,6 @@ void Audio::fadeOutMusic(int milliseconds) {
// Registra un callback que el motor dispararà quan la pista actual acabi de // Registra un callback que el motor dispararà quan la pista actual acabi de
// drenar (times == 0 + stream buit). S'executa al mateix thread que // drenar (times == 0 + stream buit). S'executa al mateix thread que
// Audio::update (render loop); els consumidors no poden fer I/O blocant. // Audio::update (render loop); els consumidors no poden fer I/O blocant.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::setOnMusicEnded(std::function<void()> callback) { void Audio::setOnMusicEnded(std::function<void()> callback) {
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); } if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
} }
@@ -251,13 +239,11 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
} }
// Estableix el volum dels sons (float 0.0..1.0) // Estableix el volum dels sons (float 0.0..1.0)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::setSoundVolume(float sound_volume, Group group) { void Audio::setSoundVolume(float sound_volume, Group group) {
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group)); engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
} }
// Estableix el volum de la música (float 0.0..1.0) // Estableix el volum de la música (float 0.0..1.0)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::setMusicVolume(float music_volume) { void Audio::setMusicVolume(float music_volume) {
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_)); engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
} }
-1
View File
@@ -5,7 +5,6 @@
#include <functional> // Per a std::function #include <functional> // Per a std::function
#include <memory> // Per a std::unique_ptr #include <memory> // Per a std::unique_ptr
#include <string> // Per a string #include <string> // Per a string
#include <utility> // Per a move
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els // Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
// tres símbols (Music/Sound per al punter que exposa la API i Engine per al // tres símbols (Music/Sound per al punter que exposa la API i Engine per al
+1 -1
View File
@@ -15,7 +15,7 @@
// (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi). // (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi).
#define STB_VORBIS_HEADER_ONLY #define STB_VORBIS_HEADER_ONLY
// clang-format off // clang-format off
// NOLINTNEXTLINE(bugprone-suspicious-include) // NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis és single-file: la macro de dalt limita aquest TU a només-declaracions; la implementació viu a external/stb_vorbis_impl.cpp.
#include "external/stb_vorbis.c" #include "external/stb_vorbis.c"
// clang-format on // clang-format on
+16 -8
View File
@@ -198,42 +198,50 @@ void DefineButtons::render() const {
// Títol: nom del mando. // Títol: nom del mando.
const std::string TITLE = pad_name_; const std::string TITLE = pad_name_;
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X, CENTER_X,
cursor_y, cursor_y,
TITLE, TITLE,
TITLE_KERNING, TITLE_KERNING,
UiConfig::get().menuTitle()); UiConfig::get().menuTitle(),
1,
UiConfig::get().menuTitleOutline());
cursor_y += TITLE_H + LINE_GAP; cursor_y += TITLE_H + LINE_GAP;
if (message_shown_) { if (message_shown_) {
// Pantalla final: missatge de configuració completa. // Pantalla final: missatge de configuració completa.
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X, CENTER_X,
cursor_y, cursor_y,
Locale::get().text("menu.values.define_buttons_complete"), Locale::get().text("menu.values.define_buttons_complete"),
ITEM_KERNING, ITEM_KERNING,
UiConfig::get().menuValue()); UiConfig::get().menuValue(),
1,
UiConfig::get().menuValueOutline());
return; return;
} }
// Instrucció + acció pendent. Replica visualment el WindowMessage de // Instrucció + acció pendent. Replica visualment el WindowMessage de
// coffee_crisis_arcade_edition. // coffee_crisis_arcade_edition.
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X, CENTER_X,
cursor_y, cursor_y,
Locale::get().text("menu.values.press_button"), Locale::get().text("menu.values.press_button"),
ITEM_KERNING, ITEM_KERNING,
UiConfig::get().menuLabel()); UiConfig::get().menuLabel(),
1,
UiConfig::get().menuLabelOutline());
cursor_y += ITEM_H + LINE_GAP; cursor_y += ITEM_H + LINE_GAP;
if (index_ < sequence_.size()) { if (index_ < sequence_.size()) {
const auto& current = sequence_[index_]; const auto& current = sequence_[index_];
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X, CENTER_X,
cursor_y, cursor_y,
Locale::get().text(current.label_key), Locale::get().text(current.label_key),
ITEM_KERNING, ITEM_KERNING,
UiConfig::get().menuCursor()); UiConfig::get().menuCursor(),
1,
UiConfig::get().menuCursorOutline());
} }
} }
-1
View File
@@ -1,6 +1,5 @@
#include "core/input/gamepad_bindings.hpp" #include "core/input/gamepad_bindings.hpp"
#include <algorithm>
#include <array> #include <array>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
+2 -3
View File
@@ -14,9 +14,8 @@
// //
// El polling real de botons i eixos NO es fa a través d'aquesta classe: // El polling real de botons i eixos NO es fa a través d'aquesta classe:
// els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a // els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a
// multi-jugador futur) i el passen a `InputState::getGamepadButton/Axis`, // multi-jugador futur) i el passen directament a
// que ho filtra pel flag de bloqueig de la UI (mateixa estratègia que // `SDL_GetGamepadButton`/`SDL_GetGamepadAxis`.
// amb el teclat).
class GamepadManager { class GamepadManager {
public: public:
static auto get() -> GamepadManager&; static auto get() -> GamepadManager&;
-2
View File
@@ -9,8 +9,6 @@
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp" #include "core/rendering/notification_presets.hpp"
#include "core/rendering/notifier.hpp"
#include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/screenshot.hpp" #include "core/rendering/screenshot.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
-39
View File
@@ -1,39 +0,0 @@
#include "core/input/input_state.hpp"
#include <array>
namespace InputState {
namespace {
bool blocked = false;
// Buffer retornat quan el bloqueig és actiu. `SDL_GetKeyboardState`
// retorna un pointer d'una mida igual a `SDL_SCANCODE_COUNT`; el
// nostre buffer ha de ser com a mínim igual de gran perquè els
// consumidors puguin indexar-lo amb qualsevol scancode sense
// llegir fora de rang.
auto zeroKeyboard() -> const bool* {
static const std::array<bool, SDL_SCANCODE_COUNT> ZEROS{};
return ZEROS.data();
}
} // namespace
void setBlocked(bool value) { blocked = value; }
auto isBlocked() -> bool { return blocked; }
auto getKeyboardState() -> const bool* {
return blocked ? zeroKeyboard() : SDL_GetKeyboardState(nullptr);
}
auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool {
return (blocked || gp == nullptr) ? false : SDL_GetGamepadButton(gp, b);
}
auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16 {
return (blocked || gp == nullptr) ? static_cast<Sint16>(0) : SDL_GetGamepadAxis(gp, ax);
}
} // namespace InputState
-46
View File
@@ -1,46 +0,0 @@
#pragma once
// Wrapper de l'estat d'input (polling) amb gating de bloqueig.
//
// El service menu (i qualsevol UI modal futura) ha de poder "amagar" l'estat
// d'entrada als consumidors de polling — el jugador, entitats controlades
// per l'usuari, etc. — mentre la UI captura l'atenció, sense interrompre la
// resta del joc. Per això tots els consumidors de polling han de llegir a
// través d'aquest mòdul i no cridar `SDL_GetKeyboardState` directament. Si
// `setBlocked(true)` està actiu, el wrapper retorna un buffer tot-a-zero;
// altrament, la crida és transparent (mateix pointer que SDL).
//
// El menú NO passa per aquí: consumeix events (`SDL_EVENT_KEY_DOWN`) abans
// que l'escena els vegi, així que la seva navegació segueix funcionant per
// molt que el flag estigui posat.
//
// El mateix patró aplica al gamepad: `getGamepadButton`/`getGamepadAxis`
// retornen zero/false quan el bloqueig és actiu. Els mandos concrets els
// obtén el caller via `GamepadManager::get().getPrimary()` o similars.
#include <SDL3/SDL.h>
namespace InputState {
// Activa/desactiva el bloqueig global de polling. El ServiceMenu el
// crida a `toggle()`/`close()`. No es toca des de cap altre lloc.
void setBlocked(bool value);
[[nodiscard]] auto isBlocked() -> bool;
// Equivalent a `SDL_GetKeyboardState(nullptr)`: retorna un buffer de
// `SDL_SCANCODE_COUNT` bools. Si l'input està bloquejat, tots zero —
// el consumidor veu com si cap tecla estigués premuda, sense branques
// explícites. El buffer és estable mentre dura la sessió; no cal
// guardar-lo frame a frame.
[[nodiscard]] auto getKeyboardState() -> const bool*;
// Versió filtrada de `SDL_GetGamepadButton`. Retorna false si el bloqueig
// és actiu o si `gp` és nullptr (cas habitual: no hi ha cap mand).
[[nodiscard]] auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool;
// Versió filtrada de `SDL_GetGamepadAxis`. Retorna 0 si el bloqueig
// és actiu o si `gp` és nullptr.
[[nodiscard]] auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16;
} // namespace InputState
-1
View File
@@ -1,6 +1,5 @@
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include <fstream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
-1
View File
@@ -1,6 +1,5 @@
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include <fstream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <cstdint>
#include <random> #include <random>
class Surface; class Surface;
-1
View File
@@ -4,7 +4,6 @@
#include <algorithm> #include <algorithm>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
namespace Effects::Shadow { namespace Effects::Shadow {
+181 -39
View File
@@ -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
+102 -34
View File
@@ -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
+1
View File
@@ -4,6 +4,7 @@
#include <numeric> #include <numeric>
#include <utility> #include <utility>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/ui/ui_config.hpp" #include "core/ui/ui_config.hpp"
-2
View File
@@ -7,9 +7,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "utils/defines.hpp"
class Text; class Text;
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <array> #include <array>
#include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
-1
View File
@@ -7,7 +7,6 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <memory> #include <memory>
#include <ranges>
#include "core/rendering/canvas.hpp" #include "core/rendering/canvas.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp" #include "core/rendering/sdl3gpu/viewport.hpp"
+1 -1
View File
@@ -17,7 +17,7 @@ namespace Screenshot {
// Estat de mòdul: ruta base on s'escriu la subcarpeta // Estat de mòdul: ruta base on s'escriu la subcarpeta
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback // `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
// si el Director no ha pogut establir la ruta per plataforma). // si el Director no ha pogut establir la ruta per plataforma).
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) -- estat de mòdul (singleton-like): s'estableix una vegada al startup via setBaseDir() i es llegeix N vegades; encapsulat dins del namespace anònim, no és accessible des de fora.
// Construeix la ruta del directori on van les captures: // Construeix la ruta del directori on van les captures:
// `<base>/screenshots/`. Si la base és buida, retorna // `<base>/screenshots/`. Si la base és buida, retorna
@@ -22,7 +22,7 @@ struct PostVOut {
struct PostFXUniforms { struct PostFXUniforms {
float vignette_strength; float vignette_strength;
float chroma_strength; float chroma_min;
float scanline_strength; float scanline_strength;
float screen_height; float screen_height;
float mask_strength; float mask_strength;
@@ -32,7 +32,7 @@ struct PostFXUniforms {
float pixel_scale; float pixel_scale;
float time; float time;
float flicker; float flicker;
float pad2; float chroma_max;
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML) // vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio; float scan_dark_ratio;
float scan_dark_floor; float scan_dark_floor;
@@ -40,6 +40,18 @@ struct PostFXUniforms {
float pad3; float pad3;
}; };
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel.
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
float2 tex_size = float2(scene.get_width(), scene.get_height());
float px = uv_target.x * tex_size.x - 0.5f;
float p_floor = floor(px);
float f = px - p_floor;
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
static float3 rgb_to_ycc(float3 rgb) { static float3 rgb_to_ycc(float3 rgb) {
return float3( return float3(
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b, 0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
@@ -94,9 +106,14 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
colour = base; colour = base;
} }
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f)); // Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r; // queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b; // NEAREST sampler amb offsets subpíxel.
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
}
if (u.gamma_strength > 0.0f) { if (u.gamma_strength > 0.0f) {
float3 lin = pow(colour, float3(2.4f)); float3 lin = pow(colour, float3(2.4f));
@@ -234,7 +234,8 @@ namespace Rendering {
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) { void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
uniforms_.vignette_strength = params.vignette; uniforms_.vignette_strength = params.vignette;
uniforms_.scanline_strength = params.scanlines; uniforms_.scanline_strength = params.scanlines;
uniforms_.chroma_strength = params.chroma; uniforms_.chroma_min = params.chroma_min;
uniforms_.chroma_max = params.chroma_max;
uniforms_.mask_strength = params.mask; uniforms_.mask_strength = params.mask;
uniforms_.gamma_strength = params.gamma; uniforms_.gamma_strength = params.gamma;
uniforms_.curvature = params.curvature; uniforms_.curvature = params.curvature;
@@ -15,7 +15,7 @@
struct PostFXUniforms { struct PostFXUniforms {
// vec4 #0 // vec4 #0
float vignette_strength; float vignette_strength;
float chroma_strength; float chroma_min;
float scanline_strength; float scanline_strength;
float screen_height; float screen_height;
// vec4 #1 // vec4 #1
@@ -27,7 +27,7 @@ struct PostFXUniforms {
float pixel_scale; float pixel_scale;
float time; float time;
float flicker; float flicker;
float pad2; float chroma_max;
// vec4 #3 — paràmetres de scanlines exposats al preset YAML // vec4 #3 — paràmetres de scanlines exposats al preset YAML
float scan_dark_ratio; float scan_dark_ratio;
float scan_dark_floor; float scan_dark_floor;
@@ -92,7 +92,7 @@ namespace Rendering {
PostFXUniforms uniforms_{ PostFXUniforms uniforms_{
.vignette_strength = 0.0F, .vignette_strength = 0.0F,
.chroma_strength = 0.0F, .chroma_min = 0.0F,
.scanline_strength = 0.0F, .scanline_strength = 0.0F,
.screen_height = 0.0F, .screen_height = 0.0F,
.mask_strength = 0.0F, .mask_strength = 0.0F,
@@ -102,7 +102,7 @@ namespace Rendering {
.pixel_scale = 1.0F, .pixel_scale = 1.0F,
.time = 0.0F, .time = 0.0F,
.flicker = 0.0F, .flicker = 0.0F,
.pad2 = 0.0F, .chroma_max = 0.0F,
// Defaults canònics: 2/3 brillant + 1/3 fosca, brillantor 42%, suavitzat complet (1 px físic). // Defaults canònics: 2/3 brillant + 1/3 fosca, brillantor 42%, suavitzat complet (1 px físic).
.scan_dark_ratio = 0.333F, .scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F, .scan_dark_floor = 0.42F,
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -19,7 +19,10 @@ namespace Rendering {
struct PostFXParams { struct PostFXParams {
float vignette{0.0F}; float vignette{0.0F};
float scanlines{0.0F}; float scanlines{0.0F};
float chroma{0.0F}; // Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
// queda estàtica. Permet un "floor" perquè la imatge mai sigui lliure de chroma.
float chroma_min{0.0F};
float chroma_max{0.0F};
float mask{0.0F}; float mask{0.0F};
float gamma{0.0F}; float gamma{0.0F};
float curvature{0.0F}; float curvature{0.0F};
+1 -1
View File
@@ -62,7 +62,7 @@ Surface::Surface(const std::string& file_path) {
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i]; palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
} }
printWithDots("Surface", getFileName(file_path), "[ LOADED ]"); printStatus("Surface", getFileName(file_path), StatusKind::SUCCESS);
} }
void Surface::clear(Uint8 color) { void Surface::clear(Uint8 color) {
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <array> #include <array>
#include <cstdint>
#include <memory> #include <memory>
#include <numeric> #include <numeric>
#include <string> #include <string>
+1 -1
View File
@@ -101,7 +101,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
} }
} }
printWithDots("Text File", getFileName(file_path), "[ LOADED ]"); printStatus("Text File", getFileName(file_path), StatusKind::SUCCESS);
return tf; return tf;
} }
+4 -4
View File
@@ -37,13 +37,13 @@ namespace Resource {
throw std::runtime_error("Cache: cannot load animation " + file_path); throw std::runtime_error("Cache: cannot load animation " + file_path);
} }
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)}); animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
printWithDots("Animation", getFileName(file_path), "[ LOADED ]"); printStatus("Animation", getFileName(file_path), StatusKind::SUCCESS);
} }
void Cache::loadPalette(const std::string& name, const std::string& file_path) { void Cache::loadPalette(const std::string& name, const std::string& file_path) {
auto palette = PaletteLoader::loadFromFile(file_path); auto palette = PaletteLoader::loadFromFile(file_path);
palettes_.emplace_back(PaletteResource{.name = name, .palette = palette}); palettes_.emplace_back(PaletteResource{.name = name, .palette = palette});
printWithDots("Palette", getFileName(file_path), "[ LOADED ]"); printStatus("Palette", getFileName(file_path), StatusKind::SUCCESS);
} }
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> { auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
@@ -83,7 +83,7 @@ namespace Resource {
throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path); throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path);
} }
musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)}); musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)});
printWithDots("Music", getFileName(file_path), "[ LOADED ]"); printStatus("Music", getFileName(file_path), StatusKind::SUCCESS);
} }
void Cache::loadSound(const std::string& name, const std::string& file_path) { void Cache::loadSound(const std::string& name, const std::string& file_path) {
@@ -96,7 +96,7 @@ namespace Resource {
throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path); throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path);
} }
sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)}); sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)});
printWithDots("Sound", getFileName(file_path), "[ LOADED ]"); printStatus("Sound", getFileName(file_path), StatusKind::SUCCESS);
} }
auto Cache::getMusic(const std::string& name) -> Ja::Music* { auto Cache::getMusic(const std::string& name) -> Ja::Music* {
+3 -7
View File
@@ -4,6 +4,7 @@
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <numeric>
#include "utils/log.hpp" #include "utils/log.hpp"
@@ -19,12 +20,7 @@ namespace Resource {
auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t { auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t {
// djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai // djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai
// reutilitzem eines de diagnòstic entre projectes. // reutilitzem eines de diagnòstic entre projectes.
std::uint32_t checksum = 0x12345678; return std::accumulate(data.begin(), data.end(), std::uint32_t{0x12345678}, [](std::uint32_t acc, unsigned char b) { return ((acc << 5) + acc) + b; });
for (unsigned char b : data) {
// cppcheck-suppress useStlAlgorithm
checksum = ((checksum << 5) + checksum) + b;
}
return checksum;
} }
void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) { void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) {
@@ -151,7 +147,7 @@ namespace Resource {
return false; return false;
} }
// cppcheck-suppress useStlAlgorithm // cppcheck-suppress useStlAlgorithm -- bucle amb early-return en error (addFile fallit) + filtre amb continue: no es pot expressar net amb std::ranges::all_of perquè la lògica combina filtratge i acumulació amb sortida prematura.
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
if (!entry.is_regular_file()) { continue; } if (!entry.is_regular_file()) { continue; }
+13 -24
View File
@@ -9,7 +9,6 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
#include <variant>
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/audio/sound_effects_config.hpp" #include "core/audio/sound_effects_config.hpp"
@@ -24,7 +23,6 @@
#include "core/rendering/render_info.hpp" #include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/screenshot.hpp" #include "core/rendering/screenshot.hpp"
#include "core/rendering/surface.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
@@ -35,8 +33,8 @@
#include "game/gameplay_config.hpp" #include "game/gameplay_config.hpp"
#ifdef _DEBUG #ifdef _DEBUG
#include "game/editor/editor_config.hpp" #include "game/editor/editor_config.hpp"
#endif
#include "game/levels/level_manager.hpp" #include "game/levels/level_manager.hpp"
#endif
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/game_scene.hpp" #include "game/scenes/game_scene.hpp"
#include "game/scenes/intro_scene.hpp" #include "game/scenes/intro_scene.hpp"
@@ -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: {
+51
View File
@@ -0,0 +1,51 @@
// relaunch.cpp - Implementació del reinici en calent
#include "core/system/relaunch.hpp"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <iostream>
#ifdef _WIN32
#include <process.h> // _execv
#else
#include <unistd.h> // execv
#endif
namespace {
// Estat global (process-scope). Aquesta TU és l'única que gestiona el
// reinici, així que els statics interns no s'escapen.
char** g_argv = nullptr;
bool g_requested = false;
} // namespace
namespace System::Relaunch {
void setArgv(int /*argc*/, char** argv) {
g_argv = argv;
}
void request() {
g_requested = true;
}
void execIfRequested() {
if (!g_requested || g_argv == nullptr || g_argv[0] == nullptr) {
return;
}
std::cout << "[Relaunch] Reiniciant " << g_argv[0] << "...\n";
#ifdef _WIN32
_execv(g_argv[0], g_argv);
#else
execv(g_argv[0], g_argv);
#endif
// Si arribem aquí, execv ha fallat. Tots els subsistemes ja estan
// destruïts; sortim amb error i el shell rebrà el codi.
std::cerr << "[Relaunch] Ha fallat: " << std::strerror(errno) << '\n';
std::exit(EXIT_FAILURE);
}
} // namespace System::Relaunch
+28
View File
@@ -0,0 +1,28 @@
// relaunch.hpp - Reinici en calent del procés (execv)
//
// Helper desacoblat perquè el menú de servei demani un reinici sense conèixer
// ni Director ni main.cpp. Patró d'ús:
//
// main() → Relaunch::setArgv(argc, argv) (a l'arrencada)
// ServiceMenu → Relaunch::request() (en activar "Reiniciar")
// main() → Relaunch::execIfRequested() (al final de SDL_AppQuit)
//
// L'execv() reemplaça el procés actual: si torna, ha fallat. Es crida després
// de destruir el Director per garantir que GPU/àudio/SDL ja són morts.
#pragma once
namespace System::Relaunch {
// Emmagatzema l'argv original. Cal cridar-ho una vegada des de main.
void setArgv(int argc, char** argv);
// Demana un reinici (no actua immediatament; només marca el flag).
void request();
// Si hi ha reinici demanat i tenim argv vàlid, fa execv. En cas d'èxit
// no torna. Si execv falla, registra l'error per stderr i surt amb
// EXIT_FAILURE (no podem reprendre — els subsistemes ja estan destruïts).
void execIfRequested();
} // namespace System::Relaunch
+30 -7
View File
@@ -81,6 +81,10 @@ UiConfig::UiConfig()
menu_value_(Defaults::UI::MENU_VALUE), menu_value_(Defaults::UI::MENU_VALUE),
menu_cursor_(Defaults::UI::MENU_CURSOR), menu_cursor_(Defaults::UI::MENU_CURSOR),
menu_disabled_(Defaults::UI::MENU_DISABLED), menu_disabled_(Defaults::UI::MENU_DISABLED),
menu_title_outline_(Defaults::UI::MENU_TITLE_OUTLINE),
menu_label_outline_(Defaults::UI::MENU_LABEL_OUTLINE),
menu_value_outline_(Defaults::UI::MENU_VALUE_OUTLINE),
menu_cursor_outline_(Defaults::UI::MENU_CURSOR_OUTLINE),
hud_bg_(Defaults::UI::HUD_BG), hud_bg_(Defaults::UI::HUD_BG),
hud_p1_label_(Defaults::UI::HUD_P1_LABEL), hud_p1_label_(Defaults::UI::HUD_P1_LABEL),
hud_p1_value_(Defaults::UI::HUD_P1_VALUE), hud_p1_value_(Defaults::UI::HUD_P1_VALUE),
@@ -215,19 +219,38 @@ void UiConfig::load(const std::string& file_path) {
readColorIndex(c, "notification_bg", notification_bg_); readColorIndex(c, "notification_bg", notification_bg_);
readColorIndex(c, "exit_text", exit_text_); readColorIndex(c, "exit_text", exit_text_);
readColorIndex(c, "exit_bg", exit_bg_); readColorIndex(c, "exit_bg", exit_bg_);
readColorIndex(c, "menu_bg", menu_bg_);
readColorIndex(c, "menu_border", menu_border_);
readColorIndex(c, "menu_title", menu_title_);
readColorIndex(c, "menu_label", menu_label_);
readColorIndex(c, "menu_value", menu_value_);
readColorIndex(c, "menu_cursor", menu_cursor_);
readColorIndex(c, "menu_disabled", menu_disabled_);
readColorIndex(c, "hud_bg", hud_bg_); readColorIndex(c, "hud_bg", hud_bg_);
readColorIndex(c, "hud_flash", hud_flash_); readColorIndex(c, "hud_flash", hud_flash_);
readColorIndex(c, "room_text", room_text_); readColorIndex(c, "room_text", room_text_);
readColorIndex(c, "room_text_outline", room_text_outline_); readColorIndex(c, "room_text_outline", room_text_outline_);
} }
// Tema actiu del menú: `menu_theme` és el nom del bloc dins de
// `menu_themes` que aporta els 7 slots de color. Si qualsevol
// de les dues claus manca o el nom no existeix, els slots
// queden amb el default compilat (= tema "classic" hardcoded
// a Defaults::UI). Per-key dins del tema també és opcional.
if (yaml.contains("menu_theme") && yaml.contains("menu_themes")) {
const auto THEME_NAME = yaml["menu_theme"].get_value<std::string>();
const auto& themes = yaml["menu_themes"];
if (themes.contains(THEME_NAME)) {
const auto& t = themes[THEME_NAME];
readColorIndex(t, "bg", menu_bg_);
readColorIndex(t, "border", menu_border_);
readColorIndex(t, "title", menu_title_);
readColorIndex(t, "label", menu_label_);
readColorIndex(t, "value", menu_value_);
readColorIndex(t, "cursor", menu_cursor_);
readColorIndex(t, "disabled", menu_disabled_);
readColorIndex(t, "title_outline", menu_title_outline_);
readColorIndex(t, "label_outline", menu_label_outline_);
readColorIndex(t, "value_outline", menu_value_outline_);
readColorIndex(t, "cursor_outline", menu_cursor_outline_);
} else {
Log::warn("UiConfig", "menu_theme '{}' no existeix a menu_themes — s'usen defaults", THEME_NAME);
}
}
// Famílies de color del HUD: blocs `hud_p1`, `hud_center`, // Famílies de color del HUD: blocs `hud_p1`, `hud_center`,
// `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i // `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i
// cada clau són opcionals — el que manqui cau al default // cada clau són opcionals — el que manqui cau al default
+8
View File
@@ -65,6 +65,10 @@ class UiConfig {
[[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; } [[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; }
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; } [[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; } [[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
[[nodiscard]] auto menuTitleOutline() const -> Uint8 { return menu_title_outline_; }
[[nodiscard]] auto menuLabelOutline() const -> Uint8 { return menu_label_outline_; }
[[nodiscard]] auto menuValueOutline() const -> Uint8 { return menu_value_outline_; }
[[nodiscard]] auto menuCursorOutline() const -> Uint8 { return menu_cursor_outline_; }
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; } [[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
// Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada // Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada
// família té label (etiqueta apagada), value (valor destacat) i // família té label (etiqueta apagada), value (valor destacat) i
@@ -214,6 +218,10 @@ class UiConfig {
Uint8 menu_value_; Uint8 menu_value_;
Uint8 menu_cursor_; Uint8 menu_cursor_;
Uint8 menu_disabled_; Uint8 menu_disabled_;
Uint8 menu_title_outline_;
Uint8 menu_label_outline_;
Uint8 menu_value_outline_;
Uint8 menu_cursor_outline_;
Uint8 hud_bg_; Uint8 hud_bg_;
Uint8 hud_p1_label_; Uint8 hud_p1_label_;
Uint8 hud_p1_value_; Uint8 hud_p1_value_;
+1 -3
View File
@@ -345,9 +345,7 @@ namespace {
} // namespace } // namespace
// cppcheck-suppress constParameterReference // cppcheck-suppress constParameterReference -- Tilemap& mutable per simetria amb `toggle()` i per futures invalidacions de caches d'edició; cppcheck no pot anticipar que les futures mutacions estan previstes en el contracte.
// Tilemap& mutable per simetria amb `toggle()` i per futures
// invalidacions de caches d'edició, encara que `save()` sigui const.
void Editor::exit(Tilemap& tilemap, const EntityManager& entities) { void Editor::exit(Tilemap& tilemap, const EntityManager& entities) {
if (!active_) { return; } if (!active_) { return; }
// Constraint del nivell: cal exactament una porta i com a mínim un // Constraint del nivell: cal exactament una porta i com a mínim un
+114 -7
View File
@@ -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_) {
+37 -8
View File
@@ -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};
+4 -33
View File
@@ -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);
} }
} }
+2 -32
View File
@@ -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;
+5 -35
View File
@@ -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;
+3 -24
View File
@@ -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