Compare commits

...
64 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
JailDesigner bc6ad6f718 chore(version): bump 0.43.0 -> 0.44.0 2026-05-12 22:41:43 +02:00
JailDesigner 9bd5e7db33 Merge branch 'feature/lighting' 2026-05-12 22:40:55 +02:00
JailDesigner 5cc8e349a5 feat(lighting): seed estable per emissor (no deriva pel moviment) 2026-05-12 22:37:27 +02:00
JailDesigner 0046d526b0 fix(lighting): llig flicker tambe del YAML del jugador 2026-05-12 22:26:17 +02:00
JailDesigner fc9947b87f feat(data): flicker tambe a jugadors i momies 2026-05-12 22:17:36 +02:00
JailDesigner 35aaa6c519 feat(lighting): flicker tipus foc per antorxes 2026-05-12 22:07:25 +02:00
JailDesigner 3195746189 feat(data): activa la llum del jugador, antorxes i mòmies + sala test 2026-05-12 21:14:16 +02:00
JailDesigner d1d5679d14 feat(game_scene): wire lighting pass entre actors i HUD 2026-05-12 21:12:06 +02:00
JailDesigner ebb161f1f5 feat(entities): light_radius + getLightCenter a Entity i Player 2026-05-12 21:06:01 +02:00
JailDesigner 68be8e55ec feat(map): Map::light_level per sala des del YAML 2026-05-12 20:58:30 +02:00
JailDesigner 5ed963b53d feat(rendering): lighting module CPU + gradient config YAML 2026-05-12 20:56:27 +02:00
JailDesigner 5e5ff835a9 Merge branch 'feature/player-stats-bus' 2026-05-12 19:55:50 +02:00
JailDesigner f5d251c33b feat(stats): Player com a font de veritat de vides/score/tresors i sarcs amb recompensa 2026-05-12 19:51:42 +02:00
JailDesigner d186f66c2c art(player): paletes d'immunitat per-personatge (wash, ice, gold) 2026-05-12 19:10:00 +02:00
JailDesigner da361a1452 feat(editor): mostra mommies i altres dynamic hazards al mode editor 2026-05-12 13:58:45 +02:00
JailDesigner a43ae21014 feat(editor): preview del brush d'entitat i fons del picker mes clar 2026-05-12 13:35:01 +02:00
JailDesigner 6749780356 feat(entities): afegeix stone i column com a muros decoratius 2026-05-12 13:03:35 +02:00
JailDesigner 1c49248b72 art(debris): afegeix column.gif i stone.gif 2026-05-12 11:06:42 +02:00
JailDesigner 63a03ae632 refactor(pause): elimina pausa F11 (cobreix-la el menú de servei) 2026-05-12 10:52:46 +02:00
JailDesigner a8bff31a7c chore(fonts): regenera ari-w9500 sense accents 2026-05-12 10:39:10 +02:00
JailDesigner 793e87a203 feat(hud): parpadeig configurable als prompts inactius 2026-05-12 10:27:23 +02:00
JailDesigner 642e0653b2 fix(sarcophagus): inclou les cantonades diagonals al perímetre 2026-05-12 09:00:56 +02:00
165 changed files with 13431 additions and 9444 deletions

No files matched your search

+7 -2
View File
@@ -52,6 +52,7 @@ set(ENTITY_SOURCES_BASE
source/game/entities/concrete/floor_trap.cpp
source/game/entities/concrete/hole.cpp
source/game/entities/concrete/dust.cpp
source/game/entities/concrete/solid_props.cpp
)
# Entitats que criden directament símbols de Player (com WhipAttack via
@@ -64,6 +65,7 @@ set(ENTITY_SOURCES
set(APP_SOURCES
source/main.cpp
source/core/system/director.cpp
source/core/system/relaunch.cpp
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
source/core/audio/audio_effects.cpp
@@ -73,13 +75,13 @@ set(APP_SOURCES
source/core/ui/ui_config.cpp
source/core/input/key_config.cpp
source/core/input/global_inputs.cpp
source/core/input/input_state.cpp
source/core/input/gamepad_manager.cpp
source/core/input/gamepad_bindings.cpp
source/core/input/define_buttons.cpp
source/core/input/mouse.cpp
source/core/rendering/screen.cpp
source/core/rendering/canvas.cpp
source/core/rendering/lighting.cpp
source/core/rendering/notifier.cpp
source/core/rendering/render_info.cpp
source/core/rendering/gif.cpp
@@ -129,6 +131,7 @@ set(APP_SOURCES
# al TilemapWriter. Només Debug.
source/game/map/map_writer.cpp
source/game/map/paint_layer.cpp
source/game/map/room_yaml.cpp
source/game/levels/level_manager.cpp
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
# En Release els fitxers compilen a object buit i l'enllaçador no
@@ -202,7 +205,7 @@ add_executable(${PROJECT_NAME} ${APP_SOURCES})
set_source_files_properties(
source/external/stb_image_write_impl.cpp
source/external/stb_vorbis_impl.cpp
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations"
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations;-Wno-tautological-compare"
)
if(NOT APPLE AND GLSLC_EXE)
@@ -372,6 +375,7 @@ if(BUILD_TESTS)
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/map/map.cpp
source/game/map/room_yaml.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE}
)
@@ -403,6 +407,7 @@ if(BUILD_TESTS)
source/core/resources/resource_pack.cpp
source/game/gameplay_config.cpp
source/game/map/map.cpp
source/game/map/room_yaml.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp
+14 -7
View File
@@ -27,8 +27,15 @@ ifeq ($(OS),Windows_NT)
else
NULDEV := /dev/null
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
# Si Ninja està al PATH, l'usem (més ràpid i paral·lelitza millor).
# Si no, deixem que CMake usi el generador per defecte (Unix Makefiles).
HAS_NINJA := $(shell ninja --version 2>/dev/null)
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN :=
endif
endif
# VERSION (única font de veritat: source/utils/defines.hpp).
# Lazy (=): només es calcula quan s'invoca un target que la usa
@@ -84,7 +91,7 @@ else
MKDIR := mkdir -p
endif
.PHONY: all debug release _windows_release _macos_release _linux_release run run-debug clean rebuild show-version pack controllerdb format format-check tidy tidy-fix cppcheck hooks-install help
.PHONY: all debug release _windows-release _macos-release _linux-release run run-debug clean rebuild show-version pack controllerdb format format-check tidy tidy-fix cppcheck hooks-install help
# ==============================================================================
# COMPILACIÓ
@@ -115,19 +122,19 @@ run-debug: debug
# resultat a $(DIST_DIR)/.
release:
ifeq ($(OS),Windows_NT)
@"$(MAKE)" _windows_release
@"$(MAKE)" _windows-release
else
ifeq ($(UNAME_S),Darwin)
@$(MAKE) _macos_release
@$(MAKE) _macos-release
else
@$(MAKE) _linux_release
@$(MAKE) _linux-release
endif
endif
# ==============================================================================
# RELEASE — Linux (.tar.gz)
# ==============================================================================
_linux_release:
_linux-release:
@echo "Creando release para Linux - Version: $(VERSION)"
# Compila Release (genera shaders, resources.pack i binari)
@@ -158,7 +165,7 @@ _linux_release:
# ==============================================================================
# RELEASE — Windows (.zip)
# ==============================================================================
_windows_release:
_windows-release:
@echo off
@echo Creando release para Windows - Version: $(VERSION)
@@ -191,7 +198,7 @@ _windows_release:
# ==============================================================================
# RELEASE — macOS (.dmg per Intel + Apple Silicon)
# ==============================================================================
_macos_release:
_macos-release:
@echo "Creando release para macOS - Version: $(VERSION)"
# Verificar/instal·lar create-dmg si cal
+2 -2
View File
@@ -8,6 +8,6 @@ picker:
# Colors ARGB en hex. Accepta enter (0xFF2244FF) o cadena ("#FF2244FF").
colors:
border: "0xFF2244FF" # vora 1 px que envolta canvas i picker
picker_bg: "0xFF101824" # fons del picker
border: "0xFF6A86C8" # vora 1 px que envolta canvas i picker (blau suau)
picker_bg: "0xFFC8C0D8" # fons del picker (lavanda clara)
picker_hover: "0xFFFFFFFF" # vora 1 px del tile sota el cursor
-3
View File
@@ -35,9 +35,6 @@ keys:
- id: toggle_render_info
code: "F10"
desc: "Cicle de l'overlay de debug: apagat / dalt / baix"
- id: pause
code: "F11"
desc: "Pausa el joc (o botó START del mando)"
- id: menu_toggle
code: "F12"
desc: "Obri / tanca el menú de servei"
+56
View File
@@ -0,0 +1,56 @@
# Configuració global del sistema d'il·luminació.
#
# Cada llum emissora (player, torxa, mòmia, ...) té un únic paràmetre
# propi: el radi en píxels. La forma de la llum dins d'aquest radi
# (els tres anells del gradient) viu en aquest fitxer perquè és una
# decisió de "feel" comuna a totes les entitats — així pots tocar la
# corba aquí sense haver de modificar 10 YAMLs.
#
# Forma del gradient (centre → vora):
# 0..inner_ratio del radi → inner_intensity (zona totalment il·luminada)
# inner_ratio..mid_ratio → mid_intensity (anell mig)
# mid_ratio..1.0 → outer_intensity (anell exterior)
# fora del radi → light_level de la sala
#
# Hi ha dues corbes paral·leles, una per mode, perquè la mateixa escala
# 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:
inner_ratio: 0.5
mid_ratio: 0.85
inner_intensity: 1.0
mid_intensity: 0.8
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
# bloc `light:`. El radi efectiu per frame és:
# 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 seed de l'emissor per a que cada
# antorxa balancegi en una fase diferent (sense estat per emissor).
# Les amplituds són **ràtios** sobre el radi base: així el feel del
# flicker és independent del radi (una torxa de 24 px oscil·la el
# 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:
wave_amplitude_ratio: 0.05 # 5% del radi (per a radi 40 → ±2 px, feel actual)
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025 # 2.5% (per a radi 40 → ±1 px, feel actual)
noise_frequency_hz: 7.0
+49 -18
View File
@@ -9,13 +9,6 @@ colors:
notification_bg: 64
exit_text: 71
exit_bg: 2
menu_bg: 2
menu_border: 232
menu_title: 237
menu_label: 58
menu_value: 232
menu_cursor: 71
menu_disabled: 16
hud_bg: 9
hud_flash: 63
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
@@ -27,6 +20,49 @@ colors:
room_text: 55 # or brillant (#FFDA00, = hud_center.value)
room_text_outline: 10 # marró cremat (#482400, = hud_center.shadow)
# Tema actiu del menú de servei. Ha de coincidir amb una clau de
# `menu_themes`; si manca o no existeix, cada slot cau al default
# compilat a Defaults::UI (= tema "classic"). Canviar de tema requereix
# reiniciar el joc — la càrrega de paleta es fa només en arrencar.
menu_theme: egyptian
# Conjunts de colors del menú de servei. Cada tema defineix els 7 slots
# (sense prefix `menu_` perquè ja som dins del bloc del tema). Si manca
# alguna clau dins d'un tema, cau al default compilat. Per afegir un
# tema nou, només cal una nova entrada aquí i posar-ne el nom a
# `menu_theme`.
menu_themes:
# Tema històric (defaults compilats). Cian + verd llima + magenta —
# paleta arcade clàssica, freda i amb molt contrast.
classic:
bg: 2 # marró-vermell fosc (#480000)
border: 232 # cian brillant (#00B6FF)
title: 237 # lavanda (#B6B6FF)
title_outline: 64 # navy fosc (#0000AA) — halo fred del títol
label: 58 # verd llima (#48FF00)
label_outline: 17 # verd molt fosc (#244800) — bord same-family del label
value: 232 # cian brillant (#00B6FF) — coincideix amb el border
value_outline: 64 # navy fosc (#0000AA) — halo cian
cursor: 71 # magenta-vermell (#FF0055)
cursor_outline: 1 # granat fosc (#240000) — halo càlid del cursor
disabled: 16 # verd fosc (#004800)
# Tema "egípci": família or/ambre/marró coherent amb el HUD i el
# rètol de transició de sala (room_text). El bg comparteix to amb
# `room_text_outline` i `hud_center.shadow` perquè el menú "creixi"
# del mateix món visual que el joc.
egyptian:
bg: 10 # marró cremat (#482400)
border: 55 # or brillant (#FFDA00)
title: 127 # groc llimona viu (#FFFF55)
title_outline: 19 # oliva fosc (#6D4800) — halo càlid del groc
label: 47 # ambre (#FFB600)
label_outline: 11 # marró (#6D2400) — bord càlid del label
value: 191 # crema clar (#FFFFAA)
value_outline: 19 # oliva fosc (#6D4800) — recolza el crema
cursor: 23 # vermilló (#FF4800)
cursor_outline: 9 # marró molt fosc (#240000) — halo profund del cursor
disabled: 19 # oliva fosc (#6D4800)
timings:
notification_ms: 4000
exit_ms: 4000
@@ -73,6 +109,12 @@ hud:
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
flash_ms: 250 # durada del flash quan un jugador rep un hit
shake_ms: 300 # durada del shake que acompanya el flash
# Parpadeig dels prompts inactius ("Polsa start..." / "Espera..."). Un
# únic temporitzador global al Hud sincronitza P1 i P2 i no es reinicia
# quan el text canvia entre CAN_JOIN i WAIT. cycle<=0 o ratio>=1 →
# sempre visible (disable trivial).
blink_cycle_ms: 1000 # durada del cicle complet
blink_visible_ratio: 0.8 # proporció del cicle visible (0..1)
# Famílies de color del HUD per bloc. Cada bloc té `label` (to apagat
# per etiquetes), `value` (to viu per a la xifra) i `shadow` (to fosc
@@ -108,17 +150,6 @@ notifier:
slide_in_curve: outCubic
slide_out_curve: inCubic
# Pausa graduada de la GameScene: el món no para d'un colp, s'atura amb
# easing i torna a arrancar amb una altra corba. Els timings es poden
# afinar independentment, i les corbes permeten estils diferents entrant
# i sortint (p.ex. frenar suau i arrancar seca). Corbes disponibles:
# linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
pause:
enter_ms: 400
exit_ms: 250
enter_curve: outCubic
exit_curve: inCubic
# Sons del menú de servei. Els fitxers viuen a data/audio/sounds/. SELECT
# acompanya navegació (amunt/avall) i back; ACCEPT acompanya modificació
# d'opció (esquerra/dreta/retorn) o entrada a un submenú.
Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

+22
View File
@@ -0,0 +1,22 @@
name: column
type: decoration
editable: true
# Sub-picker de frames a l'editor: 6 variants (atlas 2×3 de sub-sprites 16×32).
editable_variants: true
sprite:
path: column.gif
size: [16, 32]
# 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
# z-order (la caixa lògica és el tile inferior).
anchor: feet
tile_size: [1, 1]
collision:
size: [16, 16]
# Mur infranquejable al salt: el jugador no pot passar pel damunt.
height: 999
flags:
collides: true
interactable: false
animations:
default: { frames: [0], frame_ms: 1000, loop: false }
+3 -3
View File
@@ -21,9 +21,9 @@ flags:
collides: true
interactable: true
animations:
closed: { frames: [1], fps: 1, loop: true }
opening: { frames: [0], fps: 6, loop: false }
open: { frames: [0], fps: 1, loop: true }
closed: { frames: [1], frame_ms: 1000, loop: true }
opening: { frames: [0], frame_ms: 167, loop: false }
open: { frames: [0], frame_ms: 1000, loop: true }
behaviour:
states: [CLOSED, OPENING, OPEN]
initial_state: CLOSED
+3 -3
View File
@@ -16,7 +16,7 @@ flags:
# apropiada via `setVariant` abans del primer tick.
# jump: frames 0..6 (7 frames) → reproduït al despegue.
# 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:
jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 15, loop: false }
land: { frames: [7, 8, 9, 10, 11, 12], 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], frame_ms: 67, loop: false }
+2 -2
View File
@@ -13,8 +13,8 @@ flags:
collides: false
interactable: false
animations:
inactive: { frames: [0], fps: 1, loop: true }
active: { frames: [0], fps: 6, loop: true }
inactive: { frames: [0], frame_ms: 1000, loop: true }
active: { frames: [0], frame_ms: 167, loop: true }
cycle:
active_duration: 0.3
inactive_duration: 2.0
+1 -1
View File
@@ -22,6 +22,6 @@ flags:
# lectura genèrica de flags.
triggers_on_overlap: true
animations:
idle: { frames: [0], fps: 1, loop: true }
idle: { frames: [0], frame_ms: 1000, loop: true }
stats:
damage: 0
+19 -5
View File
@@ -32,11 +32,18 @@ flags:
# no decideix flip per direcció (només selecciona quina animació mostrar).
# La momia no té variants `stand_*` perquè mai no para.
animations:
walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true }
walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true }
walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true }
walk_west: { frames: [8, 9, 10, 11], fps: 8, 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 }
walk_south: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true }
walk_north: { frames: [4, 5, 6, 7], frame_ms: 125, loop: true }
walk_east: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true }
walk_west: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true, flip_h: true }
# 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:
type: corridor_wanderer
direction_choice: random
@@ -68,3 +75,10 @@ hurt:
# So opcional: basename del fitxer dins `data/audio/sounds/`. Es dispara
# un sol cop per fuetada quan la mòmia entra a HURT. Si manca, no sona.
sound: mummy_hurt.wav
# Halo tènue al voltant de la mòmia. Radi més curt que el del jugador
# perquè el rastre lumínic delati la seva posició sense "il·luminar
# la sala" — vol ser una pista visual a la foscor.
light:
radius: 24
flicker: true
-35
View File
@@ -1,35 +0,0 @@
JASC-PAL
0100
32
0 0 0
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
+9 -1
View File
@@ -139,4 +139,12 @@ death:
immunity:
duration_ms: 3000
blink_period_ms: 100
palette: immune
palette: morcus_wash
# Llum emesa pel jugador. `radius` és el radi del cercle de llum
# en píxels del canvas; el gradient (3 anells: 1.0 / 0.9 / 0.8) viu
# a data/config/lighting.yaml i és compartit per tots els emissors.
# Omet el bloc per desactivar la llum d'aquest personatge.
light:
radius: 64
flicker: true
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
150 133 88 255
253 233 185 255
237 215 165 255
214 193 145 255
255 252 208 255
252 234 189 255
239 222 176 255
175 190 128 255
173 175 115 255
159 159 104 255
246 211 157 255
229 192 139 255
213 177 127 255
243 226 184 255
0 0 0 255
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
102 118 132 255
226 238 247 255
206 216 222 255
179 190 198 255
245 255 255 255
223 239 253 255
208 224 238 255
124 190 178 255
125 170 162 255
109 151 150 255
221 210 210 255
200 187 189 255
181 169 175 255
213 229 247 255
0 0 0 255
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
190 190 190 255
255 255 255 255
255 248 239 255
239 233 226 255
255 255 255 255
255 255 255 255
254 254 254 255
184 244 213 255
193 229 202 255
184 216 198 255
255 242 225 255
255 228 213 255
251 216 207 255
255 255 255 255
0 0 0 255
+9 -1
View File
@@ -139,4 +139,12 @@ death:
immunity:
duration_ms: 3000
blink_period_ms: 100
palette: immune
palette: sam_wash
# Llum emesa pel jugador. `radius` és el radi del cercle de llum
# en píxels del canvas; el gradient (3 anells: 1.0 / 0.9 / 0.8) viu
# a data/config/lighting.yaml i és compartit per tots els emissors.
# Omet el bloc per desactivar la llum d'aquest personatge.
light:
radius: 64
flicker: true
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
150 133 88 255
202 176 123 255
191 165 112 255
181 156 103 255
217 158 115 255
194 142 101 255
188 143 98 255
214 193 145 255
196 176 128 255
181 161 115 255
246 211 157 255
229 192 139 255
213 177 127 255
243 226 184 255
0 0 0 255
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
102 118 132 255
166 169 171 255
152 156 158 255
140 144 147 255
191 144 160 255
161 125 146 255
152 127 142 255
179 190 198 255
157 168 179 255
139 151 164 255
221 210 210 255
200 187 189 255
181 169 175 255
213 229 247 255
0 0 0 255
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
190 190 190 255
235 220 205 255
226 212 197 255
219 205 191 255
255 194 197 255
248 184 193 255
238 188 189 255
239 233 226 255
226 220 216 255
214 210 208 255
255 242 225 255
255 228 213 255
251 216 207 255
255 255 255 255
0 0 0 255
+1 -1
View File
@@ -13,7 +13,7 @@ flags:
collides: true
interactable: false
animations:
roll: { frames: [0], fps: 10, loop: true }
roll: { frames: [0], frame_ms: 100, loop: true }
ai:
type: straight_line
initial_direction: right
+9 -3
View File
@@ -19,9 +19,9 @@ flags:
collides: true
interactable: true
animations:
closed: { frames: [0], fps: 1, loop: true }
opening: { frames: [1, 2], fps: 6, loop: false }
open: { frames: [3], fps: 1, loop: true }
closed: { frames: [0], frame_ms: 1000, loop: true }
opening: { frames: [1, 2], frame_ms: 167, loop: false }
open: { frames: [3], frame_ms: 1000, loop: true }
behaviour:
states: [CLOSED, OPENING, OPEN]
initial_state: CLOSED
@@ -40,3 +40,9 @@ behaviour:
sound:
opening: open_sarc.wav
opening_pitch_spread: 0.5
# Recompensa que rep el jugador que pinta el tile que tanca el perímetre
# (el GameScene la reparteix consumint `sarcophagus_opened`). Si es lleva
# aquest bloc o un dels camps, el camp absent val 0 i no s'aplica.
reward:
points: 100
treasures: 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

+22
View File
@@ -0,0 +1,22 @@
name: stone
type: decoration
editable: true
# Sub-picker de frames a l'editor: ofereix una graella amb les 24 variants
# del atlas (6×4 sub-sprites 16×16) per escollir quina representa cada
# instància col·locada.
editable_variants: true
sprite:
path: stone.gif
size: [16, 16]
tile_size: [1, 1]
collision:
size: [16, 16]
# Mur infranquejable al salt: el jugador no la pot saltar per damunt.
height: 999
flags:
collides: true
interactable: false
# Animació "default" amb un sol frame: el spawn YAML pot sobreescriure-la
# via `frame: N` per fixar la variant escollida al editor.
animations:
default: { frames: [0], frame_ms: 1000, loop: false }
+16 -1
View File
@@ -15,4 +15,19 @@ flags:
# 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.
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;
# el gradient global (data/config/lighting.yaml) hi posa la forma (3
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame
# (suma de dues sinusoides amb fase derivada de la posició) per a
# que el cercle "respiri" com una flama; els paràmetres del flicker
# també viuen al config global. `offset: [x, y]` desplaça el centre
# 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:
radius: 40
flicker: true
offset: [0, -4]
+2 -2
View File
@@ -13,8 +13,8 @@ flags:
collides: false
interactable: false
animations:
inactive: { frames: [0], fps: 1, loop: true }
active: { frames: [0], fps: 6, loop: true }
inactive: { frames: [0], frame_ms: 1000, loop: true }
active: { frames: [0], frame_ms: 167, loop: true }
cycle:
active_duration: 0.5
inactive_duration: 1.5
+7 -7
View File
@@ -2,7 +2,7 @@
# Generado con font_gen.py
box_width 12
box_height 14
box_height 13
columns 15
# codepoint_decimal ancho_visual
@@ -105,8 +105,8 @@ columns 15
193 8 # Á
200 7 # È
201 7 # É
204 4 # Ì
205 4 # Í
204 3 # Ì
205 3 # Í
210 9 # Ò
211 9 # Ó
217 7 # Ù
@@ -117,8 +117,8 @@ columns 15
225 7 # á
232 7 # è
233 7 # é
236 4 # ì
237 4 # í
236 3 # ì
237 3 # í
242 7 # ò
243 7 # ó
249 7 # ù
@@ -127,7 +127,7 @@ columns 15
231 7 # ç
161 3 # ¡
191 7 # ¿
171 10 # «
187 10 # »
171 7 # «
187 7 # »
183 3 # ·
169 10 # ©
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

+7 -7
View File
@@ -2,7 +2,7 @@
# Generado con font_gen.py
box_width 12
box_height 14
box_height 13
columns 15
# codepoint_decimal ancho_visual
@@ -105,8 +105,8 @@ columns 15
193 9 # Á
200 8 # È
201 8 # É
204 4 # Ì
205 4 # Í
204 3 # Ì
205 3 # Í
210 10 # Ò
211 10 # Ó
217 8 # Ù
@@ -117,8 +117,8 @@ columns 15
225 8 # á
232 8 # è
233 8 # é
236 4 # ì
237 4 # í
236 3 # ì
237 3 # í
242 8 # ò
243 8 # ó
249 8 # ù
@@ -127,7 +127,7 @@ columns 15
231 8 # ç
161 3 # ¡
191 8 # ¿
171 10 # «
187 10 # »
171 7 # «
187 7 # »
183 3 # ·
169 10 # ©
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

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

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 164 KiB

-14
View File
@@ -90,7 +90,6 @@ void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossf
}
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::pauseMusic() {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
engine_->pauseMusic();
@@ -98,7 +97,6 @@ void Audio::pauseMusic() {
}
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::resumeMusic() {
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
engine_->resumeMusic();
@@ -106,14 +104,12 @@ void Audio::resumeMusic() {
}
// Atura la música (l'estat el transiciona Engine::stopMusic)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::stopMusic() {
if (music_enabled_) {
engine_->stopMusic();
}
}
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) {
engine_->setMusicSpeed(ratio);
@@ -121,7 +117,6 @@ void Audio::setMusicSpeed(float ratio) {
}
// Reprodueix un so per nom
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(const std::string& name, Group group) {
if (sound_enabled_) {
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
@@ -129,7 +124,6 @@ void Audio::playSound(const std::string& name, Group group) {
}
// Reprodueix un so per punter directe
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(Ja::Sound* sound, Group group) {
if (sound_enabled_ && sound != nullptr) {
engine_->playSound(sound, 0, static_cast<int>(group));
@@ -141,7 +135,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
// processa tot el sample amb el ratio des del primer pull del callback.
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// la crida al ratio — sense efectes col·laterals.
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::playSound(const std::string& name, Group group, float speed) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
@@ -155,7 +148,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) {
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
// sec — l'usuari sent el so encara que la cua no s'apliqui.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
@@ -175,7 +167,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
// fallback que playSoundWithEcho.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
@@ -194,7 +185,6 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
}
// Atura tots els sons
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::stopAllSounds() {
if (sound_enabled_) {
engine_->stopChannel(-1);
@@ -202,7 +192,6 @@ void Audio::stopAllSounds() {
}
// Fa una fosa de sortida de la música
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::fadeOutMusic(int milliseconds) {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
engine_->fadeOutMusic(milliseconds);
@@ -212,7 +201,6 @@ void Audio::fadeOutMusic(int milliseconds) {
// Registra un callback que el motor dispararà quan la pista actual acabi de
// drenar (times == 0 + stream buit). S'executa al mateix thread que
// Audio::update (render loop); els consumidors no poden fer I/O blocant.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::setOnMusicEnded(std::function<void()> callback) {
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
}
@@ -251,13 +239,11 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
}
// Estableix el volum dels sons (float 0.0..1.0)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::setSoundVolume(float sound_volume, Group group) {
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
}
// Estableix el volum de la música (float 0.0..1.0)
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::setMusicVolume(float music_volume) {
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
}
-1
View File
@@ -5,7 +5,6 @@
#include <functional> // Per a std::function
#include <memory> // Per a std::unique_ptr
#include <string> // Per a string
#include <utility> // Per a move
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
// tres símbols (Music/Sound per al punter que exposa la API i Engine per al
+1 -1
View File
@@ -15,7 +15,7 @@
// (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi).
#define STB_VORBIS_HEADER_ONLY
// clang-format off
// NOLINTNEXTLINE(bugprone-suspicious-include)
// NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis és single-file: la macro de dalt limita aquest TU a només-declaracions; la implementació viu a external/stb_vorbis_impl.cpp.
#include "external/stb_vorbis.c"
// clang-format on
+16 -8
View File
@@ -198,42 +198,50 @@ void DefineButtons::render() const {
// Títol: nom del mando.
const std::string TITLE = pad_name_;
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X,
cursor_y,
TITLE,
TITLE_KERNING,
UiConfig::get().menuTitle());
UiConfig::get().menuTitle(),
1,
UiConfig::get().menuTitleOutline());
cursor_y += TITLE_H + LINE_GAP;
if (message_shown_) {
// Pantalla final: missatge de configuració completa.
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X,
cursor_y,
Locale::get().text("menu.values.define_buttons_complete"),
ITEM_KERNING,
UiConfig::get().menuValue());
UiConfig::get().menuValue(),
1,
UiConfig::get().menuValueOutline());
return;
}
// Instrucció + acció pendent. Replica visualment el WindowMessage de
// coffee_crisis_arcade_edition.
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X,
cursor_y,
Locale::get().text("menu.values.press_button"),
ITEM_KERNING,
UiConfig::get().menuLabel());
UiConfig::get().menuLabel(),
1,
UiConfig::get().menuLabelOutline());
cursor_y += ITEM_H + LINE_GAP;
if (index_ < sequence_.size()) {
const auto& current = sequence_[index_];
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG,
CENTER_X,
cursor_y,
Locale::get().text(current.label_key),
ITEM_KERNING,
UiConfig::get().menuCursor());
UiConfig::get().menuCursor(),
1,
UiConfig::get().menuCursorOutline());
}
}
-1
View File
@@ -1,6 +1,5 @@
#include "core/input/gamepad_bindings.hpp"
#include <algorithm>
#include <array>
#include <fstream>
#include <sstream>
+2 -3
View File
@@ -14,9 +14,8 @@
//
// El polling real de botons i eixos NO es fa a través d'aquesta classe:
// els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a
// multi-jugador futur) i el passen a `InputState::getGamepadButton/Axis`,
// que ho filtra pel flag de bloqueig de la UI (mateixa estratègia que
// amb el teclat).
// multi-jugador futur) i el passen directament a
// `SDL_GetGamepadButton`/`SDL_GetGamepadAxis`.
class GamepadManager {
public:
static auto get() -> GamepadManager&;
-2
View File
@@ -9,8 +9,6 @@
#include "core/input/key_config.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp"
#include "core/rendering/notifier.hpp"
#include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/screenshot.hpp"
#include "core/system/event_buffer.hpp"
-39
View File
@@ -1,39 +0,0 @@
#include "core/input/input_state.hpp"
#include <array>
namespace InputState {
namespace {
bool blocked = false;
// Buffer retornat quan el bloqueig és actiu. `SDL_GetKeyboardState`
// retorna un pointer d'una mida igual a `SDL_SCANCODE_COUNT`; el
// nostre buffer ha de ser com a mínim igual de gran perquè els
// consumidors puguin indexar-lo amb qualsevol scancode sense
// llegir fora de rang.
auto zeroKeyboard() -> const bool* {
static const std::array<bool, SDL_SCANCODE_COUNT> ZEROS{};
return ZEROS.data();
}
} // namespace
void setBlocked(bool value) { blocked = value; }
auto isBlocked() -> bool { return blocked; }
auto getKeyboardState() -> const bool* {
return blocked ? zeroKeyboard() : SDL_GetKeyboardState(nullptr);
}
auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool {
return (blocked || gp == nullptr) ? false : SDL_GetGamepadButton(gp, b);
}
auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16 {
return (blocked || gp == nullptr) ? static_cast<Sint16>(0) : SDL_GetGamepadAxis(gp, ax);
}
} // namespace InputState
-46
View File
@@ -1,46 +0,0 @@
#pragma once
// Wrapper de l'estat d'input (polling) amb gating de bloqueig.
//
// El service menu (i qualsevol UI modal futura) ha de poder "amagar" l'estat
// d'entrada als consumidors de polling — el jugador, entitats controlades
// per l'usuari, etc. — mentre la UI captura l'atenció, sense interrompre la
// resta del joc. Per això tots els consumidors de polling han de llegir a
// través d'aquest mòdul i no cridar `SDL_GetKeyboardState` directament. Si
// `setBlocked(true)` està actiu, el wrapper retorna un buffer tot-a-zero;
// altrament, la crida és transparent (mateix pointer que SDL).
//
// El menú NO passa per aquí: consumeix events (`SDL_EVENT_KEY_DOWN`) abans
// que l'escena els vegi, així que la seva navegació segueix funcionant per
// molt que el flag estigui posat.
//
// El mateix patró aplica al gamepad: `getGamepadButton`/`getGamepadAxis`
// retornen zero/false quan el bloqueig és actiu. Els mandos concrets els
// obtén el caller via `GamepadManager::get().getPrimary()` o similars.
#include <SDL3/SDL.h>
namespace InputState {
// Activa/desactiva el bloqueig global de polling. El ServiceMenu el
// crida a `toggle()`/`close()`. No es toca des de cap altre lloc.
void setBlocked(bool value);
[[nodiscard]] auto isBlocked() -> bool;
// Equivalent a `SDL_GetKeyboardState(nullptr)`: retorna un buffer de
// `SDL_SCANCODE_COUNT` bools. Si l'input està bloquejat, tots zero —
// el consumidor veu com si cap tecla estigués premuda, sense branques
// explícites. El buffer és estable mentre dura la sessió; no cal
// guardar-lo frame a frame.
[[nodiscard]] auto getKeyboardState() -> const bool*;
// Versió filtrada de `SDL_GetGamepadButton`. Retorna false si el bloqueig
// és actiu o si `gp` és nullptr (cas habitual: no hi ha cap mand).
[[nodiscard]] auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool;
// Versió filtrada de `SDL_GetGamepadAxis`. Retorna 0 si el bloqueig
// és actiu o si `gp` és nullptr.
[[nodiscard]] auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16;
} // namespace InputState
-1
View File
@@ -1,6 +1,5 @@
#include "core/input/key_config.hpp"
#include <fstream>
#include <string>
#include "core/resources/resource_helper.hpp"
-1
View File
@@ -1,6 +1,5 @@
#include "core/locale/locale.hpp"
#include <fstream>
#include <string>
#include "core/resources/resource_helper.hpp"
@@ -1,6 +1,5 @@
#pragma once
#include <cstdint>
#include <random>
class Surface;
-1
View File
@@ -4,7 +4,6 @@
#include <algorithm>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
namespace Effects::Shadow {
+376
View File
@@ -0,0 +1,376 @@
#include "core/rendering/lighting.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <numbers>
#include <vector>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace Lighting {
namespace {
// Buffer de luminància compartit entre frames. Es redimensiona
// només si canvia la mida del canvas (mai en un run normal: el
// canvas del joc és 320×240 fix). El reusem perquè una `apply`
// allotjant cada frame seria un cost evitable.
auto luminanceBuffer() -> std::vector<std::uint8_t>& {
static std::vector<std::uint8_t> buf_;
return buf_;
}
// Corba activa del mode subtractive. loadConfig() la reemplaça
// si troba el bloc `gradient:` al YAML.
auto activeSubtractiveConfig() -> GradientConfig& {
static GradientConfig cfg_{
.inner_ratio = Defaults::Lighting::INNER_RATIO,
.mid_ratio = Defaults::Lighting::MID_RATIO,
.inner_intensity = Defaults::Lighting::INNER_INTENSITY,
.mid_intensity = Defaults::Lighting::MID_INTENSITY,
.outer_intensity = Defaults::Lighting::OUTER_INTENSITY,
};
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& {
static FlickerConfig fc_{
.wave_amplitude_ratio = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_RATIO,
.wave_frequency_hz = Defaults::Lighting::FLICKER_WAVE_FREQUENCY_HZ,
.noise_amplitude_ratio = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_RATIO,
.noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ,
};
return fc_;
}
// Modula el radi base d'un emissor amb la suma de dues sinusoides
// (`wave` + `noise`) i una fase derivada de `seed`. La seed és un
// identificador estable de l'emissor (no canvia amb la posició) —
// així el patró no "deriva" quan l'emissor es mou. El factor
// ~φ (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 {
constexpr float TWO_PI = 2.0F * std::numbers::pi_v<float>;
constexpr float GOLDEN_FRACTION = 0.6180339F;
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 W2 = std::sin((TWO_PI * fc.noise_frequency_hz * t_s) + (PHASE * 2.7F));
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));
return std::max(1, R);
}
// Quantitza un factor 0..1 a 0..255. `clamp` per defensar-nos de
// YAMLs que entreguen valors fora de rang sense forçar la mà.
auto toByte(float v) -> std::uint8_t {
const float CLAMPED = std::clamp(v, 0.0F, 1.0F);
return static_cast<std::uint8_t>(std::lround(CLAMPED * 255.0F));
}
void readField(const fkyaml::node& node, const char* key, float& dst) {
if (node.contains(key)) { dst = node[key].get_value<float>(); }
}
// Pre-calcula els 3 quadrats de radi i les intensitats que farem
// servir dins del bucle per píxel. Es resol una sola vegada per
// emissor i s'evita arrastrar 6 paràmetres a `rasterizeEmitter`.
struct EmitterRings {
int radius_sq{0};
int mid_sq{0};
int inner_sq{0};
std::uint8_t inner{0};
std::uint8_t mid{0};
std::uint8_t outer{0};
};
auto computeRings(int radius, const GradientConfig& cfg) -> EmitterRings {
const auto R_F = static_cast<float>(radius);
const float R_INNER = R_F * cfg.inner_ratio;
const float R_MID = R_F * cfg.mid_ratio;
return EmitterRings{
.radius_sq = radius * radius,
.mid_sq = static_cast<int>(R_MID * R_MID),
.inner_sq = static_cast<int>(R_INNER * R_INNER),
.inner = toByte(cfg.inner_intensity),
.mid = toByte(cfg.mid_intensity),
.outer = toByte(cfg.outer_intensity),
};
}
// Itera la bounding box de l'emissor clipada al canvas i actualitza
// la fila de luminància corresponent amb `max(actual, ring)`. Es
// crida un cop per emissor, així extreure-la deixa `apply` simple.
void rasterizeEmitter(const Emitter& e, const EmitterRings& rings, std::uint8_t* lum, 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::uint8_t* row = lum + (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;
}
row[x] = std::max(intensity, row[x]);
}
}
}
// Pasada final del mode subtractive: pixel = pixel * lum / 255
// per canal. El canal alpha es preserva (la resta del pipeline
// assumeix opacitat total al canvas; veure Rendering::OPAQUE_ALPHA).
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
for (int y = 0; y < height; ++y) {
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
const std::uint8_t* lrow = lum + (static_cast<std::ptrdiff_t>(y) * width);
for (int x = 0; x < width; ++x) {
const std::uint8_t L = lrow[x];
if (L == 255) { continue; } // píxel intacte.
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 * L) / 255);
const auto G = static_cast<std::uint8_t>((SG * L) / 255);
const auto B = static_cast<std::uint8_t>((SB * L) / 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);
}
}
}
// 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
auto config() -> const GradientConfig& {
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) {
auto bytes = Resource::Helper::loadFile(yaml_path);
if (bytes.empty()) {
Log::warn("Lighting", "no s'ha pogut obrir {} — s'usaran els defaults compilats", yaml_path);
return;
}
try {
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (yaml.contains("gradient")) {
readGradient(yaml["gradient"], activeSubtractiveConfig());
}
if (yaml.contains("gradient_additive")) {
readGradient(yaml["gradient_additive"], activeAdditiveConfig());
}
if (yaml.contains("flicker")) {
const auto& f = yaml["flicker"];
auto& fc = activeFlicker();
readField(f, "wave_amplitude_ratio", fc.wave_amplitude_ratio);
readField(f, "wave_frequency_hz", fc.wave_frequency_hz);
readField(f, "noise_amplitude_ratio", fc.noise_amplitude_ratio);
readField(f, "noise_frequency_hz", fc.noise_frequency_hz);
}
} catch (const std::exception& ex) {
Log::warn("Lighting", "error parsejant {}: {} — s'usaran els defaults compilats", yaml_path, ex.what());
}
}
namespace {
// Helper: emissor "patxat" amb el radi efectiu (post-flicker) per
// a aquest frame. Mantenim l'`Emitter` del caller intacte i la
// resta de camps (color, seed, flicker_params, ...) propagats.
auto withEffectiveRadius(const Emitter& e, int r_eff) -> Emitter {
return Emitter{
.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();
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
std::ranges::fill(lum, room);
const auto& cfg = activeSubtractiveConfig();
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);
rasterizeEmitter(EFF, RINGS, 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
+150
View File
@@ -0,0 +1,150 @@
#pragma once
#include <cstdint>
#include <span>
#include <string>
// 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
// haja pintat tilemap, paint layer, sombres i actors, una pasada final
// modifica cada píxel del canvas en funció de la "llum de la sala" i
// dels cercles emissors al voltant de certes entitats.
//
// Dos modes seleccionables per la sala (`lighting.mode` al YAML):
//
// 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
// inner_ratio..mid_ratio → mid_intensity
// mid_ratio..1.0 → outer_intensity
//
// La corba viu en dos blocs separats al YAML perquè la mateixa escala
// 0..1 té significats molt diferents per mode:
// `gradient:` → corba del mode subtractive (1.0 = "no fosqueix")
// `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 {
// Mode de composició. La sala el selecciona via `lighting.mode:
// additive | subtractive` (default subtractive si manca, retrocompat).
enum class Mode : std::uint8_t {
SUBTRACTIVE,
ADDITIVE,
};
// Color emès. Default blanc (sense modificació quan s'usa en mode
// additive). En mode subtractive s'ignora — la pasada multiplicativa
// només té un canal (intensitat). El YAML accepta `color: [r, g, b]`
// a qualsevol nivell de la cadena de resolució (tipus, sala, spawn).
struct Color {
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
// duplicar la corba a cada YAML d'entitat. Càrrega via loadConfig.
struct GradientConfig {
float inner_ratio{0.8F};
float mid_ratio{0.9F};
float inner_intensity{1.0F};
float mid_intensity{0.9F};
float outer_intensity{0.8F};
};
// Paràmetres del flicker per a emissors amb `flicker=true`. La
// forma final del radi és la suma d'una sinusoide "respiració"
// (wave_*) i una sinusoide secundària de freqüència més alta
// (noise_*) que aporta la sensació d'inquietud. Les fases es
// 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 {
float wave_amplitude_ratio{0.05F};
float wave_frequency_hz{3.0F};
float noise_amplitude_ratio{0.025F};
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
// 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
// simplement reemplaça la configuració activa.
void loadConfig(const std::string& yaml_path);
// 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.
[[nodiscard]] auto config() -> const GradientConfig&;
// Defaults globals del flicker (data/config/lighting.yaml, bloc
// `flicker:`). Els callers del construct path d'entitats el llegeixen
// per inicialitzar la `FlickerConfig` de cada emissor, i aplicar a
// sobre els overrides per tipus/sala/spawn.
[[nodiscard]] auto flickerConfig() -> const FlickerConfig&;
// 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
+1
View File
@@ -4,6 +4,7 @@
#include <numeric>
#include <utility>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp"
#include "core/ui/ui_config.hpp"
-2
View File
@@ -7,9 +7,7 @@
#include <string>
#include <vector>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/surface.hpp"
#include "utils/defines.hpp"
class Text;
-1
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h>
#include <array>
#include <cstdint>
#include <memory>
#include <string>
-1
View File
@@ -7,7 +7,6 @@
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <ranges>
#include "core/rendering/canvas.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp"
+1 -1
View File
@@ -72,7 +72,7 @@ class Screen {
// Alpha-blend sobre el canvas: `alpha=0` no fa res, `alpha=255` equival
// a fillRect. El color entra com a ARGB little-endian; `alpha` és el
// factor de mescla amb el que ja hi ha al canvas. Pensat per a overlays
// (menú, pauses) que volen veure's a través el joc atenuat.
// (menú de servei) que volen veure's a través el joc atenuat.
void blendRect(int x, int y, int w, int h, std::uint32_t argb, std::uint8_t alpha);
// Hook invocat al principi de `present()`, abans de pujar els píxels al
// backend. Serveix per a pintar overlays que han de quedar per damunt
+1 -1
View File
@@ -17,7 +17,7 @@ namespace Screenshot {
// Estat de mòdul: ruta base on s'escriu la subcarpeta
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
// si el Director no ha pogut establir la ruta per plataforma).
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables)
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) -- estat de mòdul (singleton-like): s'estableix una vegada al startup via setBaseDir() i es llegeix N vegades; encapsulat dins del namespace anònim, no és accessible des de fora.
// Construeix la ruta del directori on van les captures:
// `<base>/screenshots/`. Si la base és buida, retorna
@@ -22,7 +22,7 @@ struct PostVOut {
struct PostFXUniforms {
float vignette_strength;
float chroma_strength;
float chroma_min;
float scanline_strength;
float screen_height;
float mask_strength;
@@ -32,7 +32,7 @@ struct PostFXUniforms {
float pixel_scale;
float time;
float flicker;
float pad2;
float chroma_max;
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio;
float scan_dark_floor;
@@ -40,6 +40,18 @@ struct PostFXUniforms {
float pad3;
};
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel.
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
float2 tex_size = float2(scene.get_width(), scene.get_height());
float px = uv_target.x * tex_size.x - 0.5f;
float p_floor = floor(px);
float f = px - p_floor;
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
static float3 rgb_to_ycc(float3 rgb) {
return float3(
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
@@ -94,9 +106,14 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
colour = base;
}
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f));
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r;
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b;
// Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
// queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
// NEAREST sampler amb offsets subpíxel.
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
}
if (u.gamma_strength > 0.0f) {
float3 lin = pow(colour, float3(2.4f));
@@ -234,7 +234,8 @@ namespace Rendering {
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
uniforms_.vignette_strength = params.vignette;
uniforms_.scanline_strength = params.scanlines;
uniforms_.chroma_strength = params.chroma;
uniforms_.chroma_min = params.chroma_min;
uniforms_.chroma_max = params.chroma_max;
uniforms_.mask_strength = params.mask;
uniforms_.gamma_strength = params.gamma;
uniforms_.curvature = params.curvature;
@@ -15,7 +15,7 @@
struct PostFXUniforms {
// vec4 #0
float vignette_strength;
float chroma_strength;
float chroma_min;
float scanline_strength;
float screen_height;
// vec4 #1
@@ -27,7 +27,7 @@ struct PostFXUniforms {
float pixel_scale;
float time;
float flicker;
float pad2;
float chroma_max;
// vec4 #3 — paràmetres de scanlines exposats al preset YAML
float scan_dark_ratio;
float scan_dark_floor;
@@ -92,7 +92,7 @@ namespace Rendering {
PostFXUniforms uniforms_{
.vignette_strength = 0.0F,
.chroma_strength = 0.0F,
.chroma_min = 0.0F,
.scanline_strength = 0.0F,
.screen_height = 0.0F,
.mask_strength = 0.0F,
@@ -102,7 +102,7 @@ namespace Rendering {
.pixel_scale = 1.0F,
.time = 0.0F,
.flicker = 0.0F,
.pad2 = 0.0F,
.chroma_max = 0.0F,
// Defaults canònics: 2/3 brillant + 1/3 fosca, brillantor 42%, suavitzat complet (1 px físic).
.scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F,
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff. Show more